aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Directory.Packages.props2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs81
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs62
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs46
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs8
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs2
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs16
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixAudioData.cs8
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs3
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs8
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs16
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs8
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs17
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs66
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs14
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs105
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs6
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs17
-rw-r--r--README.md8
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs10
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs5
26 files changed, 387 insertions, 135 deletions
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8d3dd064b..755d1427f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -60,7 +60,7 @@
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
- <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
+ <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index ee1598e7f..5291999dc 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
public abstract class BaseSqliteRepository : IDisposable
{
private bool _disposed = false;
+ private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+ private SqliteConnection _writeConnection;
/// <summary>
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
@@ -87,9 +90,55 @@ namespace Emby.Server.Implementations.Data
}
}
- protected SqliteConnection GetConnection()
+ protected ManagedConnection GetConnection(bool readOnly = false)
{
- var connection = new SqliteConnection($"Filename={DbFilePath}");
+ if (!readOnly)
+ {
+ _writeLock.Wait();
+ if (_writeConnection is not null)
+ {
+ return new ManagedConnection(_writeConnection, _writeLock);
+ }
+
+ var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
+ writeConnection.Open();
+
+ if (CacheSize.HasValue)
+ {
+ writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
+ }
+
+ if (!string.IsNullOrWhiteSpace(LockingMode))
+ {
+ writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
+ }
+
+ if (!string.IsNullOrWhiteSpace(JournalMode))
+ {
+ writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
+ }
+
+ if (JournalSizeLimit.HasValue)
+ {
+ writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
+ }
+
+ if (Synchronous.HasValue)
+ {
+ writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
+ }
+
+ if (PageSize.HasValue)
+ {
+ writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
+ }
+
+ writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
+
+ return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
+ }
+
+ var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
connection.Open();
if (CacheSize.HasValue)
@@ -124,17 +173,17 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
- return connection;
+ return new ManagedConnection(connection, null);
}
- public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
+ public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
{
var command = connection.CreateCommand();
command.CommandText = sql;
return command;
}
- protected bool TableExists(SqliteConnection connection, string name)
+ protected bool TableExists(ManagedConnection connection, string name)
{
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery())
@@ -148,7 +197,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
- protected List<string> GetColumnNames(SqliteConnection connection, string table)
+ protected List<string> GetColumnNames(ManagedConnection connection, string table)
{
var columnNames = new List<string>();
@@ -163,7 +212,7 @@ namespace Emby.Server.Implementations.Data
return columnNames;
}
- protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
+ protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
@@ -196,6 +245,24 @@ namespace Emby.Server.Implementations.Data
return;
}
+ if (dispose)
+ {
+ _writeLock.Wait();
+ try
+ {
+ _writeConnection.Dispose();
+ }
+ finally
+ {
+ _writeLock.Release();
+ }
+
+ _writeLock.Dispose();
+ }
+
+ _writeConnection = null;
+ _writeLock = null;
+
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
new file mode 100644
index 000000000..860950b30
--- /dev/null
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -0,0 +1,62 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.Data.Sqlite;
+
+namespace Emby.Server.Implementations.Data;
+
+public sealed class ManagedConnection : IDisposable
+{
+ private readonly SemaphoreSlim? _writeLock;
+
+ private SqliteConnection _db;
+
+ private bool _disposed = false;
+
+ public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
+ {
+ _db = db;
+ _writeLock = writeLock;
+ }
+
+ public SqliteTransaction BeginTransaction()
+ => _db.BeginTransaction();
+
+ public SqliteCommand CreateCommand()
+ => _db.CreateCommand();
+
+ public void Execute(string commandText)
+ => _db.Execute(commandText);
+
+ public SqliteCommand PrepareStatement(string sql)
+ => _db.PrepareStatement(sql);
+
+ public IEnumerable<SqliteDataReader> Query(string commandText)
+ => _db.Query(commandText);
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (_writeLock is null)
+ {
+ // Read connections are managed with an internal pool
+ _db.Dispose();
+ }
+ else
+ {
+ // Write lock is managed by BaseSqliteRepository
+ // Don't dispose here
+ _writeLock.Release();
+ }
+
+ _db = null!;
+
+ _disposed = true;
+ }
+}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index a4c6b2d31..81ee55d26 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -600,7 +600,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit();
}
- private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
+ private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
{
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@@ -1260,7 +1260,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1886,7 +1886,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
var chapters = new List<ChapterInfo>();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1905,7 +1905,7 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1979,7 +1979,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit();
}
- private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
+ private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
{
var startIndex = 0;
var limit = 100;
@@ -2468,7 +2468,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2536,7 +2536,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2744,7 +2744,7 @@ namespace Emby.Server.Implementations.Data
var list = new List<BaseItem>();
var result = new QueryResult<BaseItem>();
- using var connection = GetConnection();
+ using var connection = GetConnection(true);
using var transaction = connection.BeginTransaction();
if (!isReturningZeroItems)
{
@@ -2926,7 +2926,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -4475,7 +4475,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
transaction.Commit();
}
- private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
+ private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
{
using (var statement = PrepareStatement(db, query))
{
@@ -4508,7 +4508,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List<string>();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4546,7 +4546,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List<PersonInfo>();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4631,7 +4631,7 @@ AND Type = @InternalPersonType)");
return whereClauses;
}
- private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
+ private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
{
if (itemId.IsEmpty())
{
@@ -4786,7 +4786,7 @@ AND Type = @InternalPersonType)");
var list = new List<string>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
foreach (var row in statement.ExecuteQuery())
@@ -4986,8 +4986,8 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction(deferred: true))
+ using (var connection = GetConnection(true))
+ using (var transaction = connection.BeginTransaction())
{
if (!isReturningZeroItems)
{
@@ -5147,7 +5147,7 @@ AND Type = @InternalPersonType)");
return list;
}
- private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
+ private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
{
if (itemId.IsEmpty())
{
@@ -5166,7 +5166,7 @@ AND Type = @InternalPersonType)");
InsertItemValues(itemId, values, db);
}
- private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
+ private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5238,7 +5238,7 @@ AND Type = @InternalPersonType)");
transaction.Commit();
}
- private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
+ private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5334,7 +5334,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC";
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
{
var list = new List<MediaStream>();
@@ -5387,7 +5387,7 @@ AND Type = @InternalPersonType)");
transaction.Commit();
}
- private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
+ private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
{
const int Limit = 10;
var startIndex = 0;
@@ -5721,7 +5721,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC";
var list = new List<MediaAttachment>();
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, cmdText))
{
statement.TryBind("@ItemId", query.ItemId);
@@ -5771,7 +5771,7 @@ AND Type = @InternalPersonType)");
private void InsertMediaAttachments(
Guid id,
IReadOnlyList<MediaAttachment> attachments,
- SqliteConnection db,
+ ManagedConnection db,
CancellationToken cancellationToken)
{
const int InsertAtOnce = 10;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 20359e4ad..634eaf85e 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
+ private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
+ private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
{
var list = new List<Guid>();
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
+ private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key);
- using (var connection = GetConnection())
+ using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index e66f2496a..953fe19e0 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
+ public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 733ae2d1a..abf2d0115 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id,
SeriesName = series.Name,
- Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
+ Path = seasonParserResult.IsSeasonFolder ? path : null
};
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 34c9e86f2..c1a615666 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
}
string? presentationUniqueKey = null;
- int? limit = null;
+ int? limit = request.Limit;
if (!request.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index bff578feb..b6de67e88 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -179,7 +179,21 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
- await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
+ var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
+ if (newLib is CollectionFolder folder)
+ {
+ foreach (var child in folder.GetPhysicalFolders())
+ {
+ await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+ await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ // We don't know if this one can be validated individually, trigger a new validation
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
}
else
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
index 74f7e9c3e..a20253369 100644
--- a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
@@ -55,8 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines
{
try
{
+ _logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
File.Copy(dbPath, bakPath);
- _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
+ _logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
break;
}
catch (Exception ex)
@@ -80,7 +81,7 @@ namespace Jellyfin.Server.Migrations.Routines
{
IncludeItemTypes = [BaseItemKind.Audio],
StartIndex = startIndex,
- Limit = 100,
+ Limit = 5000,
SkipDeserialization = true
})
.Cast<Audio>()
@@ -97,7 +98,8 @@ namespace Jellyfin.Server.Migrations.Routines
}
_itemRepository.SaveItems(results, CancellationToken.None);
- startIndex += 100;
+ startIndex += results.Count;
+ _logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 0f2ec1f8f..fcb45e7e5 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities
if (IsFileProtocol)
{
- IEnumerable<BaseItem> nonCachedChildren;
+ IEnumerable<BaseItem> nonCachedChildren = [];
try
{
@@ -373,7 +373,6 @@ namespace MediaBrowser.Controller.Entities
catch (Exception ex)
{
Logger.LogError(ex, "Error retrieving children folder");
- return;
}
progress.Report(ProgressHelpers.RetrievedChildren);
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 37703ceee..b802b7e6e 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -149,6 +149,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
+ /// <summary>
+ /// Reloads the root media folder.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="removeRoot">Is remove the library itself allowed.</param>
+ /// <returns>Task.</returns>
+ Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
+
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary>
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 7fe2f64af..56b07ebae 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
+ public List<FileSystemMetadata> GetDirectories(string path)
+ {
+ var list = new List<FileSystemMetadata>();
+ var items = GetFileSystemEntries(path);
+ for (var i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ if (item.IsDirectory)
+ {
+ list.Add(item);
+ }
+ }
+
+ return list;
+ }
+
public List<FileSystemMetadata> GetFiles(string path)
{
var list = new List<FileSystemMetadata>();
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 6d7550ab5..a3c06cde5 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
{
FileSystemMetadata[] GetFileSystemEntries(string path);
+ List<FileSystemMetadata> GetDirectories(string path);
+
List<FileSystemMetadata> GetFiles(string path);
FileSystemMetadata? GetFile(string path);
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index eb5069b06..b52f16edc 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -141,6 +141,14 @@ namespace MediaBrowser.Controller.Providers
where T : BaseItem;
/// <summary>
+ /// Gets the metadata savers for the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <returns>The metadata savers.</returns>
+ IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
+
+ /// <summary>
/// Gets all metadata plugins.
/// </summary>
/// <returns>IEnumerable{MetadataPlugin}.</returns>
diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
index a8e2946f1..6a5e3bf04 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -38,19 +38,28 @@ namespace MediaBrowser.LocalMetadata.Images
}
var parentPathFiles = directoryService.GetFiles(parentPath);
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
- var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
+ var thumbName = string.Concat(nameWithoutExtension, "-thumb");
+ var images = GetImageFilesFromFolder(thumbName, parentPathFiles);
- return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
+ var metadataSubPath = directoryService.GetDirectories(parentPath).Where(d => d.Name.EndsWith("metadata", StringComparison.OrdinalIgnoreCase)).ToList();
+ foreach (var path in metadataSubPath)
+ {
+ var files = directoryService.GetFiles(path.FullName);
+ images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
+ }
+
+ return images;
}
- private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
+ private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
{
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1);
- foreach (var i in parentPathFiles)
+ foreach (var i in filePaths)
{
if (i.IsDirectory)
{
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index d82716831..e0677aa9f 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -100,8 +100,8 @@ namespace MediaBrowser.Providers.Manager
{
saveLocally = false;
- // If season is virtual under a physical series, save locally if using compatible convention
- if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
+ // If season is virtual under a physical series, save locally
+ if (item is Season season)
{
var series = season.Series;
@@ -126,7 +126,11 @@ namespace MediaBrowser.Providers.Manager
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
- var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
+ string[] retryPaths = [];
+ if (saveLocally)
+ {
+ retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
+ }
// If there are more than one output paths, the stream will need to be seekable
if (paths.Length > 1 && !source.CanSeek)
@@ -183,6 +187,13 @@ namespace MediaBrowser.Providers.Manager
try
{
_fileSystem.DeleteFile(currentPath);
+
+ // Remove containing directory if empty
+ var folder = Path.GetDirectoryName(currentPath);
+ if (!_fileSystem.GetFiles(folder).Any())
+ {
+ Directory.Delete(folder);
+ }
}
catch (FileNotFoundException)
{
@@ -374,6 +385,45 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
}
+ if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ extension = ".jpg";
+ }
+
+ extension = extension.ToLowerInvariant();
+
+ if (type == ImageType.Primary && saveLocally)
+ {
+ if (season is not null && season.IndexNumber.HasValue)
+ {
+ var seriesFolder = season.SeriesPath;
+
+ var seasonMarker = season.IndexNumber.Value == 0
+ ? "-specials"
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
+
+ var imageFilename = "season" + seasonMarker + "-poster" + extension;
+
+ return Path.Combine(seriesFolder, imageFilename);
+ }
+ }
+
+ if (type == ImageType.Backdrop && saveLocally)
+ {
+ if (season is not null && season.IndexNumber.HasValue)
+ {
+ var seriesFolder = season.SeriesPath;
+
+ var seasonMarker = season.IndexNumber.Value == 0
+ ? "-specials"
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
+
+ var imageFilename = "season" + seasonMarker + "-fanart" + extension;
+
+ return Path.Combine(seriesFolder, imageFilename);
+ }
+ }
+
if (type == ImageType.Thumb && saveLocally)
{
if (season is not null && season.IndexNumber.HasValue)
@@ -447,20 +497,12 @@ namespace MediaBrowser.Providers.Manager
break;
}
- if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
- {
- extension = ".jpg";
- }
-
- extension = extension.ToLowerInvariant();
-
string path = null;
-
if (saveLocally)
{
if (type == ImageType.Primary && item is Episode)
{
- path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
+ path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
}
else if (item.IsInMixedFolder)
{
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 1a5dbd7a5..bee420d95 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -371,12 +371,21 @@ namespace MediaBrowser.Providers.Manager
}
catch (FileNotFoundException)
{
- // nothing to do, already gone
+ // Nothing to do, already gone
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
}
+ finally
+ {
+ // Always remove empty parent folder
+ var folder = Path.GetDirectoryName(image.Path);
+ if (Directory.Exists(folder) && !_fileSystem.GetFiles(folder).Any())
+ {
+ Directory.Delete(folder);
+ }
+ }
}
}
@@ -419,7 +428,8 @@ namespace MediaBrowser.Providers.Manager
var type = _singularImages[i];
var image = GetFirstLocalImageInfoByType(images, type);
- if (image is not null)
+ // Only use local images if we are not replacing and saving
+ if (image is not null && !(item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages))
{
var currentImage = item.GetImageInfo(type, 0);
// if image file is stored with media, don't replace that later
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 0a98967da..61a4d7586 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -154,7 +154,8 @@ namespace MediaBrowser.Providers.Manager
id.IsAutomated = refreshOptions.IsAutomated;
- var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
+ var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
+ var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -639,6 +640,7 @@ namespace MediaBrowser.Providers.Manager
MetadataRefreshOptions options,
ICollection<IMetadataProvider> providers,
ItemImageProvider imageService,
+ bool isSavingMetadata,
CancellationToken cancellationToken)
{
var refreshResult = new RefreshResult
@@ -669,69 +671,74 @@ namespace MediaBrowser.Providers.Manager
temp.Item.Id = item.Id;
var foundImageTypes = new List<ImageType>();
- foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
- {
- var providerName = provider.GetType().Name;
- Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
- var itemInfo = new ItemInfo(item);
-
- try
+ // Do not execute local providers if we are identifying or replacing with local metadata saving enabled
+ if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
+ {
+ foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{
- var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
+ var providerName = provider.GetType().Name;
+ Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
+
+ var itemInfo = new ItemInfo(item);
- if (localItem.HasMetadata)
+ try
{
- foreach (var remoteImage in localItem.RemoteImages)
+ var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
+
+ if (localItem.HasMetadata)
{
- try
+ foreach (var remoteImage in localItem.RemoteImages)
{
- if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
- && !options.IsReplacingImage(remoteImage.Type))
+ try
{
- continue;
- }
+ if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
+ && !options.IsReplacingImage(remoteImage.Type))
+ {
+ continue;
+ }
- await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
- refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
+ refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
- // remember imagetype that has just been downloaded
- foundImageTypes.Add(remoteImage.Type);
+ // remember imagetype that has just been downloaded
+ foundImageTypes.Add(remoteImage.Type);
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
+ }
}
- catch (HttpRequestException ex)
+
+ if (foundImageTypes.Count > 0)
{
- Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
+ imageService.UpdateReplaceImages(options, foundImageTypes);
}
- }
- if (foundImageTypes.Count > 0)
- {
- imageService.UpdateReplaceImages(options, foundImageTypes);
- }
+ if (imageService.MergeImages(item, localItem.Images, options))
+ {
+ refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ }
- if (imageService.MergeImages(item, localItem.Images, options))
- {
- refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
- }
+ MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
+ refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
- MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
- refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
+ break;
+ }
- break;
+ Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error in {Provider}", provider.Name);
- Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error in {Provider}", provider.Name);
-
- // If a local provider fails, consider that a failure
- refreshResult.ErrorMessage = ex.Message;
+ // If a local provider fails, consider that a failure
+ refreshResult.ErrorMessage = ex.Message;
+ }
}
}
@@ -763,7 +770,7 @@ namespace MediaBrowser.Providers.Manager
else
{
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
- MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
+ MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
}
}
}
@@ -1050,7 +1057,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
+ target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
}
}
@@ -1080,7 +1087,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
+ target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
}
MergeAlbumArtist(source, target, replaceData);
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 275f4028d..f2ca99da6 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
}
+ /// <inheritdoc />
+ public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
+ {
+ return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
+ }
+
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
where T : BaseItem
{
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 41c6bd8f2..2389bce57 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -119,7 +119,8 @@ namespace MediaBrowser.Providers.TV
virtualSeason,
new DeleteOptions
{
- DeleteFileLocation = true
+ // Internal metadata paths are removed regardless of this.
+ DeleteFileLocation = false
},
false);
}
@@ -176,7 +177,8 @@ namespace MediaBrowser.Providers.TV
episode,
new DeleteOptions
{
- DeleteFileLocation = true
+ // Internal metadata paths are removed regardless of this.
+ DeleteFileLocation = false
},
false);
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index b25cfc83f..a547779de 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -825,7 +825,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
private string GetOutputTrailerUrl(string url)
{
// This is what xbmc expects
- return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase);
+ return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase);
}
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 8fa22fad9..bc344d87e 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -45,27 +45,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
{
+ var path = item.ContainingFolderPath;
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
{
- var path = item.ContainingFolderPath;
-
yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo");
}
- if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
+ // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
+ if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{
- var path = item.ContainingFolderPath;
+ yield return Path.Combine(path, "movie.nfo");
+ }
+ if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
+ {
yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
}
else
{
- // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
- if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
- {
- yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
- }
-
yield return Path.ChangeExtension(item.Path, ".nfo");
}
}
diff --git a/README.md b/README.md
index 36828f59a..e5e16c716 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,6 @@
<a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/?utm_source=widget">
<img alt="Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-core/svg-badge.svg"/>
</a>
-<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=29">
-<img alt="Azure Builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20Server"/>
-</a>
<a href="https://hub.docker.com/r/jellyfin/jellyfin">
<img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/>
</a>
@@ -30,10 +27,7 @@
<img alt="Submit Feature Requests" src="https://img.shields.io/badge/fider-vote%20on%20features-success.svg"/>
</a>
<a href="https://matrix.to/#/#jellyfinorg:matrix.org">
-<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
-</a>
-<a href="https://www.reddit.com/r/jellyfin">
-<img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/>
+<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfinorg:matrix.org.svg?logo=matrix"/>
</a>
<a href="https://github.com/jellyfin/jellyfin/releases.atom">
<img alt="Release RSS Feed" src="https://img.shields.io/badge/rss-releases-ffa500?logo=rss" />
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index be5a401b1..d6284e4a1 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -575,18 +575,22 @@ namespace Jellyfin.Providers.Tests.Manager
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
- var item = new Video();
+ var item = new Mock<Video>
+ {
+ CallBase = true
+ };
+ item.Setup(m => m.IsSaveLocalMetadataEnabled()).Returns(false);
var path = validPaths ? _testDataImagePath.Format : "invalid path {0}";
for (int i = 0; i < count; i++)
{
- item.SetImagePath(type, i, new FileSystemMetadata
+ item.Object.SetImagePath(type, i, new FileSystemMetadata
{
FullName = string.Format(CultureInfo.InvariantCulture, path, i),
});
}
- return item;
+ return item.Object;
}
private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths)
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
index 8019e0ab3..2f05c4ea2 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
@@ -47,6 +47,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
var movie = new Movie() { Path = "/media/movies/Avengers Endgame", VideoType = VideoType.Dvd };
var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
var path2 = "/media/movies/Avengers Endgame/VIDEO_TS/VIDEO_TS.nfo";
+ var path3 = "/media/movies/Avengers Endgame/movie.nfo";
// uses ContainingFolderPath which uses Operating system specific paths
if (OperatingSystem.IsWindows())
@@ -54,12 +55,14 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\');
path2 = path2.Replace('/', '\\');
+ path3 = path3.Replace('/', '\\');
}
var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
- Assert.Equal(2, paths.Length);
+ Assert.Equal(3, paths.Length);
Assert.Contains(path1, paths);
Assert.Contains(path2, paths);
+ Assert.Contains(path3, paths);
}
}
}