From 6454a35ef831157fb10d8cbdf39017b2df2b8449 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 22 Jan 2025 18:20:57 +0100 Subject: Extract trickplay files into own subdirectory --- Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'Jellyfin.Server.Implementations') diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index cd73d67c3..e94673bce 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -602,9 +602,11 @@ public class TrickplayManager : ITrickplayManager /// public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { + var basePath = _config.ApplicationPaths.TrickplayPath; + var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); var path = saveWithMedia ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + : Path.Combine(basePath, idString); var subdirectory = string.Format( CultureInfo.InvariantCulture, -- cgit v1.2.3 From 85b5bebda4a887bad03a114e727d9ee5d87961cc Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 18 Mar 2025 17:37:04 -0600 Subject: Add fast-path to getting just the SeriesPresentationUniqueKey for NextUp (#13687) * Add more optimized query to calculate series that should be processed for next up * Filter series based on last watched date --- .../Library/LibraryManager.cs | 15 +++ Emby.Server.Implementations/TV/TVSeriesManager.cs | 61 ++--------- Jellyfin.Api/Controllers/TvShowsController.cs | 4 +- .../Item/BaseItemRepository.cs | 31 ++++++ MediaBrowser.Controller/Library/ILibraryManager.cs | 9 ++ .../Persistence/IItemRepository.cs | 8 ++ MediaBrowser.Model/Querying/NextUpQuery.cs | 113 ++++++++++----------- 7 files changed, 128 insertions(+), 113 deletions(-) (limited to 'Jellyfin.Server.Implementations') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cc2092e21..7b3a54039 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1344,6 +1344,21 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemList(query); } + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection 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 QueryItems(InternalItemsQuery query) { if (query.User is not 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() - .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(); if (lastWatchedEpisode is not null) { diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index df46c2dac..cc070244b 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false, + [FromQuery][ParameterObsolete] bool disableFirstEpisode = false, [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { @@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController StartIndex = startIndex, User = user, EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, EnableResumable = enableResumable, EnableRewatching = enableRewatching diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 392b7de74..e20ad79ad 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -255,6 +255,37 @@ public sealed class BaseItemRepository return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); } + /// + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff) + { + ArgumentNullException.ThrowIfNull(filter); + ArgumentNullException.ThrowIfNull(filter.User); + + using var context = _dbProvider.CreateDbContext(); + + var query = context.BaseItems + .AsNoTracking() + .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value)) + .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]) + .Join( + context.UserData.AsNoTracking(), + i => new { UserId = filter.User.Id, ItemId = i.Id }, + u => new { UserId = u.UserId, ItemId = u.ItemId }, + (entity, data) => new { Item = entity, UserData = data }) + .GroupBy(g => g.Item.SeriesPresentationUniqueKey) + .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) }) + .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff) + .OrderByDescending(g => g.LastPlayedDate) + .Select(g => g.Key!); + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + + return query.ToArray(); + } + private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) { // This whole block is needed to filter duplicate entries on request diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 47b1cb16e..03a28fd8c 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -565,6 +565,15 @@ namespace MediaBrowser.Controller.Library /// List of items. IReadOnlyList GetItemList(InternalItemsQuery query, List parents); + /// + /// Gets the list of series presentation keys for next up. + /// + /// The query to use. + /// Items to use for query. + /// The minimum date for a series to have been most recently watched. + /// List of series presentation keys. + IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff); + /// /// Gets the items result. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index afe2d833d..f1ed4fe27 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -59,6 +59,14 @@ public interface IItemRepository /// List<BaseItem>. IReadOnlyList GetItemList(InternalItemsQuery filter); + /// + /// Gets the list of series presentation keys for next up. + /// + /// The query. + /// The minimum date for a series to have been most recently watched. + /// The list of keys. + IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff); + /// /// Updates the inherited values. /// diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 8dece28a0..aee720aa7 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -4,76 +4,69 @@ using System; using Jellyfin.Data.Entities; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Querying +namespace MediaBrowser.Model.Querying; + +public class NextUpQuery { - public class NextUpQuery + public NextUpQuery() { - public NextUpQuery() - { - EnableImageTypes = Array.Empty(); - EnableTotalRecordCount = true; - DisableFirstEpisode = false; - NextUpDateCutoff = DateTime.MinValue; - EnableResumable = false; - EnableRewatching = false; - } - - /// - /// Gets or sets the user. - /// - /// The user. - public required User User { get; set; } + EnableImageTypes = Array.Empty(); + EnableTotalRecordCount = true; + NextUpDateCutoff = DateTime.MinValue; + EnableResumable = false; + EnableRewatching = false; + } - /// - /// Gets or sets the parent identifier. - /// - /// The parent identifier. - public Guid? ParentId { get; set; } + /// + /// Gets or sets the user. + /// + /// The user. + public required User User { get; set; } - /// - /// Gets or sets the series id. - /// - /// The series id. - public Guid? SeriesId { get; set; } + /// + /// Gets or sets the parent identifier. + /// + /// The parent identifier. + public Guid? ParentId { get; set; } - /// - /// Gets or sets the start index. Use for paging. - /// - /// The start index. - public int? StartIndex { get; set; } + /// + /// Gets or sets the series id. + /// + /// The series id. + public Guid? SeriesId { get; set; } - /// - /// Gets or sets the maximum number of items to return. - /// - /// The limit. - public int? Limit { get; set; } + /// + /// Gets or sets the start index. Use for paging. + /// + /// The start index. + public int? StartIndex { get; set; } - /// - /// Gets or sets the enable image types. - /// - /// The enable image types. - public ImageType[] EnableImageTypes { get; set; } + /// + /// Gets or sets the maximum number of items to return. + /// + /// The limit. + public int? Limit { get; set; } - public bool EnableTotalRecordCount { get; set; } + /// + /// Gets or sets the enable image types. + /// + /// The enable image types. + public ImageType[] EnableImageTypes { get; set; } - /// - /// Gets or sets a value indicating whether do disable sending first episode as next up. - /// - public bool DisableFirstEpisode { get; set; } + public bool EnableTotalRecordCount { get; set; } - /// - /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. - /// - public DateTime NextUpDateCutoff { get; set; } + /// + /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. + /// + public DateTime NextUpDateCutoff { get; set; } - /// - /// Gets or sets a value indicating whether to include resumable episodes as next up. - /// - public bool EnableResumable { get; set; } + /// + /// Gets or sets a value indicating whether to include resumable episodes as next up. + /// + public bool EnableResumable { get; set; } - /// - /// Gets or sets a value indicating whether getting rewatching next up list. - /// - public bool EnableRewatching { get; set; } - } + /// + /// Gets or sets a value indicating whether getting rewatching next up list. + /// + public bool EnableRewatching { get; set; } } -- cgit v1.2.3 From 8db6a39e92acfd76689e77c71b00ac96e60c515b Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 23 Mar 2025 17:05:13 +0100 Subject: Remove all DB data on item removal, delete internal trickplay files (#13753) --- .../Data/CleanDatabaseScheduledTask.cs | 106 ++++++++++----------- .../Library/LibraryManager.cs | 56 +++++++---- Emby.Server.Implementations/Library/PathManager.cs | 36 +++++++ .../Item/BaseItemRepository.cs | 19 ++-- .../Trickplay/TrickplayManager.cs | 16 ++-- MediaBrowser.Controller/IO/IPathManager.cs | 17 ++++ 6 files changed, 164 insertions(+), 86 deletions(-) create mode 100644 Emby.Server.Implementations/Library/PathManager.cs create mode 100644 MediaBrowser.Controller/IO/IPathManager.cs (limited to 'Jellyfin.Server.Implementations') 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 _logger; + private readonly IDbContextFactory _dbProvider; + + public CleanDatabaseScheduledTask( + ILibraryManager libraryManager, + ILogger logger, + IDbContextFactory dbProvider) { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IDbContextFactory _dbProvider; + _libraryManager = libraryManager; + _logger = logger; + _dbProvider = dbProvider; + } - public CleanDatabaseScheduledTask( - ILibraryManager libraryManager, - ILogger logger, - IDbContextFactory dbProvider) - { - _libraryManager = libraryManager; - _logger = logger; - _dbProvider = dbProvider; - } + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); + } - public async Task Run(IProgress progress, CancellationToken cancellationToken) + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); - } + HasDeadParentId = true + }); - private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress 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 7b3a54039..3432aa322 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; /// /// The _root folder sync lock. @@ -113,7 +114,8 @@ namespace Emby.Server.Implementations.Library /// The image processor. /// The naming options. /// The directory service. - /// The People Repository. + /// The people repository. + /// The path manager. 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(); @@ -148,6 +151,7 @@ namespace Emby.Server.Implementations.Library _cache = new ConcurrentDictionary(); _namingOptions = namingOptions; _peopleRepository = peopleRepository; + _pathManager = pathManager; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -200,33 +204,33 @@ namespace Emby.Server.Implementations.Library /// Gets or sets the postscan tasks. /// /// The postscan tasks. - private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty(); + private ILibraryPostScanTask[] PostscanTasks { get; set; } = []; /// /// Gets or sets the intro providers. /// /// The intro providers. - private IIntroProvider[] IntroProviders { get; set; } = Array.Empty(); + private IIntroProvider[] IntroProviders { get; set; } = []; /// /// Gets or sets the list of entity resolution ignore rules. /// /// The entity resolution ignore rules. - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty(); + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = []; /// /// Gets or sets the list of currently registered entity resolvers. /// /// The entity resolvers enumerable. - private IItemResolver[] EntityResolvers { get; set; } = Array.Empty(); + private IItemResolver[] EntityResolvers { get; set; } = []; - private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty(); + private IMultiItemResolver[] MultiItemResolvers { get; set; } = []; /// /// Gets or sets the comparers. /// /// The comparers. - private IBaseItemComparer[] Comparers { get; set; } = Array.Empty(); + 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(); + : []; foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -465,14 +469,28 @@ namespace Emby.Server.Implementations.Library ReportItemRemoved(item, parent); } - private static List GetMetadataPaths(BaseItem item, IEnumerable children) + private List GetMetadataPaths(BaseItem item, IEnumerable children) + { + var list = GetInternalMetadataPaths(item); + foreach (var child in children) + { + list.AddRange(GetInternalMetadataPaths(child)); + } + + return list; + } + + private List GetInternalMetadataPaths(BaseItem item) { var list = new List { 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(); + files = []; } else { @@ -1463,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(); + query.AncestorIds = []; // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) @@ -1583,7 +1601,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } if (!view.ParentId.IsEmpty()) @@ -1594,7 +1612,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } // Handle grouping @@ -1609,7 +1627,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return Array.Empty(); + return []; } if (item is CollectionFolder collectionFolder) @@ -1623,7 +1641,7 @@ namespace Emby.Server.Implementations.Library return new[] { topParent.Id }; } - return Array.Empty(); + return []; } /// @@ -1667,7 +1685,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return Enumerable.Empty(); + return []; } } @@ -2894,7 +2912,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()).ConfigureAwait(false); + await File.WriteAllBytesAsync(path, []).ConfigureAwait(false); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); 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; + +/// +/// IPathManager implementation. +/// +public class PathManager : IPathManager +{ + private readonly IServerConfigurationManager _config; + + /// + /// Initializes a new instance of the class. + /// + /// The server configuration manager. + public PathManager( + IServerConfigurationManager config) + { + _config = config; + } + + /// + 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/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e20ad79ad..630a169cb 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -101,16 +101,23 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); - context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); - context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); - context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); - context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); - context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); + context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete(); context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete(); context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); + context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); + context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); + context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.SaveChanges(); transaction.Commit(); } diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 9c0f5b57b..6949ec1a8 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Trickplay; @@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager private readonly IImageEncoder _imageEncoder; private readonly IDbContextFactory _dbProvider; private readonly IApplicationPaths _appPaths; + private readonly IPathManager _pathManager; private static readonly AsyncNonKeyedLocker _resourcePool = new(1); - private static readonly string[] _trickplayImgExtensions = { ".jpg" }; + private static readonly string[] _trickplayImgExtensions = [".jpg"]; /// /// Initializes a new instance of the class. @@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager /// The image encoder. /// The database provider. /// The application paths. + /// The path manager. public TrickplayManager( ILogger logger, IMediaEncoder mediaEncoder, @@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager IServerConfigurationManager config, IImageEncoder imageEncoder, IDbContextFactory dbProvider, - IApplicationPaths appPaths) + IApplicationPaths appPaths, + IPathManager pathManager) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager _imageEncoder = imageEncoder; _dbProvider = dbProvider; _appPaths = appPaths; + _pathManager = pathManager; } /// @@ -610,12 +615,7 @@ public class TrickplayManager : ITrickplayManager /// public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { - var basePath = _config.ApplicationPaths.TrickplayPath; - var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); - var path = saveWithMedia - ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(basePath, idString); - + var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia); var subdirectory = string.Format( CultureInfo.InvariantCulture, "{0} - {1}x{2}", diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs new file mode 100644 index 000000000..036889810 --- /dev/null +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.IO; + +/// +/// Interface ITrickplayManager. +/// +public interface IPathManager +{ + /// + /// Gets the path to the trickplay image base folder. + /// + /// The item. + /// Whether or not the tile should be saved next to the media file. + /// The absolute path. + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false); +} -- cgit v1.2.3 From 671d801d9f734665d0acbd441246712ad2e3d91f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 02:52:34 +0100 Subject: #13540 Fixed (#13757) #13508 Partially fixed Co-authored-by: JPVenson --- .../Item/BaseItemRepository.cs | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) (limited to 'Jellyfin.Server.Implementations') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 630a169cb..bea69b282 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -963,25 +963,11 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); - var innerQuery = new InternalItemsQuery(filter.User) - { - ExcludeItemTypes = filter.ExcludeItemTypes, - IncludeItemTypes = filter.IncludeItemTypes, - MediaTypes = filter.MediaTypes, - AncestorIds = filter.AncestorIds, - ItemIds = filter.ItemIds, - TopParentIds = filter.TopParentIds, - ParentId = filter.ParentId, - IsAiring = filter.IsAiring, - IsMovie = filter.IsMovie, - IsSports = filter.IsSports, - IsKids = filter.IsKids, - IsNews = filter.IsNews, - IsSeries = filter.IsSeries - }; - var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery); + var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); - query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); + query = query.Where(e => e.Type == returnType); + // this does not seem to be nesseary but it does not make any sense why this isn't working. + // && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); if (filter.OrderBy.Count != 0 || !string.IsNullOrEmpty(filter.SearchTerm)) -- cgit v1.2.3