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/LibraryManager.cs78
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs10
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs86
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs36
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs7
6 files changed, 165 insertions, 54 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index cc2092e21..b810ad4de 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -78,6 +78,7 @@ namespace Emby.Server.Implementations.Library
private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
+ private readonly IPathManager _pathManager;
/// <summary>
/// The _root folder sync lock.
@@ -113,7 +114,8 @@ namespace Emby.Server.Implementations.Library
/// <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="peopleRepository">The people repository.</param>
+ /// <param name="pathManager">The path manager.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -130,7 +132,8 @@ namespace Emby.Server.Implementations.Library
IImageProcessor imageProcessor,
NamingOptions namingOptions,
IDirectoryService directoryService,
- IPeopleRepository peopleRepository)
+ IPeopleRepository peopleRepository,
+ IPathManager pathManager)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -148,6 +151,7 @@ namespace Emby.Server.Implementations.Library
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
+ _pathManager = pathManager;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -200,33 +204,33 @@ namespace Emby.Server.Implementations.Library
/// 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; }
@@ -359,7 +363,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
- : Array.Empty<BaseItem>();
+ : [];
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -465,14 +469,28 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
- private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ 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)
+ {
+ list.Add(_pathManager.GetTrickplayDirectory(video));
+ }
return list;
}
@@ -593,7 +611,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
- files = Array.Empty<FileSystemMetadata>();
+ files = [];
}
else
{
@@ -1344,6 +1362,21 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemList(query);
}
+ 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)
@@ -1448,7 +1481,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)
@@ -1568,7 +1601,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
if (!view.ParentId.IsEmpty())
@@ -1579,7 +1612,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
// Handle grouping
@@ -1594,7 +1627,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
- return Array.Empty<Guid>();
+ return [];
}
if (item is CollectionFolder collectionFolder)
@@ -1608,7 +1641,7 @@ namespace Emby.Server.Implementations.Library
return new[] { topParent.Id };
}
- return Array.Empty<Guid>();
+ return [];
}
/// <summary>
@@ -1652,7 +1685,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
- return Enumerable.Empty<IntroInfo>();
+ return [];
}
}
@@ -2479,8 +2512,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)
@@ -2879,7 +2915,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);
+ await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 5795c47cc..92a5e9ffd 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -782,9 +782,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)
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index ea223e3ec..6791e3ca9 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -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/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
new file mode 100644
index 000000000..c910abadb
--- /dev/null
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -0,0 +1,36 @@
+using System.Globalization;
+using System.IO;
+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;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PathManager"/> class.
+ /// </summary>
+ /// <param name="config">The server configuration manager.</param>
+ public PathManager(
+ IServerConfigurationManager config)
+ {
+ _config = config;
+ }
+
+ /// <inheritdoc />
+ public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
+ {
+ var basePath = _config.ApplicationPaths.TrickplayPath;
+ var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+
+ return saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(basePath, idString);
+ }
+}
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;
}