#pragma warning disable RS0030 // Do not use banned APIs #pragma warning disable CA1304 // Specify CultureInfo #pragma warning disable CA1311 // Specify a culture or use an invariant version using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Persistence; using Microsoft.EntityFrameworkCore; using DbLinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType; using LinkedChildType = MediaBrowser.Controller.Entities.LinkedChildType; namespace Jellyfin.Server.Implementations.Item; /// /// Provides linked children query and manipulation operations. /// public class LinkedChildrenService : ILinkedChildrenService { private readonly IDbContextFactory _dbProvider; private readonly IItemTypeLookup _itemTypeLookup; private readonly IItemQueryHelpers _queryHelpers; /// /// Initializes a new instance of the class. /// /// The database context factory. /// The item type lookup. /// The shared query helpers. public LinkedChildrenService( IDbContextFactory dbProvider, IItemTypeLookup itemTypeLookup, IItemQueryHelpers queryHelpers) { _dbProvider = dbProvider; _itemTypeLookup = itemTypeLookup; _queryHelpers = queryHelpers; } /// public IReadOnlyList GetLinkedChildrenIds(Guid parentId, int? childType = null) { using var dbContext = _dbProvider.CreateDbContext(); var query = dbContext.LinkedChildren .Where(lc => lc.ParentId.Equals(parentId)); if (childType.HasValue) { query = query.Where(lc => (int)lc.ChildType == childType.Value); } return query .OrderBy(lc => lc.SortOrder) .Select(lc => lc.ChildId) .ToArray(); } /// public IReadOnlySet GetItemIdsWithAlternateVersions(IReadOnlyList itemIds) { if (itemIds.Count == 0) { return new HashSet(); } using var dbContext = _dbProvider.CreateDbContext(); return dbContext.LinkedChildren .Where(lc => lc.ChildType == DbLinkedChildType.LocalAlternateVersion || lc.ChildType == DbLinkedChildType.LinkedAlternateVersion) .WhereOneOrMany(itemIds, lc => lc.ParentId) .Select(lc => lc.ParentId) .Distinct() .ToHashSet(); } /// public IReadOnlyDictionary FindArtists(IReadOnlyList artistNames) { using var dbContext = _dbProvider.CreateDbContext(); var cleanNames = artistNames.Select(n => (Original: n, Clean: n.GetCleanValue())).ToArray(); var cleanValues = cleanNames.Select(x => x.Clean).ToArray(); var artists = dbContext.BaseItems .AsNoTracking() .Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!) .Where(e => cleanValues.Contains(e.CleanName)) .ToArray(); var lookup = artists .GroupBy(e => e.CleanName!) .ToDictionary( g => g.Key, g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast().ToArray()); var result = new Dictionary(cleanNames.Length); foreach (var (original, clean) in cleanNames) { if (lookup.TryGetValue(clean, out var artistArray)) { result[original] = artistArray; } } return result; } /// public IReadOnlyList GetManualLinkedParentIds(Guid childId, BaseItemKind? parentType = null) { using var context = _dbProvider.CreateDbContext(); var query = context.LinkedChildren .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual); if (parentType.HasValue) { var parentTypeName = _itemTypeLookup.BaseItemKindNames[parentType.Value]; query = query.Join( context.BaseItems .Where(item => item.Type == parentTypeName), lc => lc.ParentId, item => item.Id, (lc, _) => lc); } return query.Select(lc => lc.ParentId).Distinct().ToList(); } /// public IReadOnlyList RerouteLinkedChildren(Guid fromChildId, Guid toChildId) { using var context = _dbProvider.CreateDbContext(); var affectedParentIds = context.LinkedChildren .Where(lc => lc.ChildId == fromChildId && lc.ChildType == DbLinkedChildType.Manual) .Select(lc => lc.ParentId) .Distinct() .ToList(); if (affectedParentIds.Count == 0) { return affectedParentIds; } var parentsWithTarget = context.LinkedChildren .Where(lc => lc.ChildId == toChildId && lc.ChildType == DbLinkedChildType.Manual) .Select(lc => lc.ParentId) .ToHashSet(); context.LinkedChildren .Where(lc => lc.ChildId == fromChildId && lc.ChildType == DbLinkedChildType.Manual && !parentsWithTarget.Contains(lc.ParentId)) .ExecuteUpdate(s => s.SetProperty(e => e.ChildId, toChildId)); context.LinkedChildren .Where(lc => lc.ChildId == fromChildId && lc.ChildType == DbLinkedChildType.Manual && parentsWithTarget.Contains(lc.ParentId)) .ExecuteDelete(); return affectedParentIds; } /// public void UpsertLinkedChild(Guid parentId, Guid childId, LinkedChildType childType) { using var context = _dbProvider.CreateDbContext(); var dbChildType = (DbLinkedChildType)childType; var existingLink = context.LinkedChildren .FirstOrDefault(lc => lc.ParentId == parentId && lc.ChildId == childId); if (existingLink is null) { var nextSortOrder = (context.LinkedChildren .Where(lc => lc.ParentId == parentId) .Max(lc => (int?)lc.SortOrder) ?? -1) + 1; context.LinkedChildren.Add(new Jellyfin.Database.Implementations.Entities.LinkedChildEntity { ParentId = parentId, ChildId = childId, ChildType = dbChildType, SortOrder = nextSortOrder }); } else { existingLink.ChildType = dbChildType; } context.SaveChanges(); } }