aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Library')
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs4
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs77
-rw-r--r--Emby.Server.Implementations/Library/ExternalDataManager.cs58
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs1
-rw-r--r--Emby.Server.Implementations/Library/KeyframeManager.cs44
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs448
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs86
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs88
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs29
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs101
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs20
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs5
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs32
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs149
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs43
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs157
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs218
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs136
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs117
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs181
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs69
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs151
29 files changed, 1477 insertions, 964 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index b01fd93a7..f29a0b3ad 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
{
if (parent is not null)
{
- // Ignore extras folders but allow it at the collection level
+ // Ignore extras for unsupported types
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
&& parent is not AggregateFolder
&& parent is not UserRootFolder)
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library
{
if (parent is not null)
{
- // Don't resolve these into audio files
+ // Don't resolve theme songs
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
{
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
new file mode 100644
index 000000000..b0ed1de8d
--- /dev/null
+++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// Resolver rule class for ignoring files via .ignore.
+/// </summary>
+public class DotIgnoreIgnoreRule : IResolverIgnoreRule
+{
+ private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
+ {
+ var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore"));
+ if (ignoreFile.Exists)
+ {
+ return ignoreFile;
+ }
+
+ var parentDir = directory.Parent;
+ if (parentDir is null)
+ {
+ return null;
+ }
+
+ return FindIgnoreFile(parentDir);
+ }
+
+ /// <inheritdoc />
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
+ {
+ return IsIgnored(fileInfo, parent);
+ }
+
+ /// <summary>
+ /// Checks whether or not the file is ignored.
+ /// </summary>
+ /// <param name="fileInfo">The file information.</param>
+ /// <param name="parent">The parent BaseItem.</param>
+ /// <returns>True if the file should be ignored.</returns>
+ public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
+ {
+ var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
+ if (string.IsNullOrEmpty(parentDirPath))
+ {
+ return false;
+ }
+
+ var folder = new DirectoryInfo(parentDirPath);
+ var ignoreFile = FindIgnoreFile(folder);
+ if (ignoreFile is null)
+ {
+ return false;
+ }
+
+ string ignoreFileString;
+ using (var reader = ignoreFile.OpenText())
+ {
+ ignoreFileString = reader.ReadToEnd();
+ }
+
+ if (string.IsNullOrEmpty(ignoreFileString))
+ {
+ // Ignore directory if we just have the file
+ return true;
+ }
+
+ // If file has content, base ignoring off the content .gitignore-style rules
+ var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ var ignore = new Ignore.Ignore();
+ ignore.Add(ignoreRules);
+
+ return ignore.IsIgnored(fileInfo.FullName);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/ExternalDataManager.cs b/Emby.Server.Implementations/Library/ExternalDataManager.cs
new file mode 100644
index 000000000..68e3aaff4
--- /dev/null
+++ b/Emby.Server.Implementations/Library/ExternalDataManager.cs
@@ -0,0 +1,58 @@
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.MediaSegments;
+using MediaBrowser.Controller.Trickplay;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IExternalDataManager implementation.
+/// </summary>
+public class ExternalDataManager : IExternalDataManager
+{
+ private readonly IKeyframeManager _keyframeManager;
+ private readonly IMediaSegmentManager _mediaSegmentManager;
+ private readonly IPathManager _pathManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
+ /// </summary>
+ /// <param name="keyframeManager">The keyframe manager.</param>
+ /// <param name="mediaSegmentManager">The media segment manager.</param>
+ /// <param name="pathManager">The path manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ public ExternalDataManager(
+ IKeyframeManager keyframeManager,
+ IMediaSegmentManager mediaSegmentManager,
+ IPathManager pathManager,
+ ITrickplayManager trickplayManager)
+ {
+ _keyframeManager = keyframeManager;
+ _mediaSegmentManager = mediaSegmentManager;
+ _pathManager = pathManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <inheritdoc/>
+ public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
+ {
+ var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
+ var itemId = item.Id;
+ if (validPaths.Count > 0)
+ {
+ foreach (var path in validPaths)
+ {
+ Directory.Delete(path, true);
+ }
+ }
+
+ await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+ await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
+ await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index bb45dd87e..25ddade82 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using DotNet.Globbing;
namespace Emby.Server.Implementations.Library
diff --git a/Emby.Server.Implementations/Library/KeyframeManager.cs b/Emby.Server.Implementations/Library/KeyframeManager.cs
new file mode 100644
index 000000000..18f4ce047
--- /dev/null
+++ b/Emby.Server.Implementations/Library/KeyframeManager.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// Manager for Keyframe data.
+/// </summary>
+public class KeyframeManager : IKeyframeManager
+{
+ private readonly IKeyframeRepository _repository;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="KeyframeManager"/> class.
+ /// </summary>
+ /// <param name="repository">The keyframe repository.</param>
+ public KeyframeManager(IKeyframeRepository repository)
+ {
+ _repository = repository;
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
+ {
+ return _repository.GetKeyframeData(itemId);
+ }
+
+ /// <inheritdoc />
+ public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
+ {
+ await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
+ {
+ await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 2d1af82b3..d03c614cf 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2,7 +2,6 @@
#pragma warning disable CA5394
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -11,6 +10,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Server.Implementations.Library.Resolvers;
@@ -18,8 +18,10 @@ using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
using Emby.Server.Implementations.Sorting;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -32,10 +34,12 @@ using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
@@ -62,21 +66,23 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
- private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataRepository;
+ private readonly IUserDataManager _userDataManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
private readonly Lazy<IProviderManager> _providerManagerFactory;
- private readonly Lazy<IUserViewManager> _userviewManagerFactory;
+ private readonly Lazy<IUserViewManager> _userViewManagerFactory;
private readonly IServerApplicationHost _appHost;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
+ private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
+ private readonly IPathManager _pathManager;
+ private readonly FastConcurrentLru<Guid, BaseItem> _cache;
/// <summary>
/// The _root folder sync lock.
@@ -102,54 +108,61 @@ namespace Emby.Server.Implementations.Library
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
- /// <param name="userDataRepository">The user data repository.</param>
+ /// <param name="userDataManager">The user data manager.</param>
/// <param name="libraryMonitorFactory">The library monitor.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
- /// <param name="userviewManagerFactory">The userview manager.</param>
+ /// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="peopleRepository">The people repository.</param>
+ /// <param name="pathManager">The path manager.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
ITaskManager taskManager,
IUserManager userManager,
IServerConfigurationManager configurationManager,
- IUserDataManager userDataRepository,
+ IUserDataManager userDataManager,
Lazy<ILibraryMonitor> libraryMonitorFactory,
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
- Lazy<IUserViewManager> userviewManagerFactory,
+ Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IPeopleRepository peopleRepository,
+ IPathManager pathManager)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
_taskManager = taskManager;
_userManager = userManager;
_configurationManager = configurationManager;
- _userDataRepository = userDataRepository;
+ _userDataManager = userDataManager;
_libraryMonitorFactory = libraryMonitorFactory;
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
- _userviewManagerFactory = userviewManagerFactory;
+ _userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _cache = new ConcurrentDictionary<Guid, BaseItem>();
- _namingOptions = namingOptions;
+ _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
+
+ _namingOptions = namingOptions;
+ _peopleRepository = peopleRepository;
+ _pathManager = pathManager;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
- RecordConfigurationValues(configurationManager.Configuration);
+ RecordConfigurationValues(_configurationManager.Configuration);
}
/// <summary>
@@ -191,39 +204,39 @@ namespace Emby.Server.Implementations.Library
private IProviderManager ProviderManager => _providerManagerFactory.Value;
- private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+ private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
+ private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
+ private IIntroProvider[] IntroProviders { get; set; } = [];
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
+ private IItemResolver[] EntityResolvers { get; set; } = [];
- private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
+ private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
+ private IBaseItemComparer[] Comparers { get; set; } = [];
public bool IsScanRunning { get; private set; }
@@ -234,20 +247,20 @@ namespace Emby.Server.Implementations.Library
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The post scan tasks.</param>
+ /// <param name="postScanTasks">The post scan tasks.</param>
public void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks)
+ IEnumerable<ILibraryPostScanTask> postScanTasks)
{
EntityResolutionIgnoreRules = rules.ToArray();
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
IntroProviders = introProviders.ToArray();
Comparers = itemComparers.ToArray();
- PostscanTasks = postscanTasks.ToArray();
+ PostScanTasks = postScanTasks.ToArray();
}
/// <summary>
@@ -297,7 +310,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _cache[item.Id] = item;
+ _cache.AddOrUpdate(item.Id, item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -356,7 +369,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
- : Array.Empty<BaseItem>();
+ : [];
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -382,7 +395,7 @@ namespace Emby.Server.Implementations.Library
}
}
- if (options.DeleteFileLocation && item.IsFileProtocol)
+ if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
{
// Assume only the first is required
// Add this flag to GetDeletePaths if required in the future
@@ -451,24 +464,85 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
_itemRepository.DeleteItem(item.Id);
+ _cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
+ _cache.TryRemove(child.Id, out _);
}
- _cache.TryRemove(item.Id, out _);
-
ReportItemRemoved(item, parent);
}
- private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ private bool IsInternalItem(BaseItem item)
+ {
+ if (!item.IsFileProtocol)
+ {
+ return false;
+ }
+
+ var pathToCheck = item switch
+ {
+ Genre => _configurationManager.ApplicationPaths.GenrePath,
+ MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
+ MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
+ Person => _configurationManager.ApplicationPaths.PeoplePath,
+ Studio => _configurationManager.ApplicationPaths.StudioPath,
+ Year => _configurationManager.ApplicationPaths.YearPath,
+ _ => null
+ };
+
+ var itemPath = item.Path;
+ if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
+ {
+ var cleanPath = _fileSystem.GetValidFilename(itemPath);
+ var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
+
+ return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
+ }
+
+ return false;
+ }
+
+ private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ {
+ var list = GetInternalMetadataPaths(item);
+ foreach (var child in children)
+ {
+ list.AddRange(GetInternalMetadataPaths(child));
+ }
+
+ return list;
+ }
+
+ private List<string> GetInternalMetadataPaths(BaseItem item)
{
var list = new List<string>
{
item.GetInternalMetadataPath()
};
- list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
+ if (item is Video video)
+ {
+ // Trickplay
+ list.Add(_pathManager.GetTrickplayDirectory(video));
+
+ // Subtitles and attachments
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
+ if (subtitleFolder is not null)
+ {
+ list.Add(subtitleFolder);
+ }
+
+ var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (attachmentFolder is not null)
+ {
+ list.Add(attachmentFolder);
+ }
+ }
+ }
return list;
}
@@ -589,7 +663,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
- files = Array.Empty<FileSystemMetadata>();
+ files = [];
}
else
{
@@ -597,7 +671,7 @@ namespace Emby.Server.Implementations.Library
}
}
- // Need to remove subpaths that may have been resolved from shortcuts
+ // Need to remove sub-paths that may have been resolved from shortcuts
// Example: if \\server\movies exists, then strip out \\server\movies\action
if (isPhysicalRoot)
{
@@ -607,10 +681,11 @@ namespace Emby.Server.Implementations.Library
args.FileSystemChildren = files;
}
- // Check to see if we should resolve based on our contents
- if (args.IsDirectory && !ShouldResolvePathContents(args))
+ // Filter content based on ignore rules
+ if (args.IsDirectory)
{
- return null;
+ var filtered = args.GetActualFileSystemChildren().ToArray();
+ args.FileSystemChildren = filtered ?? [];
}
return ResolveItem(args, resolvers);
@@ -625,10 +700,10 @@ namespace Emby.Server.Implementations.Library
var list = originalList.Where(i => i.IsDirectory)
.Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
- .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Distinct()
.ToList();
- var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath)))
+ var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath)))
.ToList();
foreach (var dupe in dupes)
@@ -636,22 +711,11 @@ namespace Emby.Server.Implementations.Library
_logger.LogInformation("Found duplicate path: {0}", dupe);
}
- var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList();
+ var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
newList.AddRange(originalList.Where(i => !i.IsDirectory));
return newList;
}
- /// <summary>
- /// Determines whether a path should be ignored based on its contents - called after the contents have been read.
- /// </summary>
- /// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- private static bool ShouldResolvePathContents(ItemResolveArgs args)
- {
- // Ignore any folders containing a file called .ignore
- return !args.ContainsFileSystemEntryByName(".ignore");
- }
-
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
{
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
@@ -726,8 +790,6 @@ namespace Emby.Server.Implementations.Library
{
var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
- Directory.CreateDirectory(rootFolderPath);
-
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
(ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong"))
.DeepCopy<Folder, AggregateFolder>();
@@ -742,23 +804,17 @@ namespace Emby.Server.Implementations.Library
// Add in the plug-in folders
var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
Folder folder = new PlaylistsFolder
{
- Path = path
+ Path = path,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
};
if (folder.Id.IsEmpty())
{
- if (string.IsNullOrEmpty(folder.Path))
- {
- folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
- }
- else
- {
- folder.Id = GetNewItemId(folder.Path, folder.GetType());
- }
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
}
var dbItem = GetItemById(folder.Id) as BasePluginFolder;
@@ -839,7 +895,7 @@ namespace Emby.Server.Implementations.Library
{
Path = path,
IsFolder = isFolder,
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
Limit = 1,
DtoOptions = new DtoOptions(true)
};
@@ -945,7 +1001,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
+ IncludeItemTypes = [BaseItemKind.MusicArtist],
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@@ -964,12 +1020,13 @@ namespace Emby.Server.Implementations.Library
var item = GetItemById(id) as T;
if (item is null)
{
+ var info = Directory.CreateDirectory(path);
item = new T
{
Name = name,
Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Path = path
};
@@ -1053,9 +1110,17 @@ namespace Emby.Server.Implementations.Library
cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
- foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
+ foreach (var child in GetUserRootFolder().Children.OfType<Folder>())
{
- await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ // If the user has somehow deleted the collection directory, remove the metadata from the database.
+ if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
+ {
+ _itemRepository.DeleteItem(collectionFolder.Id);
+ }
+ else
+ {
+ await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
}
}
@@ -1087,7 +1152,7 @@ namespace Emby.Server.Implementations.Library
/// <returns>Task.</returns>
private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
{
- var tasks = PostscanTasks.ToList();
+ var tasks = PostScanTasks.ToList();
var numComplete = 0;
var numTasks = tasks.Count;
@@ -1210,7 +1275,7 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
- var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
+ var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
foreach (ReadOnlySpan<char> file in files)
{
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
@@ -1230,7 +1295,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_cache.TryGetValue(id, out BaseItem? item))
+ if (_cache.TryGet(id, out var item))
{
return item;
}
@@ -1247,7 +1312,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public T? GetItemById<T>(Guid id)
- where T : BaseItem
+ where T : BaseItem
{
var item = GetItemById(id);
if (item is T typedItem)
@@ -1274,14 +1339,14 @@ namespace Emby.Server.Implementations.Library
return ItemIsVisible(item, user) ? item : null;
}
- public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new[] { parent });
+ SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1300,7 +1365,7 @@ namespace Emby.Server.Implementations.Library
return itemList;
}
- public List<BaseItem> GetItemList(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
{
return GetItemList(query, true);
}
@@ -1312,7 +1377,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new[] { parent });
+ SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1324,7 +1389,7 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetCount(query);
}
- public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -1339,6 +1404,36 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemList(query);
}
+ public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, CollectionType collectionType)
+ {
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return _itemRepository.GetLatestItemList(query, collectionType);
+ }
+
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff)
+ {
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
+ }
+
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
{
if (query.User is not null)
@@ -1357,7 +1452,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.GetItemList(query));
}
- public List<Guid> GetItemIds(InternalItemsQuery query)
+ public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
{
if (query.User is not null)
{
@@ -1443,7 +1538,7 @@ namespace Emby.Server.Implementations.Library
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
- query.AncestorIds = Array.Empty<Guid>();
+ query.AncestorIds = [];
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
@@ -1470,7 +1565,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
- SetTopParentIdsOrAncestors(query, new[] { parent });
+ SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1500,7 +1595,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid() };
+ query.TopParentIds = [Guid.NewGuid()];
}
}
else
@@ -1511,7 +1606,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
- query.AncestorIds = new[] { Guid.NewGuid() };
+ query.AncestorIds = [Guid.NewGuid()];
}
}
@@ -1540,7 +1635,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid() };
+ query.TopParentIds = [Guid.NewGuid()];
}
}
}
@@ -1551,7 +1646,7 @@ namespace Emby.Server.Implementations.Library
{
if (view.ViewType == CollectionType.livetv)
{
- return new[] { view.Id };
+ return [view.Id];
}
// Translate view into folders
@@ -1563,7 +1658,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
if (!view.ParentId.IsEmpty())
@@ -1574,7 +1669,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
// Handle grouping
@@ -1589,7 +1684,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
- return Array.Empty<Guid>();
+ return [];
}
if (item is CollectionFolder collectionFolder)
@@ -1600,10 +1695,10 @@ namespace Emby.Server.Implementations.Library
var topParent = item.GetTopParent();
if (topParent is not null)
{
- return new[] { topParent.Id };
+ return [topParent.Id];
}
- return Array.Empty<Guid>();
+ return [];
}
/// <summary>
@@ -1647,7 +1742,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
- return Enumerable.Empty<IntroInfo>();
+ return [];
}
}
@@ -1796,7 +1891,7 @@ namespace Emby.Server.Implementations.Library
userComparer.User = user;
userComparer.UserManager = _userManager;
- userComparer.UserDataRepository = _userDataRepository;
+ userComparer.UserDataManager = _userDataManager;
return userComparer;
}
@@ -1807,7 +1902,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent)
{
- CreateItems(new[] { item }, parent, CancellationToken.None);
+ CreateItems([item], parent, CancellationToken.None);
}
/// <inheritdoc />
@@ -1955,13 +2050,13 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
+ _itemRepository.SaveItems(items, cancellationToken);
+
foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
- _itemRepository.SaveItems(items, cancellationToken);
-
if (ItemUpdated is not null)
{
foreach (var item in items)
@@ -1993,7 +2088,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
- => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
+ => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
@@ -2222,13 +2317,13 @@ namespace Emby.Server.Implementations.Library
if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName
@@ -2270,13 +2365,13 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName,
@@ -2334,20 +2429,19 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
- ForcedSortName = sortName
+ ForcedSortName = sortName,
+ DisplayParentId = parentId
};
- item.DisplayParentId = parentId;
-
CreateItem(item, null);
isNew = true;
@@ -2404,20 +2498,19 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
- Directory.CreateDirectory(path);
-
+ var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
- DateCreated = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
- ForcedSortName = sortName
+ ForcedSortName = sortName,
+ DisplayParentId = parentId
};
- item.DisplayParentId = parentId;
-
CreateItem(item, null);
isNew = true;
@@ -2474,8 +2567,11 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public int? GetSeasonNumberFromPath(string path)
- => SeasonPathParser.Parse(path, true, true).SeasonNumber;
+ public int? GetSeasonNumberFromPath(string path, Guid? parentId)
+ {
+ var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
+ return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
+ }
/// <inheritdoc />
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
@@ -2492,7 +2588,6 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- // TODO nullable - what are we trying to do there with empty episodeInfo?
EpisodeInfo? episodeInfo = null;
if (episode.IsFileProtocol)
{
@@ -2510,44 +2605,12 @@ namespace Emby.Server.Implementations.Library
}
}
- episodeInfo ??= new EpisodeInfo(episode.Path);
-
- try
- {
- var libraryOptions = GetLibraryOptions(episode);
- if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- // Read from metadata
- var mediaInfo = _mediaEncoder.GetMediaInfo(
- new MediaInfoRequest
- {
- MediaSource = episode.GetMediaSources(false)[0],
- MediaType = DlnaProfileType.Video
- },
- CancellationToken.None).GetAwaiter().GetResult();
- if (mediaInfo.ParentIndexNumber > 0)
- {
- episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
- }
-
- if (mediaInfo.IndexNumber > 0)
- {
- episodeInfo.EpisodeNumber = mediaInfo.IndexNumber;
- }
-
- if (!string.IsNullOrEmpty(mediaInfo.ShowName))
- {
- episodeInfo.SeriesName = mediaInfo.ShowName;
- }
- }
- }
- catch (Exception ex)
+ var changed = false;
+ if (episodeInfo is null)
{
- _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path);
+ return changed;
}
- var changed = false;
-
if (episodeInfo.IsByDate)
{
if (episode.IndexNumber.HasValue)
@@ -2626,15 +2689,6 @@ namespace Emby.Server.Implementations.Library
{
episode.ParentIndexNumber = season.IndexNumber;
}
- else
- {
- /*
- Anime series don't generally have a season in their file name, however,
- TVDb needs a season to correctly get the metadata.
- Hence, a null season needs to be filled with something. */
- // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
- episode.ParentIndexNumber = 1;
- }
if (episode.ParentIndexNumber.HasValue)
{
@@ -2659,27 +2713,33 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
- var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ // Apply .ignore rules
+ var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
}
- var count = fileSystemChildren.Count;
+ var count = filtered.Count;
for (var i = 0; i < count; i++)
{
- var current = fileSystemChildren[i];
+ var current = filtered[i];
if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
{
var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
- foreach (var file in filesInSubFolder)
+ var filesInSubFolderList = filesInSubFolder.ToList();
+
+ bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
+
+ foreach (var file in filesInSubFolderList)
{
if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
{
continue;
}
- var extra = GetExtra(file, extraType.Value);
+ var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
if (extra is not null)
{
yield return extra;
@@ -2688,7 +2748,7 @@ namespace Emby.Server.Implementations.Library
}
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType))
{
- var extra = GetExtra(current, extraType.Value);
+ var extra = GetExtra(current, extraType.Value, false);
if (extra is not null)
{
yield return extra;
@@ -2696,7 +2756,7 @@ namespace Emby.Server.Implementations.Library
}
}
- BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType)
+ BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
{
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
if (extra is not Video && extra is not Audio)
@@ -2719,6 +2779,7 @@ namespace Emby.Server.Implementations.Library
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
+ extra.IsInMixedFolder = isInMixedFolder;
return extra;
}
}
@@ -2736,12 +2797,12 @@ namespace Emby.Server.Implementations.Library
return path;
}
- public List<PersonInfo> GetPeople(InternalPeopleQuery query)
+ public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{
- return _itemRepository.GetPeople(query);
+ return _peopleRepository.GetPeople(query);
}
- public List<PersonInfo> GetPeople(BaseItem item)
+ public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
{
if (item.SupportsPeople)
{
@@ -2756,12 +2817,12 @@ namespace Emby.Server.Implementations.Library
}
}
- return new List<PersonInfo>();
+ return [];
}
- public List<Person> GetPeopleItems(InternalPeopleQuery query)
+ public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query)
+ return _peopleRepository.GetPeopleNames(query)
.Select(i =>
{
try
@@ -2779,9 +2840,9 @@ namespace Emby.Server.Implementations.Library
.ToList()!; // null values are filtered out
}
- public List<string> GetPeopleNames(InternalPeopleQuery query)
+ public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query);
+ return _peopleRepository.GetPeopleNames(query);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@@ -2790,16 +2851,17 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
+ public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
return;
}
- _itemRepository.UpdatePeople(item.Id, people);
if (people is not null)
{
+ people = people.Where(e => e is not null).ToArray();
+ _peopleRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
}
@@ -2848,7 +2910,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(name));
}
- name = _fileSystem.GetValidFilename(name);
+ name = _fileSystem.GetValidFilename(name.Trim());
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
@@ -2882,7 +2944,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
- await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
+ FileHelper.CreateEmpty(path);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2914,30 +2976,32 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
- List<BaseItem>? personsToSave = null;
-
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
+ var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
{
var path = Person.GetPath(person.Name);
+ var info = Directory.CreateDirectory(path);
+ var lastWriteTime = info.LastWriteTimeUtc;
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
+ DateCreated = info.CreationTimeUtc,
+ DateModified = lastWriteTime,
Path = path
};
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
+ createEntity = true;
}
foreach (var id in person.ProviderIds)
@@ -2965,15 +3029,15 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
- (personsToSave ??= new()).Add(personEntity);
+ if (createEntity)
+ {
+ CreateItems([personEntity], null, CancellationToken.None);
+ }
+
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ CreateItems([personEntity], null, CancellationToken.None);
}
}
-
- if (personsToSave is not null)
- {
- CreateItems(personsToSave, null, CancellationToken.None);
- }
}
private void StartScanInBackground()
@@ -3027,7 +3091,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
- libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
+ libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 90a01c052..ab30971e2 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -12,8 +13,10 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
@@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Library
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char LiveStreamIdDelimeter = '_';
+ private const char LiveStreamIdDelimiter = '_';
private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo;
@@ -51,7 +54,8 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
-
+ private readonly IMediaStreamRepository _mediaStreamRepository;
+ private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -69,7 +73,9 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IMediaStreamRepository mediaStreamRepository,
+ IMediaAttachmentRepository mediaAttachmentRepository)
{
_appHost = appHost;
_itemRepo = itemRepo;
@@ -82,6 +88,8 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager;
_appPaths = applicationPaths;
_directoryService = directoryService;
+ _mediaStreamRepository = mediaStreamRepository;
+ _mediaAttachmentRepository = mediaAttachmentRepository;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -89,9 +97,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray();
}
- public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
+ public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
- var list = _itemRepo.GetMediaStreams(query);
+ var list = _mediaStreamRepository.GetMediaStreams(query);
foreach (var stream in list)
{
@@ -121,7 +129,7 @@ namespace Emby.Server.Implementations.Library
return false;
}
- public List<MediaStream> GetMediaStreams(Guid itemId)
+ public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
@@ -131,7 +139,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list);
}
- private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
+ private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
{
foreach (var stream in streams)
{
@@ -145,13 +153,13 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
- return _itemRepo.GetMediaAttachments(query);
+ return _mediaAttachmentRepository.GetMediaAttachments(query);
}
/// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(Guid itemId)
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
{
return GetMediaAttachments(new MediaAttachmentQuery
{
@@ -159,7 +167,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -212,7 +220,7 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
- return SortMediaSources(list);
+ return SortMediaSources(list).ToArray();
}
/// <inheritdoc />>
@@ -307,7 +315,7 @@ namespace Emby.Server.Implementations.Library
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
- var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
+ var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
@@ -332,7 +340,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
- public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
+ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
ArgumentNullException.ThrowIfNull(item);
@@ -419,6 +427,7 @@ namespace Emby.Server.Implementations.Library
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
{
source.DefaultAudioStreamIndex = index;
+ source.DefaultAudioIndexSource = AudioIndexSource.User;
return;
}
}
@@ -426,6 +435,15 @@ namespace Emby.Server.Implementations.Library
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
+ if (user.PlayDefaultAudioTrack)
+ {
+ source.DefaultAudioIndexSource |= AudioIndexSource.Default;
+ }
+
+ if (preferredAudio.Count > 0)
+ {
+ source.DefaultAudioIndexSource |= AudioIndexSource.Language;
+ }
}
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
@@ -453,7 +471,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+ private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -470,8 +488,7 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0;
})
- .Where(i => i.Type != MediaSourceType.Placeholder)
- .ToList();
+ .Where(i => i.Type != MediaSourceType.Placeholder);
}
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -664,17 +681,17 @@ namespace Emby.Server.Implementations.Library
mediaInfo = await _mediaEncoder.GetMediaInfo(
new MediaInfoRequest
- {
- MediaSource = mediaSource,
- MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
- ExtractChapters = false
- },
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+ },
cancellationToken).ConfigureAwait(false);
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- FileStream createStream = File.Create(cacheFilePath);
+ FileStream createStream = AsyncFile.Create(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -777,9 +794,13 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(id);
- // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
- var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
- return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
+ var info = GetLiveStreamInfo(id);
+ if (info is null)
+ {
+ return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
+ }
+
+ return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
public ILiveStream GetLiveStreamInfo(string id)
@@ -806,7 +827,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
- public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
var stream = new MediaSourceInfo
{
@@ -829,10 +850,7 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
- return new List<MediaSourceInfo>
- {
- stream
- };
+ return [stream];
}
public async Task CloseLiveStream(string id)
@@ -864,11 +882,11 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(key);
- var keys = key.Split(LiveStreamIdDelimeter, 2);
+ var keys = key.Split(LiveStreamIdDelimiter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
- var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
+ var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
return (provider, keyId);
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index ea223e3ec..631179ffc 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
@@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ // Sort in the following order: Default > No tag > Forced
var sortedStreams = streams
.Where(i => i.Type == MediaStreamType.Subtitle)
.OrderByDescending(x => x.IsExternal)
- .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- .ThenByDescending(x => x.IsForced)
.ThenByDescending(x => x.IsDefault)
- .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+ .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+ .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
+ .ThenByDescending(x => x.IsForced)
.ToList();
MediaStream? stream = null;
+
if (mode == SubtitlePlaybackMode.Default)
{
- // Load subtitles according to external, forced and default flags.
- stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ // Load subtitles according to external, default and forced flags.
+ stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
- // If no subtitles of preferred language available, use default behaviour.
+ // If no subtitles of preferred language available, use none.
+ // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
- sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
}
else
{
- // Respect forced flag.
- stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+ stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
- stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
- sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
+ stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
+ BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // Only load subtitles that are flagged forced.
- stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+ // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+ stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
return stream?.Index;
@@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
- filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
+ // Load subtitles according to external, default, and forced flags.
+ filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
.ToList();
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Prefer smart logic over embedded metadata
+ // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
+ filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
.ToList();
}
+ else
+ {
+ filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
+ }
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // Always load the most suitable full subtitles
- filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
+ filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
+ .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // Always load the most suitable full subtitles
- filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
+ // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+ filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
- // Load forced subs if we have found no suitable full subtitles
- var iterStreams = filteredStreams is null || filteredStreams.Count == 0
- ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- : filteredStreams;
+ // If filteredStreams is null, initialize it as an empty list to avoid null reference errors
+ filteredStreams ??= new List<MediaStream>();
- foreach (var stream in iterStreams)
+ foreach (var stream in filteredStreams)
{
stream.Score = GetStreamScore(stream, preferredLanguages);
}
}
+ private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
+ {
+ // If preferredLanguages is empty, treat it as "any language" (wildcard)
+ return preferredLanguages.Count == 0 ||
+ preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsLanguageUndefined(string language)
+ {
+ // Check for null, empty, or known placeholders
+ return string.IsNullOrEmpty(language) ||
+ language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
+ {
+ return sortedStreams
+ .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
+ .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
+ .ThenByDescending(s => IsLanguageUndefined(s.Language))
+ .ToList();
+ }
+
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index a69a0f33f..28cf69500 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -2,9 +2,11 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -24,30 +26,23 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
- public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
- var list = new List<BaseItem>
- {
- item
- };
-
- list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
-
- return list;
+ return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
/// <inheritdoc />
- public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
@@ -63,12 +58,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@@ -85,7 +80,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -97,7 +92,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
new file mode 100644
index 000000000..a9b7a1274
--- /dev/null
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IPathManager implementation.
+/// </summary>
+public class PathManager : IPathManager
+{
+ private readonly IServerConfigurationManager _config;
+ private readonly IApplicationPaths _appPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PathManager"/> class.
+ /// </summary>
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="appPaths">The application paths.</param>
+ public PathManager(
+ IServerConfigurationManager config,
+ IApplicationPaths appPaths)
+ {
+ _config = config;
+ _appPaths = appPaths;
+ }
+
+ private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
+
+ private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
+
+ /// <inheritdoc />
+ public string GetAttachmentPath(string mediaSourceId, string fileName)
+ {
+ return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
+ }
+
+ /// <inheritdoc />
+ public string GetAttachmentFolderPath(string mediaSourceId)
+ {
+ var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return Path.Join(AttachmentCachePath, id[..2], id);
+ }
+
+ /// <inheritdoc />
+ public string GetSubtitleFolderPath(string mediaSourceId)
+ {
+ var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return Path.Join(SubtitleCachePath, id[..2], id);
+ }
+
+ /// <inheritdoc />
+ public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
+ {
+ return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
+ }
+
+ /// <inheritdoc />
+ public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
+ {
+ var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
+ : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
+ }
+
+ /// <inheritdoc/>
+ public string GetChapterImageFolderPath(BaseItem item)
+ {
+ return Path.Combine(item.GetInternalMetadataPath(), "chapters");
+ }
+
+ /// <inheritdoc/>
+ public string GetChapterImagePath(BaseItem item, long chapterPositionTicks)
+ {
+ var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
+
+ return Path.Combine(GetChapterImageFolderPath(item), filename);
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
+ {
+ var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ return [
+ GetAttachmentFolderPath(mediaSourceId),
+ GetSubtitleFolderPath(mediaSourceId),
+ GetTrickplayDirectory(item, false),
+ GetTrickplayDirectory(item, true),
+ GetChapterImageFolderPath(item)
+ ];
+ }
+}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index c9e3a4daf..ab6bc4907 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
if (config.UseFileCreationTimeForDateAdded)
{
- // directoryService.getFile may return null
- if (info is not null)
+ var fileCreationDate = info?.CreationTimeUtc;
+ if (fileCreationDate is not null)
{
- var dateCreated = info.CreationTimeUtc;
-
+ var dateCreated = fileCreationDate;
if (dateCreated.Equals(DateTime.MinValue))
{
dateCreated = DateTime.UtcNow;
}
- item.DateCreated = dateCreated;
+ item.DateCreated = dateCreated.Value;
}
}
else
{
item.DateCreated = DateTime.UtcNow;
}
+
+ if (info is not null && !info.IsDirectory)
+ {
+ item.Size = info.Length;
+ }
+
+ var fileModificationDate = info?.LastWriteTimeUtc;
+ if (fileModificationDate.HasValue)
+ {
+ item.DateModified = fileModificationDate.Value;
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index b4791b945..b9f9f2972 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
- public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
+ public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
{
- var extraResult = GetExtraInfo(path, _namingOptions);
+ var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
if (extraResult.ExtraType is null)
{
extraType = null;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 4debe722b..d78f8b991 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
+ var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var result = new MultiItemResolverResult
{
@@ -456,8 +456,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
+ var hasOtherSubfolders = multiDiscFolders.Count > 0;
- if (!hasPhotos)
+ if (!hasPhotos && !hasOtherSubfolders)
{
var movie = (T)result.Items[0];
movie.IsInMixedFolder = false;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index abf2d0115..6cb63a28a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
- var seasonParserResult = SeasonPathParser.Parse(path, true, true);
+ var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
var season = new Season
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index fb48d7bf1..c81a0adb8 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
- if (IsSeasonFolder(child.FullName, isTvContentType))
+ if (IsSeasonFolder(child.FullName, path, isTvContentType))
{
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="parentPath">The parentpath.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
- private static bool IsSeasonFolder(string path, bool isTvContentType)
+ private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
{
- var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+ var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 7f3f8615e..9d81b835c 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -3,8 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -171,7 +172,7 @@ namespace Emby.Server.Implementations.Library
}
};
- List<BaseItem> mediaItems;
+ IReadOnlyList<BaseItem> mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 320685b1f..71ce3b601 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -4,13 +4,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
/// <inheritdoc />
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
- var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
- var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
+ var posters = GetItemsWithImageType(ImageType.Primary)
+ .Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
+ var backdrops = GetItemsWithImageType(ImageType.Thumb)
+ .Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
if (backdrops.Count == 0)
{
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
- backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
+ backdrops = GetItemsWithImageType(ImageType.Backdrop)
+ .Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
}
_imageEncoder.CreateSplashscreen(posters, backdrops);
@@ -65,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
CollapseBoxSetItems = false,
Recursive = true,
DtoOptions = new DtoOptions(false),
- ImageTypes = new[] { imageType },
+ ImageTypes = [imageType],
Limit = 30,
// TODO max parental rating configurable
- MaxParentalRating = 10,
- OrderBy = new[]
- {
+ MaxParentalRating = new(10, null),
+ OrderBy =
+ [
(ItemSortBy.Random, SortOrder.Ascending)
- },
- IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
+ ],
+ IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
});
}
}
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index ceb3d65a4..be1d96bf0 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,17 +1,20 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
+using System.Linq;
using System.Threading;
-using Jellyfin.Data.Entities;
+using BitFaster.Caching.Lru;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@@ -22,27 +25,22 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
- private readonly ConcurrentDictionary<string, UserItemData> _userData =
- new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
-
private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
- private readonly IUserDataRepository _repository;
+ private readonly IDbContextFactory<JellyfinDbContext> _repository;
+ private readonly FastConcurrentLru<string, UserItemData> _cache;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
+ /// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public UserDataManager(
IServerConfigurationManager config,
- IUserManager userManager,
- IUserDataRepository repository)
+ IDbContextFactory<JellyfinDbContext> repository)
{
_config = config;
- _userManager = userManager;
_repository = repository;
+ _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -59,15 +57,29 @@ namespace Emby.Server.Implementations.Library
var keys = item.GetUserDataKeys();
- var userId = user.InternalId;
+ using var dbContext = _repository.CreateDbContext();
+ using var transaction = dbContext.Database.BeginTransaction();
foreach (var key in keys)
{
- _repository.SaveUserData(userId, key, userData, cancellationToken);
+ userData.Key = key;
+ var userDataEntry = Map(userData, user.Id, item.Id);
+ if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
+ {
+ dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
+ }
+ else
+ {
+ dbContext.UserData.Add(userDataEntry);
+ }
}
+ dbContext.SaveChanges();
+ transaction.Commit();
+
+ var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
- _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
+ _cache.AddOrUpdate(cacheKey, userData);
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{
@@ -86,7 +98,7 @@ namespace Emby.Server.Implementations.Library
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(userDataDto);
- var userData = GetUserData(user, item);
+ var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
if (userDataDto.PlaybackPositionTicks.HasValue)
{
@@ -126,33 +138,91 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
- private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
+ private UserData Map(UserItemData dto, Guid userId, Guid itemId)
{
- var userId = user.InternalId;
-
- var cacheKey = GetCacheKey(userId, itemId);
+ return new UserData()
+ {
+ ItemId = itemId,
+ CustomDataKey = dto.Key,
+ Item = null,
+ User = null,
+ AudioStreamIndex = dto.AudioStreamIndex,
+ IsFavorite = dto.IsFavorite,
+ LastPlayedDate = dto.LastPlayedDate,
+ Likes = dto.Likes,
+ PlaybackPositionTicks = dto.PlaybackPositionTicks,
+ PlayCount = dto.PlayCount,
+ Played = dto.Played,
+ Rating = dto.Rating,
+ UserId = userId,
+ SubtitleStreamIndex = dto.SubtitleStreamIndex,
+ };
+ }
- return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
+ private UserItemData Map(UserData dto)
+ {
+ return new UserItemData()
+ {
+ Key = dto.CustomDataKey!,
+ AudioStreamIndex = dto.AudioStreamIndex,
+ IsFavorite = dto.IsFavorite,
+ LastPlayedDate = dto.LastPlayedDate,
+ Likes = dto.Likes,
+ PlaybackPositionTicks = dto.PlaybackPositionTicks,
+ PlayCount = dto.PlayCount,
+ Played = dto.Played,
+ Rating = dto.Rating,
+ SubtitleStreamIndex = dto.SubtitleStreamIndex,
+ };
}
- private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
+ private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
{
- var userData = _repository.GetUserData(internalUserId, keys);
+ var cacheKey = GetCacheKey(user.InternalId, itemId);
- if (userData is not null)
+ if (_cache.TryGet(cacheKey, out var data))
{
- return userData;
+ return data;
}
- if (keys.Count > 0)
+ data = GetUserDataInternal(user.Id, itemId, keys);
+
+ if (data is null)
{
- return new UserItemData
+ return new UserItemData()
{
- Key = keys[0]
+ Key = keys[0],
};
}
- throw new UnreachableException();
+ return _cache.GetOrAdd(cacheKey, _ => data);
+ }
+
+ private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
+ {
+ if (keys.Count == 0)
+ {
+ return null;
+ }
+
+ using var context = _repository.CreateDbContext();
+ var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
+
+ if (userData.Length > 0)
+ {
+ var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
+ if (directDataReference is not null)
+ {
+ return Map(directDataReference);
+ }
+
+ return Map(userData.First());
+ }
+
+ return new UserItemData
+ {
+ Key = keys.Last()!
+ };
}
/// <summary>
@@ -165,20 +235,25 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public UserItemData GetUserData(User user, BaseItem item)
+ public UserItemData? GetUserData(User user, BaseItem item)
{
return GetUserData(user, item.Id, item.GetUserDataKeys());
}
/// <inheritdoc />
- public UserItemDataDto GetUserDataDto(BaseItem item, User user)
+ public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
/// <inheritdoc />
- public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
+ public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
- var dto = GetUserItemDataDto(userData);
+ if (userData is null)
+ {
+ return null;
+ }
+
+ var dto = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto;
@@ -188,9 +263,10 @@ namespace Emby.Server.Implementations.Library
/// Converts a UserItemData to a DTOUserItemData.
/// </summary>
/// <param name="data">The data.</param>
+ /// <param name="itemId">The reference key to an Item.</param>
/// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
- private UserItemDataDto GetUserItemDataDto(UserItemData data)
+ private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
{
ArgumentNullException.ThrowIfNull(data);
@@ -203,6 +279,7 @@ namespace Emby.Server.Implementations.Library
Rating = data.Rating,
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
+ ItemId = itemId,
Key = data.Key
};
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e9cf47d46..87214c273 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -6,8 +6,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -308,39 +310,40 @@ namespace Emby.Server.Implementations.Library
}
}
- var mediaTypes = new List<MediaType>();
+ MediaType[] mediaTypes = [];
if (includeItemTypes.Length == 0)
{
+ HashSet<MediaType> tmpMediaTypes = [];
foreach (var parent in parents.OfType<ICollectionFolder>())
{
switch (parent.CollectionType)
{
case CollectionType.books:
- mediaTypes.Add(MediaType.Book);
- mediaTypes.Add(MediaType.Audio);
+ tmpMediaTypes.Add(MediaType.Book);
+ tmpMediaTypes.Add(MediaType.Audio);
break;
case CollectionType.music:
- mediaTypes.Add(MediaType.Audio);
+ tmpMediaTypes.Add(MediaType.Audio);
break;
case CollectionType.photos:
- mediaTypes.Add(MediaType.Photo);
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Photo);
+ tmpMediaTypes.Add(MediaType.Video);
break;
case CollectionType.homevideos:
- mediaTypes.Add(MediaType.Photo);
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Photo);
+ tmpMediaTypes.Add(MediaType.Video);
break;
default:
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Video);
break;
}
}
- mediaTypes = mediaTypes.Distinct().ToList();
+ mediaTypes = tmpMediaTypes.ToArray();
}
- var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
+ var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
? new[]
{
BaseItemKind.Person,
@@ -366,12 +369,22 @@ namespace Emby.Server.Implementations.Library
Limit = limit * 5,
IsPlayed = isPlayed,
DtoOptions = options,
- MediaTypes = mediaTypes.ToArray()
+ MediaTypes = mediaTypes
};
- if (parents.Count == 0)
+ if (request.GroupItems)
{
- return _libraryManager.GetItemList(query, false);
+ if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows))
+ {
+ query.Limit = limit;
+ return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
+ }
+
+ if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music))
+ {
+ query.Limit = limit;
+ return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
+ }
}
return _libraryManager.GetItemList(query, parents);
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
index d51f9aaa7..a31d5ecca 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class ArtistsPostScanTask.
+/// </summary>
+public class ArtistsPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class ArtistsPostScanTask.
+ /// The _library manager.
/// </summary>
- public class ArtistsPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<ArtistsValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<ArtistsValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public ArtistsPostScanTask(
- ILibraryManager libraryManager,
- ILogger<ArtistsValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public ArtistsPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<ArtistsValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index 7591e8391..7cc851b73 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class ArtistsValidator.
+/// </summary>
+public class ArtistsValidator
{
/// <summary>
- /// Class ArtistsValidator.
+ /// The library manager.
/// </summary>
- public class ArtistsValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<ArtistsValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<ArtistsValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetAllArtistNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetAllArtistNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetArtist(name);
-
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {ArtistName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
+ var item = _libraryManager.GetArtist(name);
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
- IsDeadArtist = true,
- IsLocked = false
- }).Cast<MusicArtist>().ToList();
-
- foreach (var item in deadEntities)
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
{
- if (!item.IsAccessedByName)
- {
- continue;
- }
+ _logger.LogError(ex, "Error refreshing {ArtistName}", name);
+ }
- _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ progress.Report(percent);
+ }
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.MusicArtist],
+ IsDeadArtist = true,
+ IsLocked = false
+ }).Cast<MusicArtist>().ToList();
+
+ foreach (var item in deadEntities)
+ {
+ if (!item.IsAccessedByName)
+ {
+ continue;
}
- progress.Report(100);
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index 89f64ee4f..38631e0de 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -4,153 +4,151 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class CollectionPostScanTask.
+/// </summary>
+public class CollectionPostScanTask : ILibraryPostScanTask
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ICollectionManager _collectionManager;
+ private readonly ILogger<CollectionPostScanTask> _logger;
+
/// <summary>
- /// Class CollectionPostScanTask.
+ /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
/// </summary>
- public class CollectionPostScanTask : ILibraryPostScanTask
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="collectionManager">The collection manager.</param>
+ /// <param name="logger">The logger.</param>
+ public CollectionPostScanTask(
+ ILibraryManager libraryManager,
+ ICollectionManager collectionManager,
+ ILogger<CollectionPostScanTask> logger)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ICollectionManager _collectionManager;
- private readonly ILogger<CollectionPostScanTask> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="collectionManager">The collection manager.</param>
- /// <param name="logger">The logger.</param>
- public CollectionPostScanTask(
- ILibraryManager libraryManager,
- ICollectionManager collectionManager,
- ILogger<CollectionPostScanTask> logger)
- {
- _libraryManager = libraryManager;
- _collectionManager = collectionManager;
- _logger = logger;
- }
+ _libraryManager = libraryManager;
+ _collectionManager = collectionManager;
+ _logger = logger;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
- foreach (var library in _libraryManager.RootFolder.Children)
+ foreach (var library in _libraryManager.RootFolder.Children)
+ {
+ if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
{
- if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
- {
- continue;
- }
+ continue;
+ }
- var startIndex = 0;
- var pagesize = 1000;
+ var startIndex = 0;
+ var pagesize = 1000;
- while (true)
+ while (true)
+ {
+ var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
- var movies = _libraryManager.GetItemList(new InternalItemsQuery
- {
- MediaTypes = new[] { MediaType.Video },
- IncludeItemTypes = new[] { BaseItemKind.Movie },
- IsVirtualItem = false,
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- Parent = library,
- StartIndex = startIndex,
- Limit = pagesize,
- Recursive = true
- });
-
- foreach (var m in movies)
+ MediaTypes = [MediaType.Video],
+ IncludeItemTypes = [BaseItemKind.Movie],
+ IsVirtualItem = false,
+ OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
+ Parent = library,
+ StartIndex = startIndex,
+ Limit = pagesize,
+ Recursive = true
+ });
+
+ foreach (var m in movies)
+ {
+ if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
- if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
+ if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
- if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
- {
- movieList.Add(movie.Id);
- }
- else
- {
- collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
- }
+ movieList.Add(movie.Id);
+ }
+ else
+ {
+ collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
+ }
- if (movies.Count < pagesize)
- {
- break;
- }
-
- startIndex += pagesize;
+ if (movies.Count < pagesize)
+ {
+ break;
}
+
+ startIndex += pagesize;
}
+ }
- var numComplete = 0;
- var count = collectionNameMoviesMap.Count;
+ var numComplete = 0;
+ var count = collectionNameMoviesMap.Count;
- if (count == 0)
- {
- progress.Report(100);
- return;
- }
+ if (count == 0)
+ {
+ progress.Report(100);
+ return;
+ }
- var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { BaseItemKind.BoxSet },
- CollapseBoxSetItems = false,
- Recursive = true
- });
+ var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.BoxSet],
+ CollapseBoxSetItems = false,
+ Recursive = true
+ });
- foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+ foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+ {
+ try
{
- try
+ var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
+ if (boxSet is null)
{
- var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
- if (boxSet is null)
+ // won't automatically create collection if only one movie in it
+ if (movieIds.Count >= 2)
{
- // won't automatically create collection if only one movie in it
- if (movieIds.Count >= 2)
+ boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
- boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
- {
- Name = collectionName,
- IsLocked = true
- });
+ Name = collectionName,
+ IsLocked = true
+ }).ConfigureAwait(false);
- await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
- }
+ await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
- else
- {
- await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
}
- catch (Exception ex)
+ else
{
- _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+ await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
- }
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+ }
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
index d21d2887b..5097e0073 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class GenresPostScanTask.
+/// </summary>
+public class GenresPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class GenresPostScanTask.
+ /// The _library manager.
/// </summary>
- public class GenresPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<GenresValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<GenresValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public GenresPostScanTask(
- ILibraryManager libraryManager,
- ILogger<GenresValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public GenresPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<GenresValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
index e59c62e23..fbfc9f7d5 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -1,81 +1,103 @@
using System;
+using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class GenresValidator.
+/// </summary>
+public class GenresValidator
{
/// <summary>
- /// Class GenresValidator.
+ /// The library manager.
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// The logger.
/// </summary>
- public class GenresValidator
+ private readonly ILogger<GenresValidator> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenresValidator"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
{
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly IItemRepository _itemRepo;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<GenresValidator> _logger;
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetGenreNames();
- /// <summary>
- /// Initializes a new instance of the <see cref="GenresValidator"/> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ var numComplete = 0;
+ var count = names.Count;
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ foreach (var name in names)
{
- var names = _itemRepo.GetGenreNames();
-
- var numComplete = 0;
- var count = names.Count;
+ try
+ {
+ var item = _libraryManager.GetGenre(name);
- foreach (var name in names)
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
{
- try
- {
- var item = _libraryManager.GetGenre(name);
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing {GenreName}", name);
+ }
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {GenreName}", name);
- }
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
+ progress.Report(percent);
+ }
- progress.Report(percent);
- }
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
+ IsDeadGenre = true,
+ IsLocked = false
+ });
- progress.Report(100);
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
index be119866b..76658a81b 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresPostScanTask.
+/// </summary>
+public class MusicGenresPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class MusicGenresPostScanTask.
+ /// The library manager.
/// </summary>
- public class MusicGenresPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<MusicGenresValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MusicGenresValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public MusicGenresPostScanTask(
- ILibraryManager libraryManager,
- ILogger<MusicGenresValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public MusicGenresPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<MusicGenresValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
index 1ecf4c87c..6203bce2b 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
@@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresValidator.
+/// </summary>
+public class MusicGenresValidator
{
/// <summary>
- /// Class MusicGenresValidator.
+ /// The library manager.
/// </summary>
- public class MusicGenresValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<MusicGenresValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<MusicGenresValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetMusicGenreNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetMusicGenreNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetMusicGenre(name);
+ var item = _libraryManager.GetMusicGenre(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {GenreName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing {GenreName}", name);
}
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 725b8f76c..b7fd24fa5 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class PeopleValidator.
+/// </summary>
+public class PeopleValidator
{
/// <summary>
- /// Class PeopleValidator.
+ /// The _library manager.
/// </summary>
- public class PeopleValidator
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger.
+ /// </summary>
+ private readonly ILogger _logger;
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="fileSystem">The file system.</param>
+ public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
{
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger _logger;
-
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _fileSystem = fileSystem;
- }
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ }
- /// <summary>
- /// Validates the people.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
- {
- var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
+ /// <summary>
+ /// Validates the people.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
- var numComplete = 0;
+ var numComplete = 0;
- var numPeople = people.Count;
+ var numPeople = people.Count;
- _logger.LogDebug("Will refresh {0} people", numPeople);
+ _logger.LogDebug("Will refresh {Amount} people", numPeople);
- foreach (var person in people)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ foreach (var person in people)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- try
- {
- var item = _libraryManager.GetPerson(person);
- if (item is null)
- {
- _logger.LogWarning("Failed to get person: {Name}", person);
- continue;
- }
-
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
- MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
- };
-
- await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
+ try
+ {
+ var item = _libraryManager.GetPerson(person);
+ if (item is null)
{
- _logger.LogError(ex, "Error validating IBN entry {Person}", person);
+ _logger.LogWarning("Failed to get person: {Name}", person);
+ continue;
}
- // Update progress
- numComplete++;
- double percent = numComplete;
- percent /= numPeople;
+ var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
+ MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
+ };
- progress.Report(100 * percent);
+ await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = [BaseItemKind.Person],
- IsDeadPerson = true,
- IsLocked = false
- });
-
- foreach (var item in deadEntities)
+ throw;
+ }
+ catch (Exception ex)
{
- _logger.LogInformation(
- "Deleting dead {2} {0} {1}.",
- item.Id.ToString("N", CultureInfo.InvariantCulture),
- item.Name,
- item.GetType().Name);
-
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ _logger.LogError(ex, "Error validating IBN entry {Person}", person);
}
- progress.Report(100);
+ // Update progress
+ numComplete++;
+ double percent = numComplete;
+ percent /= numPeople;
- _logger.LogInformation("People validation complete");
+ progress.Report(100 * percent);
}
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Person],
+ IsDeadPerson = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
+ }
+
+ progress.Report(100);
+
+ _logger.LogInformation("People validation complete");
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
index c682b156b..67c56c104 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
@@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresPostScanTask.
+/// </summary>
+public class StudiosPostScanTask : ILibraryPostScanTask
{
/// <summary>
- /// Class MusicGenresPostScanTask.
+ /// The _library manager.
/// </summary>
- public class StudiosPostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- private readonly ILogger<StudiosValidator> _logger;
- private readonly IItemRepository _itemRepo;
+ private readonly ILogger<StudiosValidator> _logger;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public StudiosPostScanTask(
- ILibraryManager libraryManager,
- ILogger<StudiosValidator> logger,
- IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public StudiosPostScanTask(
+ ILibraryManager libraryManager,
+ ILogger<StudiosValidator> logger,
+ IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
- }
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index 26bc49c1f..5b87e4d9d 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class StudiosValidator.
+/// </summary>
+public class StudiosValidator
{
/// <summary>
- /// Class StudiosValidator.
+ /// The library manager.
/// </summary>
- public class StudiosValidator
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
- private readonly IItemRepository _itemRepo;
+ private readonly IItemRepository _itemRepo;
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<StudiosValidator> _logger;
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<StudiosValidator> _logger;
- /// <summary>
- /// Initializes a new instance of the <see cref="StudiosValidator" /> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="itemRepo">The item repository.</param>
- public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _itemRepo = itemRepo;
- }
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StudiosValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="itemRepo">The item repository.</param>
+ public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var names = _itemRepo.GetStudioNames();
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetStudioNames();
- var numComplete = 0;
- var count = names.Count;
+ var numComplete = 0;
+ var count = names.Count;
- foreach (var name in names)
+ foreach (var name in names)
+ {
+ try
{
- try
- {
- var item = _libraryManager.GetStudio(name);
+ var item = _libraryManager.GetStudio(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Don't clutter the log
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error refreshing {StudioName}", name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
-
- progress.Report(percent);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
-
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ catch (OperationCanceledException)
{
- IncludeItemTypes = new[] { BaseItemKind.Studio },
- IsDeadStudio = true,
- IsLocked = false
- });
-
- foreach (var item in deadEntities)
+ // Don't clutter the log
+ throw;
+ }
+ catch (Exception ex)
{
- _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
-
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false
- },
- false);
+ _logger.LogError(ex, "Error refreshing {StudioName}", name);
}
- progress.Report(100);
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
}
+
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Studio],
+ IsDeadStudio = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
+ }
+
+ progress.Report(100);
}
}