aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs106
-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
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json2
-rw-r--r--Emby.Server.Implementations/Localization/countries.json2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs16
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs61
14 files changed, 260 insertions, 170 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 29967c6df..4fe1d2b17 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -57,6 +57,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
@@ -508,6 +509,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
+ serviceCollection.AddSingleton<IPathManager, PathManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 7ea863d76..a83ded439 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -5,80 +5,80 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Data
+namespace Emby.Server.Implementations.Data;
+
+public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
- public class CleanDatabaseScheduledTask : ILibraryPostScanTask
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ public CleanDatabaseScheduledTask(
+ ILibraryManager libraryManager,
+ ILogger<CleanDatabaseScheduledTask> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<CleanDatabaseScheduledTask> _logger;
- private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _dbProvider = dbProvider;
+ }
- public CleanDatabaseScheduledTask(
- ILibraryManager libraryManager,
- ILogger<CleanDatabaseScheduledTask> logger,
- IDbContextFactory<JellyfinDbContext> dbProvider)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _dbProvider = dbProvider;
- }
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
+ }
- public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
- await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
- }
+ HasDeadParentId = true
+ });
- private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
- {
- var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
- {
- HasDeadParentId = true
- });
+ var numComplete = 0;
+ var numItems = itemIds.Count + 1;
- var numComplete = 0;
- var numItems = itemIds.Count + 1;
+ _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
- _logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
+ foreach (var itemId in itemIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- foreach (var itemId in itemIds)
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is not null)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- var item = _libraryManager.GetItemById(itemId);
+ _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
- if (item is not null)
+ _libraryManager.DeleteItem(item, new DeleteOptions
{
- _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
-
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- });
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= numItems;
- progress.Report(percent * 100);
+ DeleteFileLocation = false
+ });
}
- var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
- await using (context.ConfigureAwait(false))
+ numComplete++;
+ double percent = numComplete;
+ percent /= numItems;
+ progress.Report(percent * 100);
+ }
+
+ var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using (transaction.ConfigureAwait(false))
{
- var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
- await using (transaction.ConfigureAwait(false))
- {
- await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
- }
+ await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
-
- progress.Report(100);
}
+
+ progress.Report(100);
}
}
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;
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 53e094bfc..f3195f0ea 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -11,7 +11,7 @@
"Collections": "Συλλογές",
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
- "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
+ "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
@@ -27,7 +27,7 @@
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
"HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
- "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
+ "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
"ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}",
@@ -40,7 +40,7 @@
"Movies": "Ταινίες",
"Music": "Μουσική",
"MusicVideos": "Μουσικά Βίντεο",
- "NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
+ "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε",
"NameSeasonNumber": "Κύκλος {0}",
"NameSeasonUnknown": "Άγνωστος Κύκλος",
"NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
@@ -54,7 +54,7 @@
"NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
"NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
"NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
- "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε",
+ "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε",
"NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
"NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
"NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
@@ -63,9 +63,9 @@
"Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής",
"Plugin": "Πρόσθετο",
- "PluginInstalledWithName": "{0} εγκαταστήθηκε",
- "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
- "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
+ "PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
+ "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
+ "PluginUpdatedWithName": "Το {0} ενημερώθηκε",
"ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία",
"ScheduledTaskStartedWithName": "{0} ξεκίνησε",
@@ -125,7 +125,7 @@
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής",
- "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+ "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 0b595c2ca..42cce1096 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -122,5 +122,9 @@
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
- "External": "Ekstera"
+ "External": "Ekstera",
+ "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
+ "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
+ "TaskAudioNormalization": "Normaligo Sonnivela",
+ "HearingImpaired": "Surda"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 879bf64b0..42ea5e0a4 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -1,6 +1,6 @@
{
"Albums": "Álbuns",
- "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
+ "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"Application": "Aplicação",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json
index 0a11b3e45..d92dc880b 100644
--- a/Emby.Server.Implementations/Localization/countries.json
+++ b/Emby.Server.Implementations/Localization/countries.json
@@ -336,7 +336,7 @@
"TwoLetterISORegionName": "IE"
},
{
- "DisplayName": "Islamic Republic of Pakistan",
+ "DisplayName": "Pakistan",
"Name": "PK",
"ThreeLetterISORegionName": "PAK",
"TwoLetterISORegionName": "PK"
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 030da6f73..df2acfc46 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -343,6 +343,11 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns>
private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
{
+ if (session is null)
+ {
+ return;
+ }
+
if (string.IsNullOrEmpty(info.MediaSourceId))
{
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
@@ -675,6 +680,11 @@ namespace Emby.Server.Implementations.Session
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{
+ if (session is null)
+ {
+ return null;
+ }
+
var item = session.FullNowPlayingItem;
if (item is not null && item.Id.Equals(itemId))
{
@@ -794,7 +804,11 @@ namespace Emby.Server.Implementations.Session
ArgumentNullException.ThrowIfNull(info);
- var session = GetSession(info.SessionId);
+ var session = GetSession(info.SessionId, false);
+ if (session is null)
+ {
+ return;
+ }
var libraryItem = info.ItemId.IsEmpty()
? null
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index f8ce473da..10d27498b 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request);
+ return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
}
if (limit.HasValue)
@@ -99,25 +99,9 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager
- .GetItemList(
- new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
- GroupBySeriesPresentationUniqueKey = true
- },
- parentsFolders.ToList())
- .Cast<Episode>()
- .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
- .Select(GetUniqueSeriesKey)
- .ToList();
-
- // Avoid implicitly captured closure
- var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
+ var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
+
+ var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
return GetResult(episodes, request);
}
@@ -133,36 +117,11 @@ namespace Emby.Server.Implementations.TV
.OrderByDescending(i => i.LastWatchedDate);
}
- // If viewing all next up for all series, remove first episodes
- // But if that returns empty, keep those first episodes (avoid completely empty view)
- var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
- var anyFound = false;
-
return allNextUp
- .Where(i =>
- {
- if (request.DisableFirstEpisode)
- {
- return i.LastWatchedDate != DateTime.MinValue;
- }
-
- if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
- {
- anyFound = true;
- return true;
- }
-
- return !anyFound && i.LastWatchedDate == DateTime.MinValue;
- })
.Select(i => i.GetEpisodeFunction())
.Where(i => i is not null)!;
}
- private static string GetUniqueSeriesKey(Episode episode)
- {
- return episode.SeriesPresentationUniqueKey;
- }
-
private static string GetUniqueSeriesKey(Series series)
{
return series.GetPresentationUniqueKey();
@@ -178,13 +137,13 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
- Fields = new[] { ItemFields.SortName },
+ Fields = [ItemFields.SortName],
EnableImages = false
}
};
@@ -202,8 +161,8 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Episode],
+ OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
Limit = 1,
IsPlayed = includePlayed,
IsVirtualItem = false,
@@ -228,7 +187,7 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = includePlayed,
IsVirtualItem = false,
DtoOptions = dtoOptions
@@ -248,7 +207,7 @@ namespace Emby.Server.Implementations.TV
consideredEpisodes.Add(nextEpisode);
}
- var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
+ var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
.Cast<Episode>();
if (lastWatchedEpisode is not null)
{