mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-19 04:34:18 +01:00
Merge branch 'master' into feature/DatabaseRefactor
This commit is contained in:
@@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase
|
||||
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the program data folder.
|
||||
/// </summary>
|
||||
/// <value>The program data path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ProgramDataPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string WebPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the system folder.
|
||||
/// </summary>
|
||||
/// <value>The path to the system folder.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder path to the data directory.
|
||||
/// </summary>
|
||||
/// <value>The data directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string DataPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache path.
|
||||
/// </summary>
|
||||
/// <value>The image cache path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ImageCachePath => Path.Combine(CachePath, "images");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin directory.
|
||||
/// </summary>
|
||||
/// <value>The plugins path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin configurations directory.
|
||||
/// </summary>
|
||||
/// <value>The plugin configurations path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the log directory.
|
||||
/// </summary>
|
||||
/// <value>The log directory path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string LogDirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the application configuration root directory.
|
||||
/// </summary>
|
||||
/// <value>The configuration directory path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ConfigurationDirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the system configuration file.
|
||||
/// </summary>
|
||||
/// <value>The system configuration file path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder path to the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The cache directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string CachePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder path to the temp directory within the cache folder.
|
||||
/// </summary>
|
||||
/// <value>The temp directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>));
|
||||
|
||||
@@ -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 numComplete = 0;
|
||||
var numItems = itemIds.Count + 1;
|
||||
|
||||
_logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
|
||||
|
||||
foreach (var itemId in itemIds)
|
||||
{
|
||||
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item is not null)
|
||||
{
|
||||
HasDeadParentId = true
|
||||
});
|
||||
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
|
||||
|
||||
var numComplete = 0;
|
||||
var numItems = itemIds.Count + 1;
|
||||
|
||||
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
|
||||
|
||||
foreach (var itemId in itemIds)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
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))
|
||||
{
|
||||
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,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.
|
||||
@@ -114,7 +115,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,
|
||||
@@ -131,7 +133,8 @@ namespace Emby.Server.Implementations.Library
|
||||
IImageProcessor imageProcessor,
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService,
|
||||
IPeopleRepository peopleRepository)
|
||||
IPeopleRepository peopleRepository,
|
||||
IPathManager pathManager)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||
@@ -149,6 +152,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;
|
||||
@@ -201,33 +205,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; }
|
||||
|
||||
@@ -360,7 +364,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: Array.Empty<BaseItem>();
|
||||
: [];
|
||||
|
||||
foreach (var metadataPath in GetMetadataPaths(item, children))
|
||||
{
|
||||
@@ -466,14 +470,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;
|
||||
}
|
||||
@@ -594,7 +612,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
|
||||
|
||||
files = Array.Empty<FileSystemMetadata>();
|
||||
files = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1345,6 +1363,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)
|
||||
@@ -1449,7 +1482,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)
|
||||
@@ -1569,7 +1602,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetTopParentIdsForQuery(displayParent, user);
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!view.ParentId.IsEmpty())
|
||||
@@ -1580,7 +1613,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetTopParentIdsForQuery(displayParent, user);
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle grouping
|
||||
@@ -1595,7 +1628,7 @@ namespace Emby.Server.Implementations.Library
|
||||
.SelectMany(i => GetTopParentIdsForQuery(i, user));
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
if (item is CollectionFolder collectionFolder)
|
||||
@@ -1609,7 +1642,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return new[] { topParent.Id };
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1653,7 +1686,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_logger.LogError(ex, "Error getting intros");
|
||||
|
||||
return Enumerable.Empty<IntroInfo>();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2480,8 +2513,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)
|
||||
@@ -2880,7 +2916,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);
|
||||
|
||||
@@ -783,9 +783,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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
36
Emby.Server.Implementations/Library/PathManager.cs
Normal file
36
Emby.Server.Implementations/Library/PathManager.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Sync": "Сінхранізаваць",
|
||||
"Playlists": "Плэйлісты",
|
||||
"Playlists": "Спісы прайгравання",
|
||||
"Latest": "Апошні",
|
||||
"LabelIpAddressValue": "IP-адрас: {0}",
|
||||
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
|
||||
@@ -16,7 +16,7 @@
|
||||
"Collections": "Калекцыі",
|
||||
"Default": "Па змаўчанні",
|
||||
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
||||
"Folders": "Папкі",
|
||||
"Folders": "Тэчкі",
|
||||
"Favorites": "Абранае",
|
||||
"External": "Знешні",
|
||||
"Genres": "Жанры",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Συλλογές",
|
||||
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
|
||||
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
|
||||
"FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
|
||||
"FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
|
||||
"Favorites": "Αγαπημένα",
|
||||
"Folders": "Φάκελοι",
|
||||
"Genres": "Είδη",
|
||||
@@ -27,8 +27,8 @@
|
||||
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
|
||||
"HomeVideos": "Προσωπικά Βίντεο",
|
||||
"Inherit": "Κληρονόμηση",
|
||||
"ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
|
||||
"ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
|
||||
"ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
|
||||
"ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
|
||||
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
|
||||
"LabelRunningTimeValue": "Διάρκεια: {0}",
|
||||
"Latest": "Πρόσφατα",
|
||||
@@ -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} ξεκίνησε",
|
||||
@@ -96,7 +96,7 @@
|
||||
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
|
||||
"TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
|
||||
"TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
|
||||
"TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
|
||||
"TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
|
||||
"TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
|
||||
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
|
||||
"TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
|
||||
@@ -125,7 +125,7 @@
|
||||
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
|
||||
"External": "Εξωτερικό",
|
||||
"HearingImpaired": "Με προβλήματα ακοής",
|
||||
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
||||
"TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
|
||||
}
|
||||
|
||||
// Fairly common for some users to have "Rated R" in their rating field
|
||||
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Trim();
|
||||
|
||||
// Use rating system matching the language
|
||||
if (!string.IsNullOrEmpty(countryCode))
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"TwoLetterISORegionName": "IE"
|
||||
},
|
||||
{
|
||||
"DisplayName": "Islamic Republic of Pakistan",
|
||||
"DisplayName": "Pakistan",
|
||||
"Name": "PK",
|
||||
"ThreeLetterISORegionName": "PAK",
|
||||
"TwoLetterISORegionName": "PK"
|
||||
|
||||
@@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
|
||||
if (item is null)
|
||||
{
|
||||
_logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
|
||||
_logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -344,6 +344,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);
|
||||
@@ -676,6 +681,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))
|
||||
{
|
||||
@@ -795,7 +805,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
|
||||
|
||||
@@ -92,7 +92,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)
|
||||
@@ -100,25 +100,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();
|
||||
var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
|
||||
var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
|
||||
|
||||
return GetResult(episodes, request);
|
||||
}
|
||||
@@ -134,36 +118,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();
|
||||
@@ -179,13 +138,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
|
||||
}
|
||||
};
|
||||
@@ -203,8 +162,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,
|
||||
@@ -229,7 +188,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
|
||||
@@ -249,7 +208,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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user