diff options
Diffstat (limited to 'Emby.Server.Implementations/Library')
8 files changed, 219 insertions, 132 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 28f7ed659..c483f3c61 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -76,13 +76,14 @@ namespace Emby.Server.Implementations.Library private readonly IItemRepository _itemRepository; private readonly IImageProcessor _imageProcessor; private readonly NamingOptions _namingOptions; + private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; /// <summary> /// The _root folder sync lock. /// </summary> - private readonly object _rootFolderSyncLock = new object(); - private readonly object _userRootFolderSyncLock = new object(); + private readonly Lock _rootFolderSyncLock = new(); + private readonly Lock _userRootFolderSyncLock = new(); private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); @@ -112,6 +113,7 @@ 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> public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -127,7 +129,8 @@ namespace Emby.Server.Implementations.Library IItemRepository itemRepository, IImageProcessor imageProcessor, NamingOptions namingOptions, - IDirectoryService directoryService) + IDirectoryService directoryService, + IPeopleRepository peopleRepository) { _appHost = appHost; _logger = loggerFactory.CreateLogger<LibraryManager>(); @@ -144,7 +147,7 @@ namespace Emby.Server.Implementations.Library _imageProcessor = imageProcessor; _cache = new ConcurrentDictionary<Guid, BaseItem>(); _namingOptions = namingOptions; - + _peopleRepository = peopleRepository; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -751,14 +754,7 @@ namespace Emby.Server.Implementations.Library if (folder.Id.IsEmpty()) { - if (string.IsNullOrEmpty(folder.Path)) - { - folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); - } - else - { - folder.Id = GetNewItemId(folder.Path, folder.GetType()); - } + folder.Id = GetNewItemId(folder.Path, folder.GetType()); } var dbItem = GetItemById(folder.Id) as BasePluginFolder; @@ -1053,9 +1049,17 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var folder in GetUserRootFolder().Children.OfType<Folder>()) + foreach (var child in GetUserRootFolder().Children.OfType<Folder>()) { - await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); + // If the user has somehow deleted the collection directory, remove the metadata from the database. + if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) + { + _itemRepository.DeleteItem(collectionFolder.Id); + } + else + { + await child.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } } } @@ -1274,7 +1278,7 @@ namespace Emby.Server.Implementations.Library return ItemIsVisible(item, user) ? item : null; } - public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) + public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { if (query.Recursive && !query.ParentId.IsEmpty()) { @@ -1300,7 +1304,7 @@ namespace Emby.Server.Implementations.Library return itemList; } - public List<BaseItem> GetItemList(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query) { return GetItemList(query, true); } @@ -1324,7 +1328,7 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetCount(query); } - public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) + public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) { SetTopParentIdsOrAncestors(query, parents); @@ -1357,7 +1361,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.GetItemList(query)); } - public List<Guid> GetItemIds(InternalItemsQuery query) + public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query) { if (query.User is not null) { @@ -1955,13 +1959,13 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { + _itemRepository.SaveItems(items, cancellationToken); + foreach (var item in items) { await RunMetadataSavers(item, updateReason).ConfigureAwait(false); } - _itemRepository.SaveItems(items, cancellationToken); - if (ItemUpdated is not null) { foreach (var item in items) @@ -2736,12 +2740,12 @@ namespace Emby.Server.Implementations.Library return path; } - public List<PersonInfo> GetPeople(InternalPeopleQuery query) + public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query) { - return _itemRepository.GetPeople(query); + return _peopleRepository.GetPeople(query); } - public List<PersonInfo> GetPeople(BaseItem item) + public IReadOnlyList<PersonInfo> GetPeople(BaseItem item) { if (item.SupportsPeople) { @@ -2756,12 +2760,12 @@ namespace Emby.Server.Implementations.Library } } - return new List<PersonInfo>(); + return []; } - public List<Person> GetPeopleItems(InternalPeopleQuery query) + public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query) { - return _itemRepository.GetPeopleNames(query) + return _peopleRepository.GetPeopleNames(query) .Select(i => { try @@ -2779,9 +2783,9 @@ namespace Emby.Server.Implementations.Library .ToList()!; // null values are filtered out } - public List<string> GetPeopleNames(InternalPeopleQuery query) + public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query) { - return _itemRepository.GetPeopleNames(query); + return _peopleRepository.GetPeopleNames(query); } public void UpdatePeople(BaseItem item, List<PersonInfo> people) @@ -2790,16 +2794,17 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken) + public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken) { if (!item.SupportsPeople) { return; } - _itemRepository.UpdatePeople(item.Id, people); if (people is not null) { + people = people.Where(e => e is not null).ToArray(); + _peopleRepository.UpdatePeople(item.Id, people); await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); } } @@ -2914,14 +2919,13 @@ namespace Emby.Server.Implementations.Library private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) { - List<BaseItem>? personsToSave = null; - foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); var itemUpdateType = ItemUpdateType.MetadataDownload; var saveEntity = false; + var createEntity = false; var personEntity = GetPerson(person.Name); if (personEntity is null) @@ -2938,6 +2942,7 @@ namespace Emby.Server.Implementations.Library personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); saveEntity = true; + createEntity = true; } foreach (var id in person.ProviderIds) @@ -2965,15 +2970,15 @@ namespace Emby.Server.Implementations.Library if (saveEntity) { - (personsToSave ??= new()).Add(personEntity); + if (createEntity) + { + CreateItems([personEntity], null, CancellationToken.None); + } + await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + CreateItems([personEntity], null, CancellationToken.None); } } - - if (personsToSave is not null) - { - CreateItems(personsToSave, null, CancellationToken.None); - } } private void StartScanInBackground() @@ -3027,7 +3032,7 @@ namespace Emby.Server.Implementations.Library { var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); - libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo]; + libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo]; SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 90a01c052..5795c47cc 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; @@ -38,7 +39,7 @@ namespace Emby.Server.Implementations.Library public class MediaSourceManager : IMediaSourceManager, IDisposable { // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. - private const char LiveStreamIdDelimeter = '_'; + private const char LiveStreamIdDelimiter = '_'; private readonly IServerApplicationHost _appHost; private readonly IItemRepository _itemRepo; @@ -51,7 +52,8 @@ namespace Emby.Server.Implementations.Library private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; private readonly IDirectoryService _directoryService; - + private readonly IMediaStreamRepository _mediaStreamRepository; + private readonly IMediaAttachmentRepository _mediaAttachmentRepository; private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; @@ -69,7 +71,9 @@ namespace Emby.Server.Implementations.Library IFileSystem fileSystem, IUserDataManager userDataManager, IMediaEncoder mediaEncoder, - IDirectoryService directoryService) + IDirectoryService directoryService, + IMediaStreamRepository mediaStreamRepository, + IMediaAttachmentRepository mediaAttachmentRepository) { _appHost = appHost; _itemRepo = itemRepo; @@ -82,6 +86,8 @@ namespace Emby.Server.Implementations.Library _localizationManager = localizationManager; _appPaths = applicationPaths; _directoryService = directoryService; + _mediaStreamRepository = mediaStreamRepository; + _mediaAttachmentRepository = mediaAttachmentRepository; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -89,9 +95,9 @@ namespace Emby.Server.Implementations.Library _providers = providers.ToArray(); } - public List<MediaStream> GetMediaStreams(MediaStreamQuery query) + public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query) { - var list = _itemRepo.GetMediaStreams(query); + var list = _mediaStreamRepository.GetMediaStreams(query); foreach (var stream in list) { @@ -121,7 +127,7 @@ namespace Emby.Server.Implementations.Library return false; } - public List<MediaStream> GetMediaStreams(Guid itemId) + public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery { @@ -131,7 +137,7 @@ namespace Emby.Server.Implementations.Library return GetMediaStreamsForItem(list); } - private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams) + private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams) { foreach (var stream in streams) { @@ -145,13 +151,13 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query) + public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query) { - return _itemRepo.GetMediaAttachments(query); + return _mediaAttachmentRepository.GetMediaAttachments(query); } /// <inheritdoc /> - public List<MediaAttachment> GetMediaAttachments(Guid itemId) + public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId) { return GetMediaAttachments(new MediaAttachmentQuery { @@ -159,7 +165,7 @@ namespace Emby.Server.Implementations.Library }); } - public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) + public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) { var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); @@ -212,7 +218,7 @@ namespace Emby.Server.Implementations.Library list.Add(source); } - return SortMediaSources(list); + return SortMediaSources(list).ToArray(); } /// <inheritdoc />> @@ -307,7 +313,7 @@ namespace Emby.Server.Implementations.Library private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource) { - var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter; + var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter; if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { @@ -332,7 +338,7 @@ namespace Emby.Server.Implementations.Library return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } - public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) + public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) { ArgumentNullException.ThrowIfNull(item); @@ -453,7 +459,7 @@ namespace Emby.Server.Implementations.Library } } - private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) + private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) { return sources.OrderBy(i => { @@ -470,8 +476,7 @@ namespace Emby.Server.Implementations.Library return stream?.Width ?? 0; }) - .Where(i => i.Type != MediaSourceType.Placeholder) - .ToList(); + .Where(i => i.Type != MediaSourceType.Placeholder); } public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) @@ -806,7 +811,7 @@ namespace Emby.Server.Implementations.Library return result.Item1; } - public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) + public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) { var stream = new MediaSourceInfo { @@ -829,10 +834,7 @@ namespace Emby.Server.Implementations.Library await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false); - return new List<MediaSourceInfo> - { - stream - }; + return [stream]; } public async Task CloseLiveStream(string id) @@ -864,11 +866,11 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(key); - var keys = key.Split(LiveStreamIdDelimeter, 2); + var keys = key.Split(LiveStreamIdDelimiter, 2); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); - var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal); + var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal); var keyId = key.Substring(splitIndex + 1); return (provider, keyId); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index a69a0f33f..71c69ec50 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -24,30 +25,23 @@ namespace Emby.Server.Implementations.Library _libraryManager = libraryManager; } - public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) { - var list = new List<BaseItem> - { - item - }; - - list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions)); - - return list; + return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } /// <inheritdoc /> - public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(artist.Genres, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item .GetRecursiveChildren(user, new InternalItemsQuery(user) @@ -63,12 +57,12 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenres(genres, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions) { return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions) { var genreIds = genres.DistinctNames().Select(i => { @@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions) { return _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -97,7 +91,7 @@ namespace Emby.Server.Implementations.Library }); } - public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions) + public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions) { if (item is MusicGenre) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index a03c1214d..14798dda6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (args.IsDirectory) { - // It's a boxset if the path is a directory with [playlist] in its name + // It's a playlist if the path is a directory with [playlist] in its name var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path)); if (string.IsNullOrEmpty(filename)) { diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 7f3f8615e..3ac1d0219 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.Library } }; - List<BaseItem> mediaItems; + IReadOnlyList<BaseItem> mediaItems; if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 320685b1f..76e564d53 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask /// <inheritdoc /> public Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); - var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + var posters = GetItemsWithImageType(ImageType.Primary) + .Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path) + .Where(path => !string.IsNullOrEmpty(path)) + .Select(path => path!) + .ToList(); + var backdrops = GetItemsWithImageType(ImageType.Thumb) + .Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path) + .Where(path => !string.IsNullOrEmpty(path)) + .Select(path => path!) + .ToList(); if (backdrops.Count == 0) { // Thumb images fit better because they include the title in the image but are not provided with TMDb. // Using backdrops as a fallback to generate an image at all _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen"); - backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + backdrops = GetItemsWithImageType(ImageType.Backdrop) + .Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path) + .Where(path => !string.IsNullOrEmpty(path)) + .Select(path => path!) + .ToList(); } _imageEncoder.CreateSplashscreen(posters, backdrops); diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 62d22b23f..a41ef888b 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -1,17 +1,21 @@ +#pragma warning disable RS0030 // Do not use banned APIs + using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; +using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; using AudioBook = MediaBrowser.Controller.Entities.AudioBook; using Book = MediaBrowser.Controller.Entities.Book; @@ -26,22 +30,18 @@ namespace Emby.Server.Implementations.Library new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); private readonly IServerConfigurationManager _config; - private readonly IUserManager _userManager; - private readonly IUserDataRepository _repository; + private readonly IDbContextFactory<JellyfinDbContext> _repository; /// <summary> /// Initializes a new instance of the <see cref="UserDataManager"/> class. /// </summary> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param> + /// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param> public UserDataManager( IServerConfigurationManager config, - IUserManager userManager, - IUserDataRepository repository) + IDbContextFactory<JellyfinDbContext> repository) { _config = config; - _userManager = userManager; _repository = repository; } @@ -59,13 +59,27 @@ namespace Emby.Server.Implementations.Library var keys = item.GetUserDataKeys(); - var userId = user.InternalId; + using var dbContext = _repository.CreateDbContext(); + using var transaction = dbContext.Database.BeginTransaction(); foreach (var key in keys) { - _repository.SaveUserData(userId, key, userData, cancellationToken); + userData.Key = key; + var userDataEntry = Map(userData, user.Id, item.Id); + if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey)) + { + dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified; + } + else + { + dbContext.UserData.Add(userDataEntry); + } } + dbContext.SaveChanges(); + transaction.Commit(); + + var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData); @@ -84,10 +98,9 @@ namespace Emby.Server.Implementations.Library { ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(item); - ArgumentNullException.ThrowIfNull(reason); ArgumentNullException.ThrowIfNull(userDataDto); - var userData = GetUserData(user, item); + var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null."); if (userDataDto.PlaybackPositionTicks.HasValue) { @@ -127,33 +140,91 @@ namespace Emby.Server.Implementations.Library SaveUserData(user, item, userData, reason, CancellationToken.None); } - private UserItemData GetUserData(User user, Guid itemId, List<string> keys) + private UserData Map(UserItemData dto, Guid userId, Guid itemId) { - var userId = user.InternalId; - - var cacheKey = GetCacheKey(userId, itemId); + return new UserData() + { + ItemId = itemId, + CustomDataKey = dto.Key, + Item = null, + User = null, + AudioStreamIndex = dto.AudioStreamIndex, + IsFavorite = dto.IsFavorite, + LastPlayedDate = dto.LastPlayedDate, + Likes = dto.Likes, + PlaybackPositionTicks = dto.PlaybackPositionTicks, + PlayCount = dto.PlayCount, + Played = dto.Played, + Rating = dto.Rating, + UserId = userId, + SubtitleStreamIndex = dto.SubtitleStreamIndex, + }; + } - return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys)); + private UserItemData Map(UserData dto) + { + return new UserItemData() + { + Key = dto.CustomDataKey!, + AudioStreamIndex = dto.AudioStreamIndex, + IsFavorite = dto.IsFavorite, + LastPlayedDate = dto.LastPlayedDate, + Likes = dto.Likes, + PlaybackPositionTicks = dto.PlaybackPositionTicks, + PlayCount = dto.PlayCount, + Played = dto.Played, + Rating = dto.Rating, + SubtitleStreamIndex = dto.SubtitleStreamIndex, + }; } - private UserItemData GetUserDataInternal(long internalUserId, List<string> keys) + private UserItemData? GetUserData(User user, Guid itemId, List<string> keys) { - var userData = _repository.GetUserData(internalUserId, keys); + var cacheKey = GetCacheKey(user.InternalId, itemId); - if (userData is not null) + if (_userData.TryGetValue(cacheKey, out var data)) { - return userData; + return data; } - if (keys.Count > 0) + data = GetUserDataInternal(user.Id, itemId, keys); + + if (data is null) { - return new UserItemData + return new UserItemData() { - Key = keys[0] + Key = keys[0], }; } - throw new UnreachableException(); + return _userData.GetOrAdd(cacheKey, data); + } + + private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys) + { + if (keys.Count == 0) + { + return null; + } + + using var context = _repository.CreateDbContext(); + var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray(); + + if (userData.Length > 0) + { + var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N")); + if (directDataReference is not null) + { + return Map(directDataReference); + } + + return Map(userData.First()); + } + + return new UserItemData + { + Key = keys.Last()! + }; } /// <summary> @@ -166,20 +237,25 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public UserItemData GetUserData(User user, BaseItem item) + public UserItemData? GetUserData(User user, BaseItem item) { return GetUserData(user, item.Id, item.GetUserDataKeys()); } /// <inheritdoc /> - public UserItemDataDto GetUserDataDto(BaseItem item, User user) + public UserItemDataDto? GetUserDataDto(BaseItem item, User user) => GetUserDataDto(item, null, user, new DtoOptions()); /// <inheritdoc /> - public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options) + public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options) { var userData = GetUserData(user, item); - var dto = GetUserItemDataDto(userData); + if (userData is null) + { + return null; + } + + var dto = GetUserItemDataDto(userData, item.Id); item.FillUserDataDtoValues(dto, userData, itemDto, user, options); return dto; @@ -189,9 +265,10 @@ namespace Emby.Server.Implementations.Library /// Converts a UserItemData to a DTOUserItemData. /// </summary> /// <param name="data">The data.</param> + /// <param name="itemId">The reference key to an Item.</param> /// <returns>DtoUserItemData.</returns> /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception> - private UserItemDataDto GetUserItemDataDto(UserItemData data) + private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId) { ArgumentNullException.ThrowIfNull(data); @@ -204,6 +281,7 @@ namespace Emby.Server.Implementations.Library Rating = data.Rating, Played = data.Played, LastPlayedDate = data.LastPlayedDate, + ItemId = itemId, Key = data.Key }; } diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index e9cf47d46..d42a0e7d2 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -308,39 +308,40 @@ namespace Emby.Server.Implementations.Library } } - var mediaTypes = new List<MediaType>(); + MediaType[] mediaTypes = []; if (includeItemTypes.Length == 0) { + HashSet<MediaType> tmpMediaTypes = []; foreach (var parent in parents.OfType<ICollectionFolder>()) { switch (parent.CollectionType) { case CollectionType.books: - mediaTypes.Add(MediaType.Book); - mediaTypes.Add(MediaType.Audio); + tmpMediaTypes.Add(MediaType.Book); + tmpMediaTypes.Add(MediaType.Audio); break; case CollectionType.music: - mediaTypes.Add(MediaType.Audio); + tmpMediaTypes.Add(MediaType.Audio); break; case CollectionType.photos: - mediaTypes.Add(MediaType.Photo); - mediaTypes.Add(MediaType.Video); + tmpMediaTypes.Add(MediaType.Photo); + tmpMediaTypes.Add(MediaType.Video); break; case CollectionType.homevideos: - mediaTypes.Add(MediaType.Photo); - mediaTypes.Add(MediaType.Video); + tmpMediaTypes.Add(MediaType.Photo); + tmpMediaTypes.Add(MediaType.Video); break; default: - mediaTypes.Add(MediaType.Video); + tmpMediaTypes.Add(MediaType.Video); break; } } - mediaTypes = mediaTypes.Distinct().ToList(); + mediaTypes = tmpMediaTypes.ToArray(); } - var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 + var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0 ? new[] { BaseItemKind.Person, @@ -366,14 +367,9 @@ namespace Emby.Server.Implementations.Library Limit = limit * 5, IsPlayed = isPlayed, DtoOptions = options, - MediaTypes = mediaTypes.ToArray() + MediaTypes = mediaTypes }; - if (parents.Count == 0) - { - return _libraryManager.GetItemList(query, false); - } - return _libraryManager.GetItemList(query, parents); } } |
