< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.LibraryManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/LibraryManager.cs
Line coverage
36%
Covered lines: 633
Uncovered lines: 1079
Coverable lines: 1712
Total lines: 4023
Line coverage: 36.9%
Branch coverage
29%
Covered branches: 268
Total branches: 916
Branch coverage: 29.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/1/2026 - 12:13:05 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33895/4/2026 - 12:15:16 AM Line coverage: 35.8% (572/1596) Branch coverage: 30.5% (261/854) Total lines: 37595/5/2026 - 12:15:44 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/7/2026 - 12:15:44 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/8/2026 - 12:15:13 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/9/2026 - 12:15:41 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/11/2026 - 12:15:59 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/14/2026 - 12:15:54 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/15/2026 - 12:15:55 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/16/2026 - 12:15:55 AM Line coverage: 35.5% (583/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/19/2026 - 12:15:13 AM Line coverage: 35.3% (580/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/20/2026 - 12:15:44 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/21/2026 - 12:15:22 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/22/2026 - 12:15:17 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/26/2026 - 12:15:12 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/27/2026 - 12:15:38 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/28/2026 - 12:15:50 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/29/2026 - 12:15:07 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/31/2026 - 12:15:44 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/3/2026 - 12:16:02 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/4/2026 - 12:15:59 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/5/2026 - 12:16:38 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/6/2026 - 12:15:50 AM Line coverage: 35.3% (581/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/7/2026 - 12:16:09 AM Line coverage: 35.5% (584/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/8/2026 - 12:16:15 AM Line coverage: 35.4% (585/1651) Branch coverage: 27.7% (245/884) Total lines: 38846/9/2026 - 12:16:23 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/19/2026 - 12:16:12 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/21/2026 - 12:16:13 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/23/2026 - 12:15:56 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/25/2026 - 12:12:37 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/27/2026 - 12:15:59 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/29/2026 - 12:15:14 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/2/2026 - 12:15:20 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/5/2026 - 12:16:51 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/9/2026 - 12:13:25 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/10/2026 - 12:11:57 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/14/2026 - 12:13:58 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/16/2026 - 12:13:45 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/17/2026 - 12:11:47 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/21/2026 - 12:16:33 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/23/2026 - 12:16:48 AM Line coverage: 35.3% (586/1659) Branch coverage: 27.7% (246/888) Total lines: 39027/24/2026 - 12:13:56 AM Line coverage: 35.5% (589/1659) Branch coverage: 27.7% (246/888) Total lines: 39027/26/2026 - 12:17:51 AM Line coverage: 36.1% (601/1661) Branch coverage: 28.7% (256/890) Total lines: 39067/28/2026 - 12:13:49 AM Line coverage: 36% (598/1661) Branch coverage: 28.7% (256/890) Total lines: 39067/29/2026 - 12:15:53 AM Line coverage: 36.1% (601/1661) Branch coverage: 28.7% (256/890) Total lines: 39067/30/2026 - 12:15:56 AM Line coverage: 36% (598/1661) Branch coverage: 28.7% (256/890) Total lines: 39068/1/2026 - 12:16:36 AM Line coverage: 36.1% (601/1661) Branch coverage: 28.7% (256/890) Total lines: 39068/3/2026 - 12:16:46 AM Line coverage: 37.1% (630/1698) Branch coverage: 29.3% (267/910) Total lines: 39898/4/2026 - 12:15:21 AM Line coverage: 36.9% (627/1698) Branch coverage: 29.3% (267/910) Total lines: 39898/6/2026 - 12:17:15 AM Line coverage: 36.9% (633/1711) Branch coverage: 29.2% (268/916) Total lines: 40178/7/2026 - 12:16:14 AM Line coverage: 36.8% (630/1711) Branch coverage: 29.2% (268/916) Total lines: 40178/8/2026 - 12:17:31 AM Line coverage: 36.9% (633/1711) Branch coverage: 29.2% (268/916) Total lines: 40178/9/2026 - 12:16:58 AM Line coverage: 36.9% (633/1712) Branch coverage: 29.2% (268/916) Total lines: 4023 5/1/2026 - 12:13:05 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33895/4/2026 - 12:15:16 AM Line coverage: 35.8% (572/1596) Branch coverage: 30.5% (261/854) Total lines: 37595/5/2026 - 12:15:44 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/7/2026 - 12:15:44 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/8/2026 - 12:15:13 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/9/2026 - 12:15:41 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/11/2026 - 12:15:59 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/14/2026 - 12:15:54 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/15/2026 - 12:15:55 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/16/2026 - 12:15:55 AM Line coverage: 35.5% (583/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/19/2026 - 12:15:13 AM Line coverage: 35.3% (580/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/20/2026 - 12:15:44 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/21/2026 - 12:15:22 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/22/2026 - 12:15:17 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/26/2026 - 12:15:12 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/27/2026 - 12:15:38 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/28/2026 - 12:15:50 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/29/2026 - 12:15:07 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/31/2026 - 12:15:44 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/3/2026 - 12:16:02 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/4/2026 - 12:15:59 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/5/2026 - 12:16:38 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/6/2026 - 12:15:50 AM Line coverage: 35.3% (581/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/7/2026 - 12:16:09 AM Line coverage: 35.5% (584/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/8/2026 - 12:16:15 AM Line coverage: 35.4% (585/1651) Branch coverage: 27.7% (245/884) Total lines: 38846/9/2026 - 12:16:23 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/19/2026 - 12:16:12 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/21/2026 - 12:16:13 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/23/2026 - 12:15:56 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/25/2026 - 12:12:37 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/27/2026 - 12:15:59 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38906/29/2026 - 12:15:14 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/2/2026 - 12:15:20 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/5/2026 - 12:16:51 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/9/2026 - 12:13:25 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/10/2026 - 12:11:57 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/14/2026 - 12:13:58 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/16/2026 - 12:13:45 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/17/2026 - 12:11:47 AM Line coverage: 35.5% (589/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/21/2026 - 12:16:33 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 38907/23/2026 - 12:16:48 AM Line coverage: 35.3% (586/1659) Branch coverage: 27.7% (246/888) Total lines: 39027/24/2026 - 12:13:56 AM Line coverage: 35.5% (589/1659) Branch coverage: 27.7% (246/888) Total lines: 39027/26/2026 - 12:17:51 AM Line coverage: 36.1% (601/1661) Branch coverage: 28.7% (256/890) Total lines: 39067/28/2026 - 12:13:49 AM Line coverage: 36% (598/1661) Branch coverage: 28.7% (256/890) Total lines: 39067/29/2026 - 12:15:53 AM Line coverage: 36.1% (601/1661) Branch coverage: 28.7% (256/890) Total lines: 39067/30/2026 - 12:15:56 AM Line coverage: 36% (598/1661) Branch coverage: 28.7% (256/890) Total lines: 39068/1/2026 - 12:16:36 AM Line coverage: 36.1% (601/1661) Branch coverage: 28.7% (256/890) Total lines: 39068/3/2026 - 12:16:46 AM Line coverage: 37.1% (630/1698) Branch coverage: 29.3% (267/910) Total lines: 39898/4/2026 - 12:15:21 AM Line coverage: 36.9% (627/1698) Branch coverage: 29.3% (267/910) Total lines: 39898/6/2026 - 12:17:15 AM Line coverage: 36.9% (633/1711) Branch coverage: 29.2% (268/916) Total lines: 40178/7/2026 - 12:16:14 AM Line coverage: 36.8% (630/1711) Branch coverage: 29.2% (268/916) Total lines: 40178/8/2026 - 12:17:31 AM Line coverage: 36.9% (633/1711) Branch coverage: 29.2% (268/916) Total lines: 40178/9/2026 - 12:16:58 AM Line coverage: 36.9% (633/1712) Branch coverage: 29.2% (268/916) Total lines: 4023

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_RootFolder()100%44100%
get_LibraryMonitor()100%11100%
get_ProviderManager()100%11100%
get_UserViewManager()100%11100%
AddParts(...)100%11100%
RecordConfigurationValues(...)100%11100%
ConfigurationUpdated(...)100%22100%
RegisterItem(...)50%191055.55%
DeleteItem(...)100%210%
DeleteItem(...)0%620%
DeleteItemsUnsafeFast(...)7.14%170147.4%
DeleteItem(...)0%4692680%
DeleteItemPath(...)0%272160%
IsInternalItem(...)0%342180%
GetMetadataPaths(...)0%620%
GetInternalMetadataPaths(...)0%7280%
ResolveItem(...)100%44100%
Resolve(...)100%1140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)83.33%66100%
ResolvePath(...)50%22100%
SetAdditionalPartsFromStack(...)0%156120%
ResolveAlternateVersion(...)0%156120%
ResolvePath(...)65%242078.12%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%221258.33%
ResolveFileList()100%4472.72%
CreateRootFolder()42.85%141488.88%
GetUserRootFolder()80%111079.16%
FindByPath(...)100%210%
GetPerson(...)50%2280%
GetStudio(...)100%210%
GetStudioId(...)100%210%
GetGenreId(...)100%210%
GetMusicGenreId(...)100%210%
GetGenre(...)100%210%
GetMusicGenre(...)100%210%
GetYear(...)0%620%
GetArtist(...)100%210%
GetArtists(...)100%11100%
GetArtist(...)100%210%
CreateItemByName(...)0%7280%
GetItemByNameId(...)100%11100%
ValidatePeopleAsync(...)100%210%
ValidateMediaLibrary(...)100%11100%
ValidateMediaLibraryInternal()100%11100%
ValidateTopLibraryFolders()100%88100%
ClearIgnoreRuleCache()100%11100%
PerformLibraryValidation()100%11100%
RunPostScanTasks()100%2290.9%
GetVirtualFolders()100%11100%
GetVirtualFolders(...)100%22100%
GetVirtualFolderInfo(...)50%101097.14%
GetCollectionType(...)50%6450%
GetItemById(...)66.66%7675%
GetItemById(...)100%22100%
GetItemById(...)50%22100%
GetItemById(...)50%22100%
GetItemList(...)87.5%88100%
GetItemList(...)100%11100%
GetCount(...)0%7280%
GetItemCounts(...)0%7280%
GetItemCountsForNameItem(...)0%620%
GetChildCountBatch(...)100%210%
GetPlayedAndTotalCountBatch(...)100%210%
GetItemList(...)0%4260%
GetLatestItemList(...)0%4260%
GetNextUpSeriesKeys(...)0%4260%
GetNextUpEpisodesBatch(...)100%210%
QueryItems(...)0%2040%
GetItemIds(...)50%2266.66%
GetStudios(...)0%620%
GetGenres(...)0%620%
GetMusicGenres(...)0%620%
GetAllArtists(...)0%620%
GetArtists(...)0%620%
SetTopParentOrAncestorIds(...)0%210140%
GetAlbumArtists(...)0%620%
GetItemsResult(...)80%1010100%
SetTopParentIdsOrAncestors(...)61.11%371861.11%
AddUserToQuery(...)55%202095%
ConfigureUserAccess(...)100%210%
GetTopParentIdsForQuery(...)8.33%4342410.71%
GetIntros()0%620%
GetIntros()100%210%
ResolveIntro(...)0%110100%
GetLocalAlternateVersionIds(...)0%620%
GetLinkedAlternateVersions(...)0%620%
UpsertLinkedChild(...)100%210%
Sort(...)50%341453.33%
Sort(...)57.14%281458.82%
GetComparer(...)75%6450%
CreateItem(...)100%210%
CreateItems(...)53.33%853060.52%
ImageNeedsRefresh(...)0%156120%
UpdateImagesAsync()28.57%1101421.05%
UpdateItemsAsync()60%683065.21%
UpdateItemAsync(...)100%11100%
ForgetDroppedLocalAlternateVersions(...)16.66%24620%
ReattachUserDataAsync()100%11100%
RunMetadataSavers()100%22100%
ReportItemRemoved(...)0%620%
RetrieveItem(...)100%11100%
GetCollectionFolders(...)100%11100%
GetCollectionFolders(...)80%111081.81%
GetCollectionFoldersInternal(...)100%11100%
GetLibraryOptions(...)75%44100%
GetContentType(...)50%4471.42%
GetInheritedContentType(...)50%2283.33%
GetConfiguredContentType(...)100%210%
GetConfiguredContentType(...)100%210%
GetConfiguredContentType(...)50%2266.66%
GetContentTypeOverride(...)50%4485.71%
GetTopFolderContentType(...)37.5%9872.72%
GetNamedView(...)100%210%
GetNamedView(...)0%4260%
GetNamedView(...)0%420200%
GetShadowView(...)0%272160%
GetNamedView(...)0%600240%
GetParentItem(...)33.33%8660%
QueueLibraryScan()100%210%
GetSeasonNumberFromPath(...)0%2040%
FillMissingEpisodeNumbersFromPath(...)0%3192560%
ParseName(...)0%620%
FindExtras(...)84.61%262696%
GetExtraName(...)100%88100%
GetExtraTypeNameKey(...)33.33%341246.66%
GetPathAfterNetworkSubstitution(...)25%6450%
GetPeople(...)100%210%
GetPeople(...)50%11425%
GetPeopleItems(...)100%210%
GetPeopleNames(...)100%210%
GetPeopleNamesByItems(...)100%210%
GetPeopleByItems(...)100%210%
UpdatePeople(...)100%210%
UpdatePeopleAsync()0%2040%
ConvertImageToLocal()0%110100%
AddVirtualFolder()66.66%221877.41%
SavePeopleMetadataAsync()0%272160%
StartScanInBackground()100%11100%
AddMediaPath(...)100%1150%
AddMediaPathInternal(...)33.33%17633.33%
UpdateMediaPath(...)100%210%
SyncLibraryOptionsToLocations(...)0%7280%
RemoveVirtualFolder()62.5%9880%
RemoveContentTypeOverrides(...)0%210140%
RemoveMediaPath(...)25%9433.33%
ItemIsVisible(...)16.66%14640%
CreateShortcut(...)0%620%
RerouteLinkedChildReferencesAsync()0%110100%
GetQueryFiltersLegacy(...)0%620%
GetMediaStreamLanguages(...)100%210%
GetMediaStreamLanguages(...)0%620%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/LibraryManager.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2#pragma warning disable CA5394
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.IO;
 8using System.Linq;
 9using System.Net;
 10using System.Net.Http;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using BitFaster.Caching.Lru;
 14using Emby.Naming.Common;
 15using Emby.Naming.TV;
 16using Emby.Naming.Video;
 17using Emby.Server.Implementations.Library.Resolvers;
 18using Emby.Server.Implementations.Library.Validators;
 19using Emby.Server.Implementations.Playlists;
 20using Emby.Server.Implementations.ScheduledTasks.Tasks;
 21using Emby.Server.Implementations.Sorting;
 22using Jellyfin.Data;
 23using Jellyfin.Data.Enums;
 24using Jellyfin.Database.Implementations.Entities;
 25using Jellyfin.Database.Implementations.Enums;
 26using Jellyfin.Extensions;
 27using MediaBrowser.Common.Extensions;
 28using MediaBrowser.Controller;
 29using MediaBrowser.Controller.Configuration;
 30using MediaBrowser.Controller.Drawing;
 31using MediaBrowser.Controller.Dto;
 32using MediaBrowser.Controller.Entities;
 33using MediaBrowser.Controller.Entities.Audio;
 34using MediaBrowser.Controller.Entities.Movies;
 35using MediaBrowser.Controller.IO;
 36using MediaBrowser.Controller.Library;
 37using MediaBrowser.Controller.LiveTv;
 38using MediaBrowser.Controller.MediaEncoding;
 39using MediaBrowser.Controller.Persistence;
 40using MediaBrowser.Controller.Playlists;
 41using MediaBrowser.Controller.Providers;
 42using MediaBrowser.Controller.Resolvers;
 43using MediaBrowser.Controller.Sorting;
 44using MediaBrowser.Model.Configuration;
 45using MediaBrowser.Model.Drawing;
 46using MediaBrowser.Model.Dto;
 47using MediaBrowser.Model.Entities;
 48using MediaBrowser.Model.Globalization;
 49using MediaBrowser.Model.IO;
 50using MediaBrowser.Model.Library;
 51using MediaBrowser.Model.Querying;
 52using MediaBrowser.Model.Tasks;
 53using Microsoft.Extensions.Logging;
 54using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 55using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 56using Genre = MediaBrowser.Controller.Entities.Genre;
 57using Person = MediaBrowser.Controller.Entities.Person;
 58using VideoResolver = Emby.Naming.Video.VideoResolver;
 59
 60namespace Emby.Server.Implementations.Library
 61{
 62    /// <summary>
 63    /// Class LibraryManager.
 64    /// </summary>
 65    public class LibraryManager : ILibraryManager
 66    {
 67        private const string ShortcutFileExtension = ".mblink";
 68
 69        private readonly ILogger<LibraryManager> _logger;
 70        private readonly ITaskManager _taskManager;
 71        private readonly IUserManager _userManager;
 72        private readonly IUserDataManager _userDataManager;
 73        private readonly IServerConfigurationManager _configurationManager;
 74        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 75        private readonly Lazy<IProviderManager> _providerManagerFactory;
 76        private readonly Lazy<IUserViewManager> _userViewManagerFactory;
 77        private readonly IServerApplicationHost _appHost;
 78        private readonly IMediaEncoder _mediaEncoder;
 79        private readonly IFileSystem _fileSystem;
 80        private readonly IItemRepository _itemRepository;
 81        private readonly IItemPersistenceService _persistenceService;
 82        private readonly INextUpService _nextUpService;
 83        private readonly IItemCountService _countService;
 84        private readonly ILinkedChildrenService _linkedChildrenService;
 85        private readonly IImageProcessor _imageProcessor;
 86        private readonly NamingOptions _namingOptions;
 87        private readonly IPeopleRepository _peopleRepository;
 88        private readonly ExtraResolver _extraResolver;
 89        private readonly IPathManager _pathManager;
 90        private readonly ILocalizationManager _localization;
 91        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 92        private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
 93        private readonly IMediaStreamRepository _mediaStreamRepository;
 94        private readonly Lazy<IExternalDataManager> _externalDataManagerFactory;
 95
 96        /// <summary>
 97        /// The _root folder sync lock.
 98        /// </summary>
 3899        private readonly Lock _rootFolderSyncLock = new();
 38100        private readonly Lock _userRootFolderSyncLock = new();
 101
 38102        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 103
 104        /// <summary>
 105        /// The _root folder.
 106        /// </summary>
 107        private volatile AggregateFolder? _rootFolder;
 108        private volatile UserRootFolder? _userRootFolder;
 109
 110        private bool _wizardCompleted;
 111
 112        /// <summary>
 113        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 114        /// </summary>
 115        /// <param name="appHost">The application host.</param>
 116        /// <param name="loggerFactory">The logger factory.</param>
 117        /// <param name="taskManager">The task manager.</param>
 118        /// <param name="userManager">The user manager.</param>
 119        /// <param name="configurationManager">The configuration manager.</param>
 120        /// <param name="userDataManager">The user data manager.</param>
 121        /// <param name="libraryMonitorFactory">The library monitor.</param>
 122        /// <param name="fileSystem">The file system.</param>
 123        /// <param name="providerManagerFactory">The provider manager.</param>
 124        /// <param name="userViewManagerFactory">The user view manager.</param>
 125        /// <param name="mediaEncoder">The media encoder.</param>
 126        /// <param name="itemRepository">The item repository.</param>
 127        /// <param name="persistenceService">The item persistence service.</param>
 128        /// <param name="nextUpService">The next up service.</param>
 129        /// <param name="countService">The item count service.</param>
 130        /// <param name="linkedChildrenService">The linked children service.</param>
 131        /// <param name="imageProcessor">The image processor.</param>
 132        /// <param name="namingOptions">The naming options.</param>
 133        /// <param name="directoryService">The directory service.</param>
 134        /// <param name="peopleRepository">The people repository.</param>
 135        /// <param name="pathManager">The path manager.</param>
 136        /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
 137        /// <param name="localization">The localization manager.</param>
 138        /// <param name="mediaStreamRepository">The media stream repository.</param>
 139        /// <param name="externalDataManagerFactory">The external data manager (lazy, to break the DI cycle through Chap
 140        public LibraryManager(
 141            IServerApplicationHost appHost,
 142            ILoggerFactory loggerFactory,
 143            ITaskManager taskManager,
 144            IUserManager userManager,
 145            IServerConfigurationManager configurationManager,
 146            IUserDataManager userDataManager,
 147            Lazy<ILibraryMonitor> libraryMonitorFactory,
 148            IFileSystem fileSystem,
 149            Lazy<IProviderManager> providerManagerFactory,
 150            Lazy<IUserViewManager> userViewManagerFactory,
 151            IMediaEncoder mediaEncoder,
 152            IItemRepository itemRepository,
 153            IItemPersistenceService persistenceService,
 154            INextUpService nextUpService,
 155            IItemCountService countService,
 156            ILinkedChildrenService linkedChildrenService,
 157            IImageProcessor imageProcessor,
 158            NamingOptions namingOptions,
 159            IDirectoryService directoryService,
 160            IPeopleRepository peopleRepository,
 161            IPathManager pathManager,
 162            DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
 163            ILocalizationManager localization,
 164            IMediaStreamRepository mediaStreamRepository,
 165            Lazy<IExternalDataManager> externalDataManagerFactory)
 166        {
 38167            _appHost = appHost;
 38168            _logger = loggerFactory.CreateLogger<LibraryManager>();
 38169            _taskManager = taskManager;
 38170            _userManager = userManager;
 38171            _configurationManager = configurationManager;
 38172            _userDataManager = userDataManager;
 38173            _libraryMonitorFactory = libraryMonitorFactory;
 38174            _fileSystem = fileSystem;
 38175            _providerManagerFactory = providerManagerFactory;
 38176            _userViewManagerFactory = userViewManagerFactory;
 38177            _mediaEncoder = mediaEncoder;
 38178            _itemRepository = itemRepository;
 38179            _persistenceService = persistenceService;
 38180            _nextUpService = nextUpService;
 38181            _countService = countService;
 38182            _linkedChildrenService = linkedChildrenService;
 38183            _imageProcessor = imageProcessor;
 184
 38185            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 186
 38187            _namingOptions = namingOptions;
 38188            _peopleRepository = peopleRepository;
 38189            _pathManager = pathManager;
 38190            _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
 38191            _localization = localization;
 38192            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 193
 38194            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 195
 38196            _mediaStreamRepository = mediaStreamRepository;
 38197            _externalDataManagerFactory = externalDataManagerFactory;
 198
 38199            RecordConfigurationValues(_configurationManager.Configuration);
 38200        }
 201
 202        /// <summary>
 203        /// Occurs when [item added].
 204        /// </summary>
 205        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 206
 207        /// <summary>
 208        /// Occurs when [item updated].
 209        /// </summary>
 210        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 211
 212        /// <summary>
 213        /// Occurs when [item removed].
 214        /// </summary>
 215        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 216
 217        /// <summary>
 218        /// Gets the root folder.
 219        /// </summary>
 220        /// <value>The root folder.</value>
 221        public AggregateFolder RootFolder
 222        {
 223            get
 224            {
 192225                if (_rootFolder is null)
 22226                {
 227                    lock (_rootFolderSyncLock)
 228                    {
 22229                        _rootFolder ??= CreateRootFolder();
 22230                    }
 231                }
 232
 192233                return _rootFolder;
 234            }
 235        }
 236
 43237        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 238
 116239        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 240
 1241        private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
 242
 243        /// <summary>
 244        /// Gets or sets the postscan tasks.
 245        /// </summary>
 246        /// <value>The postscan tasks.</value>
 247        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 248
 249        /// <summary>
 250        /// Gets or sets the intro providers.
 251        /// </summary>
 252        /// <value>The intro providers.</value>
 253        private IIntroProvider[] IntroProviders { get; set; } = [];
 254
 255        /// <summary>
 256        /// Gets or sets the list of entity resolution ignore rules.
 257        /// </summary>
 258        /// <value>The entity resolution ignore rules.</value>
 259        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 260
 261        /// <summary>
 262        /// Gets or sets the list of currently registered entity resolvers.
 263        /// </summary>
 264        /// <value>The entity resolvers enumerable.</value>
 265        private IItemResolver[] EntityResolvers { get; set; } = [];
 266
 267        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 268
 269        /// <summary>
 270        /// Gets or sets the comparers.
 271        /// </summary>
 272        /// <value>The comparers.</value>
 273        private IBaseItemComparer[] Comparers { get; set; } = [];
 274
 275        public bool IsScanRunning { get; private set; }
 276
 277        /// <summary>
 278        /// Adds the parts.
 279        /// </summary>
 280        /// <param name="rules">The rules.</param>
 281        /// <param name="resolvers">The resolvers.</param>
 282        /// <param name="introProviders">The intro providers.</param>
 283        /// <param name="itemComparers">The item comparers.</param>
 284        /// <param name="postScanTasks">The post scan tasks.</param>
 285        public void AddParts(
 286            IEnumerable<IResolverIgnoreRule> rules,
 287            IEnumerable<IItemResolver> resolvers,
 288            IEnumerable<IIntroProvider> introProviders,
 289            IEnumerable<IBaseItemComparer> itemComparers,
 290            IEnumerable<ILibraryPostScanTask> postScanTasks)
 291        {
 38292            EntityResolutionIgnoreRules = rules.ToArray();
 38293            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 38294            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 38295            IntroProviders = introProviders.ToArray();
 38296            Comparers = itemComparers.ToArray();
 38297            PostScanTasks = postScanTasks.ToArray();
 38298        }
 299
 300        /// <summary>
 301        /// Records the configuration values.
 302        /// </summary>
 303        /// <param name="configuration">The configuration.</param>
 304        private void RecordConfigurationValues(ServerConfiguration configuration)
 305        {
 144306            _wizardCompleted = configuration.IsStartupWizardCompleted;
 144307        }
 308
 309        /// <summary>
 310        /// Configurations the updated.
 311        /// </summary>
 312        /// <param name="sender">The sender.</param>
 313        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 314        private void ConfigurationUpdated(object? sender, EventArgs e)
 315        {
 106316            var config = _configurationManager.Configuration;
 317
 106318            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 319
 106320            RecordConfigurationValues(config);
 321
 106322            if (wizardChanged)
 323            {
 17324                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 325            }
 106326        }
 327
 328        public void RegisterItem(BaseItem item)
 329        {
 190330            ArgumentNullException.ThrowIfNull(item);
 331
 190332            if (item is IItemByName)
 333            {
 0334                if (item is not MusicArtist)
 335                {
 0336                    return;
 337                }
 338            }
 190339            else if (!item.IsFolder)
 340            {
 0341                if (item is not Video && item is not LiveTvChannel)
 342                {
 0343                    return;
 344                }
 345            }
 346
 190347            _cache.AddOrUpdate(item.Id, item);
 190348        }
 349
 350        public void DeleteItem(BaseItem item, DeleteOptions options)
 351        {
 0352            DeleteItem(item, options, false);
 0353        }
 354
 355        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 356        {
 0357            ArgumentNullException.ThrowIfNull(item);
 358
 0359            var parent = item.GetOwner() ?? item.GetParent();
 360
 0361            DeleteItem(item, options, parent, notifyParentItem);
 0362        }
 363
 364        public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
 365        {
 54366            if (items.Count == 0)
 367            {
 54368                return;
 369            }
 370
 0371            var pathMaps = items.Select(e =>
 0372                (Item: e,
 0373                InternalPath: GetInternalMetadataPaths(e),
 0374                DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
 375
 0376            foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
 377            {
 0378                foreach (var metadataPath in internalPaths)
 379                {
 0380                    if (!Directory.Exists(metadataPath))
 381                    {
 382                        continue;
 383                    }
 384
 0385                    _logger.LogDebug(
 0386                        "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0387                        item.GetType().Name,
 0388                        item.Name ?? "Unknown name",
 0389                        metadataPath,
 0390                        item.Id);
 391
 392                    try
 393                    {
 0394                        Directory.Delete(metadataPath, true);
 0395                    }
 0396                    catch (Exception ex)
 397                    {
 0398                        _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0399                    }
 400                }
 401
 0402                foreach (var fileSystemInfo in pathsToDelete)
 403                {
 0404                    DeleteItemPath(item, false, fileSystemInfo);
 405                }
 406            }
 407
 0408            var externalDataManager = _externalDataManagerFactory.Value;
 0409            foreach (var (item, _, _) in pathMaps)
 410            {
 0411                externalDataManager.DeleteExternalItemFiles(item);
 412            }
 413
 0414            _persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
 0415        }
 416
 417        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 418        {
 0419            ArgumentNullException.ThrowIfNull(item);
 420
 0421            if (item.SourceType == SourceType.Channel)
 422            {
 0423                if (options.DeleteFromExternalProvider)
 424                {
 425                    try
 426                    {
 0427                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0428                    }
 0429                    catch (ArgumentException)
 430                    {
 431                        // channel no longer installed
 0432                    }
 433                }
 434
 0435                options.DeleteFileLocation = false;
 436            }
 437
 0438            if (item is LiveTvProgram)
 439            {
 0440                _logger.LogDebug(
 0441                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0442                    item.GetType().Name,
 0443                    item.Name ?? "Unknown name",
 0444                    item.Path ?? string.Empty,
 0445                    item.Id);
 446            }
 447            else
 448            {
 0449                _logger.LogInformation(
 0450                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0451                    item.GetType().Name,
 0452                    item.Name ?? "Unknown name",
 0453                    item.Path ?? string.Empty,
 0454                    item.Id);
 455            }
 456
 457            // If deleting a primary version video, clear PrimaryVersionId from alternate versions
 458            // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
 0459            if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
 460            {
 0461                var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
 0462                var allAlternateVersions = localAlternateIds
 0463                    .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
 0464                    .Distinct()
 0465                    .Select(id => GetItemById(id))
 0466                    .OfType<Video>()
 0467                    .ToList();
 468
 469                // Partition alternates by whether their files still exist on disk
 0470                var alternateVersions = new List<Video>();
 0471                var missingAlternates = new List<Video>();
 0472                foreach (var alt in allAlternateVersions)
 473                {
 0474                    if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
 475                    {
 0476                        missingAlternates.Add(alt);
 477                    }
 478                    else
 479                    {
 0480                        alternateVersions.Add(alt);
 481                    }
 482                }
 483
 484                // Delete alternates whose files no longer exist to avoid ghost items.
 485                // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
 0486                foreach (var missing in missingAlternates)
 487                {
 0488                    _logger.LogInformation(
 0489                        "Deleting missing alternate version {Name} ({Path})",
 0490                        missing.Name ?? "Unknown name",
 0491                        missing.Path ?? string.Empty);
 0492                    missing.SetPrimaryVersionId(null);
 0493                    missing.OwnerId = Guid.Empty;
 0494                    missing.LocalAlternateVersions = [];
 0495                    missing.LinkedAlternateVersions = [];
 0496                    DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
 497                }
 498
 0499                if (alternateVersions.Count > 0)
 500                {
 0501                    _logger.LogInformation(
 0502                        "Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
 0503                        alternateVersions.Count,
 0504                        item.Name ?? "Unknown name");
 505
 506                    // Promote the first alternate version to be the new primary
 0507                    var newPrimary = alternateVersions[0];
 0508                    newPrimary.SetPrimaryVersionId(null);
 0509                    newPrimary.OwnerId = Guid.Empty;
 510
 511                    // Transfer alternate version arrays from old primary to new primary
 512                    // so UpdateToRepositoryAsync creates correct LinkedChildren entries
 0513                    newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
 0514                        .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
 0515                        .ToArray();
 0516                    newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
 0517                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
 0518                        .ToArray();
 519
 0520                    newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter()
 521
 522                    // Re-route playlist/collection references from deleted primary to new primary
 0523                    RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
 524
 525                    // Update remaining alternates to point to new primary
 0526                    foreach (var alternate in alternateVersions.Skip(1))
 527                    {
 0528                        alternate.SetPrimaryVersionId(newPrimary.Id);
 529                        // Only set OwnerId for local alternates; linked alternates are independent items
 0530                        alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
 0531                        alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaite
 532                    }
 533                }
 534            }
 0535            else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
 536            {
 537                // If deleting an alternate version, re-route references to its primary
 0538                RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter()
 539
 540                // Remove deleted alternate from primary's LinkedAlternateVersions
 0541                if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
 542                {
 0543                    primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
 0544                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
 0545                        .ToArray();
 0546                    primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter
 547                }
 548            }
 549
 0550            var children = item.IsFolder
 0551                ? ((Folder)item).GetRecursiveChildren(false)
 0552                : [];
 553
 0554            foreach (var metadataPath in GetMetadataPaths(item, children))
 555            {
 0556                if (!Directory.Exists(metadataPath))
 557                {
 558                    continue;
 559                }
 560
 0561                _logger.LogDebug(
 0562                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0563                    item.GetType().Name,
 0564                    item.Name ?? "Unknown name",
 0565                    metadataPath,
 0566                    item.Id);
 567
 568                try
 569                {
 0570                    Directory.Delete(metadataPath, true);
 0571                }
 0572                catch (Exception ex)
 573                {
 0574                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0575                }
 576            }
 577
 0578            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 579            {
 580                // Assume only the first is required
 581                // Add this flag to GetDeletePaths if required in the future
 0582                var isRequiredForDelete = true;
 583
 0584                foreach (var fileSystemInfo in item.GetDeletePaths())
 585                {
 0586                    DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
 587
 0588                    isRequiredForDelete = false;
 589                }
 590            }
 591
 0592            item.SetParent(null);
 593
 0594            var externalDataManager = _externalDataManagerFactory.Value;
 0595            externalDataManager.DeleteExternalItemFiles(item);
 0596            foreach (var child in children)
 597            {
 0598                externalDataManager.DeleteExternalItemFiles(child);
 599            }
 600
 0601            _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
 0602            _cache.TryRemove(item.Id, out _);
 0603            foreach (var child in children)
 604            {
 0605                _cache.TryRemove(child.Id, out _);
 606            }
 607
 0608            if (parent is Folder folder)
 609            {
 0610                folder.Children = null;
 0611                folder.UserData = null;
 612            }
 613
 0614            ReportItemRemoved(item, parent);
 0615        }
 616
 617        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 618        {
 0619            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 620            {
 621                try
 622                {
 0623                    _logger.LogInformation(
 0624                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0625                        item.GetType().Name,
 0626                        item.Name ?? "Unknown name",
 0627                        fileSystemInfo.FullName,
 0628                        item.Id);
 629
 0630                    if (fileSystemInfo.IsDirectory)
 631                    {
 0632                        Directory.Delete(fileSystemInfo.FullName, true);
 633                    }
 634                    else
 635                    {
 0636                        File.Delete(fileSystemInfo.FullName);
 637                    }
 0638                }
 0639                catch (DirectoryNotFoundException)
 640                {
 0641                    _logger.LogInformation(
 0642                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0643                        item.GetType().Name,
 0644                        item.Name ?? "Unknown name",
 0645                        fileSystemInfo.FullName,
 0646                        item.Id);
 0647                }
 0648                catch (FileNotFoundException)
 649                {
 0650                    _logger.LogInformation(
 0651                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0652                        item.GetType().Name,
 0653                        item.Name ?? "Unknown name",
 0654                        fileSystemInfo.FullName,
 0655                        item.Id);
 0656                }
 0657                catch (IOException)
 658                {
 0659                    if (isRequiredForDelete)
 660                    {
 0661                        throw;
 662                    }
 0663                }
 0664                catch (UnauthorizedAccessException)
 665                {
 0666                    if (isRequiredForDelete)
 667                    {
 0668                        throw;
 669                    }
 0670                }
 671            }
 0672        }
 673
 674        private bool IsInternalItem(BaseItem item)
 675        {
 0676            if (!item.IsFileProtocol)
 677            {
 0678                return false;
 679            }
 680
 0681            var pathToCheck = item switch
 0682            {
 0683                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0684                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0685                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0686                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0687                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0688                Year => _configurationManager.ApplicationPaths.YearPath,
 0689                _ => null
 0690            };
 691
 0692            var itemPath = item.Path;
 0693            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 694            {
 0695                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0696                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 697
 0698                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 699            }
 700
 0701            return false;
 702        }
 703
 704        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 705        {
 0706            var list = GetInternalMetadataPaths(item);
 0707            foreach (var child in children)
 708            {
 0709                list.AddRange(GetInternalMetadataPaths(child));
 710            }
 711
 0712            return list;
 713        }
 714
 715        private List<string> GetInternalMetadataPaths(BaseItem item)
 716        {
 0717            var list = new List<string>
 0718            {
 0719                item.GetInternalMetadataPath()
 0720            };
 721
 0722            if (item is Video video)
 723            {
 724                // Trickplay
 0725                list.Add(_pathManager.GetTrickplayDirectory(video));
 726
 727                // Chapter Images
 0728                list.Add(_pathManager.GetChapterImageFolderPath(video));
 729
 730                // Subtitles and attachments
 0731                foreach (var mediaSource in item.GetMediaSources(false))
 732                {
 0733                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0734                    if (subtitleFolder is not null)
 735                    {
 0736                        list.Add(subtitleFolder);
 737                    }
 738
 0739                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0740                    if (attachmentFolder is not null)
 741                    {
 0742                        list.Add(attachmentFolder);
 743                    }
 744                }
 745            }
 746
 0747            return list;
 748        }
 749
 750        /// <summary>
 751        /// Resolves the item.
 752        /// </summary>
 753        /// <param name="args">The args.</param>
 754        /// <param name="resolvers">The resolvers.</param>
 755        /// <returns>BaseItem.</returns>
 756        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 757        {
 97758            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 97759                .FirstOrDefault(i => i is not null);
 760
 97761            if (item is not null)
 762            {
 87763                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 764            }
 765
 97766            return item;
 767        }
 768
 769        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 770        {
 771            try
 772            {
 329773                return resolver.ResolvePath(args);
 774            }
 0775            catch (Exception ex)
 776            {
 0777                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0778                return null;
 779            }
 329780        }
 781
 782        public Guid GetNewItemId(string key, Type type)
 783        {
 153784            return GetNewItemIdInternal(key, type, false);
 785        }
 786
 787        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 788        {
 154789            ArgumentException.ThrowIfNullOrEmpty(key);
 154790            ArgumentNullException.ThrowIfNull(type);
 791
 154792            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 154793            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 794            {
 795                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 118796                key = key.Substring(programDataPath.Length)
 118797                    .TrimStart('/', '\\')
 118798                    .Replace('/', '\\');
 799            }
 800
 154801            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 802            {
 1803                key = key.ToLowerInvariant();
 804            }
 805
 154806            key = type.FullName + key;
 807
 154808            return key.GetMD5();
 809        }
 810
 811        public BaseItem? ResolvePath(
 812            FileSystemMetadata fileInfo,
 813            Folder? parent = null,
 814            IDirectoryService? directoryService = null,
 815            CollectionType? collectionType = null)
 44816            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType
 817
 818        private void SetAdditionalPartsFromStack(Video altVideo, string path)
 819        {
 0820            if (altVideo.AdditionalParts is { Length: > 0 })
 821            {
 0822                return;
 823            }
 824
 0825            var directory = Path.GetDirectoryName(path);
 0826            if (string.IsNullOrEmpty(directory))
 827            {
 0828                return;
 829            }
 830
 831            IEnumerable<FileSystemMetadata> siblings;
 832            try
 833            {
 0834                siblings = _fileSystem.GetFiles(directory);
 0835            }
 0836            catch (Exception ex)
 837            {
 0838                _logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
 0839                return;
 840            }
 841
 0842            var stacks = StackResolver.Resolve(siblings, _namingOptions);
 0843            foreach (var stack in stacks)
 844            {
 0845                if (stack.Files.Count > 1
 0846                    && string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
 847                {
 0848                    altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
 0849                    return;
 850                }
 851            }
 0852        }
 853
 854        /// <inheritdoc />
 855        public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? colle
 856        {
 857            // Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
 858            // This happens when items were previously resolved without proper type context
 859            // in mixed-content libraries where collectionType is null.
 0860            var expectedId = GetNewItemId(path, expectedVideoType);
 0861            if (expectedVideoType != typeof(Video))
 862            {
 0863                var wrongTypeId = GetNewItemId(path, typeof(Video));
 0864                if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
 865                {
 0866                    _logger.LogInformation(
 0867                        "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
 0868                        wrongTypeItem.GetType().Name,
 0869                        expectedVideoType.Name,
 0870                        path);
 0871                    DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
 872                }
 873            }
 874
 0875            var resolved = ResolvePath(
 0876                _fileSystem.GetFileSystemInfo(path),
 0877                parent,
 0878                collectionType: collectionType) as Video;
 879
 0880            if (resolved is null)
 881            {
 0882                return null;
 883            }
 884
 885            // Ensure the alternate version has the same concrete type as the primary video.
 886            // ResolvePath may return a generic Video for files in mixed-content libraries
 887            // where collectionType is null, even though the primary is a Movie/Episode/etc.
 0888            if (resolved.GetType() != expectedVideoType)
 889            {
 0890                if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
 891                {
 0892                    correctVideo.Path = resolved.Path;
 0893                    correctVideo.Name = resolved.Name;
 0894                    correctVideo.VideoType = resolved.VideoType;
 0895                    correctVideo.ProductionYear = resolved.ProductionYear;
 0896                    correctVideo.ExtraType = resolved.ExtraType;
 0897                    resolved = correctVideo;
 898                }
 899            }
 900
 0901            resolved.Id = expectedId;
 0902            return resolved;
 903        }
 904
 905        private BaseItem? ResolvePath(
 906            FileSystemMetadata fileInfo,
 907            IDirectoryService directoryService,
 908            IItemResolver[]? resolvers,
 909            Folder? parent = null,
 910            CollectionType? collectionType = null,
 911            LibraryOptions? libraryOptions = null)
 912        {
 97913            ArgumentNullException.ThrowIfNull(fileInfo);
 914
 97915            var fullPath = fileInfo.FullName;
 916
 97917            if (collectionType is null && parent is not null)
 918            {
 17919                collectionType = GetContentTypeOverride(fullPath, true);
 920            }
 921
 97922            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 97923            {
 97924                Parent = parent,
 97925                FileInfo = fileInfo,
 97926                CollectionType = collectionType,
 97927                LibraryOptions = libraryOptions
 97928            };
 929
 930            // Return null if ignore rules deem that we should do so
 97931            if (IgnoreFile(args.FileInfo, args.Parent))
 932            {
 0933                return null;
 934            }
 935
 936            // Gather child folder and files
 97937            if (args.IsDirectory)
 938            {
 51939                var isPhysicalRoot = args.IsPhysicalRoot;
 940
 941                // When resolving the root, we need it's grandchildren (children of user views)
 51942                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 943
 944                FileSystemMetadata[] files;
 51945                var isVf = args.IsVf;
 946
 947                try
 948                {
 51949                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 51950                }
 0951                catch (Exception ex)
 952                {
 0953                    if (parent is not null && parent.IsPhysicalRoot)
 954                    {
 0955                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 956
 0957                        files = [];
 958                    }
 959                    else
 960                    {
 0961                        throw;
 962                    }
 0963                }
 964
 965                // Need to remove sub-paths that may have been resolved from shortcuts
 966                // Example: if \\server\movies exists, then strip out \\server\movies\action
 51967                if (isPhysicalRoot)
 968                {
 22969                    files = NormalizeRootPathList(files).ToArray();
 970                }
 971
 51972                args.FileSystemChildren = files;
 973            }
 974
 975            // Filter content based on ignore rules
 97976            if (args.IsDirectory)
 977            {
 51978                var filtered = args.GetActualFileSystemChildren().ToArray();
 51979                args.FileSystemChildren = filtered ?? [];
 980            }
 981
 97982            return ResolveItem(args, resolvers);
 983        }
 984
 985        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 122986            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 987
 988        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 989        {
 84990            var originalList = paths.ToList();
 991
 84992            var list = originalList.Where(i => i.IsDirectory)
 84993                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 84994                .Distinct()
 84995                .ToList();
 996
 84997            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 84998                .ToList();
 999
 1681000            foreach (var dupe in dupes)
 1001            {
 01002                _logger.LogInformation("Found duplicate path: {0}", dupe);
 1003            }
 1004
 841005            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 841006            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 841007            return newList;
 1008        }
 1009
 1010        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 1011        {
 611012            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 1013        }
 1014
 1015        public IEnumerable<BaseItem> ResolvePaths(
 1016            IEnumerable<FileSystemMetadata> files,
 1017            IDirectoryService directoryService,
 1018            Folder parent,
 1019            LibraryOptions libraryOptions,
 1020            CollectionType? collectionType,
 1021            IItemResolver[] resolvers)
 1022        {
 611023            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 1024
 611025            if (parent is not null)
 1026            {
 611027                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 1028
 3661029                foreach (var resolver in multiItemResolvers)
 1030                {
 1221031                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 1032
 1221033                    if (result?.Items.Count > 0)
 1034                    {
 01035                        var items = result.Items;
 01036                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 01037                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 01038                        return items;
 1039                    }
 1040                }
 1041            }
 1042
 611043            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 01044        }
 1045
 1046        private IEnumerable<BaseItem> ResolveFileList(
 1047            IReadOnlyList<FileSystemMetadata> fileList,
 1048            IDirectoryService directoryService,
 1049            Folder? parent,
 1050            CollectionType? collectionType,
 1051            IItemResolver[]? resolvers,
 1052            LibraryOptions libraryOptions)
 1053        {
 1054            // Given that fileList is a list we can save enumerator allocations by indexing
 1541055            for (var i = 0; i < fileList.Count; i++)
 1056            {
 171057                var file = fileList[i];
 171058                BaseItem? result = null;
 1059                try
 1060                {
 171061                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 171062                }
 01063                catch (Exception ex)
 1064                {
 01065                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 01066                }
 1067
 171068                if (result is not null)
 1069                {
 71070                    yield return result;
 1071                }
 1072            }
 601073        }
 1074
 1075        /// <summary>
 1076        /// Creates the root media folder.
 1077        /// </summary>
 1078        /// <returns>AggregateFolder.</returns>
 1079        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 1080        public AggregateFolder CreateRootFolder()
 1081        {
 221082            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 1083
 221084            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 221085                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 221086                             .DeepCopy<Folder, AggregateFolder>();
 1087
 1088            // In case program data folder was moved
 221089            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 1090            {
 01091                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 01092                rootFolder.Path = rootFolderPath;
 1093            }
 1094
 1095            // Add in the plug-in folders
 221096            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 1097
 221098            var info = Directory.CreateDirectory(path);
 221099            Folder folder = new PlaylistsFolder
 221100            {
 221101                Path = path,
 221102                DateCreated = info.CreationTimeUtc,
 221103                DateModified = info.LastWriteTimeUtc,
 221104            };
 1105
 221106            if (folder.Id.IsEmpty())
 1107            {
 221108                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 1109            }
 1110
 221111            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 1112
 221113            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 1114            {
 01115                folder = dbItem;
 1116            }
 1117
 221118            if (!folder.ParentId.Equals(rootFolder.Id))
 1119            {
 221120                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 221121                folder.ParentId = rootFolder.Id;
 221122                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 1123            }
 1124
 221125            rootFolder.AddVirtualChild(folder);
 1126
 221127            RegisterItem(folder);
 1128
 221129            return rootFolder;
 1130        }
 1131
 1132        public Folder GetUserRootFolder()
 1133        {
 9091134            if (_userRootFolder is null)
 221135            {
 1136                lock (_userRootFolderSyncLock)
 1137                {
 221138                    if (_userRootFolder is null)
 1139                    {
 221140                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 1141
 221142                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 221143                        Directory.CreateDirectory(userRootPath);
 1144
 221145                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 221146                        UserRootFolder? tmpItem = null;
 1147                        try
 1148                        {
 221149                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 221150                        }
 01151                        catch (Exception ex)
 1152                        {
 01153                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 01154                        }
 1155
 221156                        if (tmpItem is null)
 1157                        {
 221158                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 221159                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 221160                                        .DeepCopy<Folder, UserRootFolder>();
 1161                        }
 1162
 1163                        // In case program data folder was moved
 221164                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 1165                        {
 01166                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 01167                            tmpItem.Path = userRootPath;
 1168                        }
 1169
 221170                        _userRootFolder = tmpItem;
 221171                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 1172                    }
 221173                }
 1174            }
 1175
 9091176            return _userRootFolder;
 1177        }
 1178
 1179        /// <inheritdoc />
 1180        public BaseItem? FindByPath(string path, bool? isFolder)
 1181        {
 1182            // If this returns multiple items it could be tricky figuring out which one is correct.
 1183            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 01184            ArgumentException.ThrowIfNullOrEmpty(path);
 1185
 01186            var query = new InternalItemsQuery
 01187            {
 01188                Path = path,
 01189                IsFolder = isFolder,
 01190                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 01191                Limit = 1,
 01192                DtoOptions = new DtoOptions(true)
 01193            };
 1194
 01195            return GetItemList(query)
 01196                .FirstOrDefault();
 1197        }
 1198
 1199        /// <inheritdoc />
 1200        public Person? GetPerson(string name)
 1201        {
 11202            var path = Person.GetPath(name);
 11203            var id = GetItemByNameId<Person>(path);
 11204            if (GetItemById(id) is Person item)
 1205            {
 01206                return item;
 1207            }
 1208
 11209            return null;
 1210        }
 1211
 1212        /// <summary>
 1213        /// Gets the studio.
 1214        /// </summary>
 1215        /// <param name="name">The name.</param>
 1216        /// <returns>Task{Studio}.</returns>
 1217        public Studio GetStudio(string name)
 1218        {
 01219            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 1220        }
 1221
 1222        public Guid GetStudioId(string name)
 1223        {
 01224            return GetItemByNameId<Studio>(Studio.GetPath(name));
 1225        }
 1226
 1227        public Guid GetGenreId(string name)
 1228        {
 01229            return GetItemByNameId<Genre>(Genre.GetPath(name));
 1230        }
 1231
 1232        public Guid GetMusicGenreId(string name)
 1233        {
 01234            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 1235        }
 1236
 1237        /// <summary>
 1238        /// Gets the genre.
 1239        /// </summary>
 1240        /// <param name="name">The name.</param>
 1241        /// <returns>Task{Genre}.</returns>
 1242        public Genre GetGenre(string name)
 1243        {
 01244            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 1245        }
 1246
 1247        /// <summary>
 1248        /// Gets the music genre.
 1249        /// </summary>
 1250        /// <param name="name">The name.</param>
 1251        /// <returns>Task{MusicGenre}.</returns>
 1252        public MusicGenre GetMusicGenre(string name)
 1253        {
 01254            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1255        }
 1256
 1257        /// <summary>
 1258        /// Gets the year.
 1259        /// </summary>
 1260        /// <param name="value">The value.</param>
 1261        /// <returns>Task{Year}.</returns>
 1262        public Year GetYear(int value)
 1263        {
 01264            if (value <= 0)
 1265            {
 01266                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1267            }
 1268
 01269            var name = value.ToString(CultureInfo.InvariantCulture);
 1270
 01271            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1272        }
 1273
 1274        /// <summary>
 1275        /// Gets a Genre.
 1276        /// </summary>
 1277        /// <param name="name">The name.</param>
 1278        /// <returns>Task{Genre}.</returns>
 1279        public MusicArtist GetArtist(string name)
 1280        {
 01281            return GetArtist(name, new DtoOptions(true));
 1282        }
 1283
 1284        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1285        {
 181286            return _linkedChildrenService.FindArtists(names);
 1287        }
 1288
 1289        public MusicArtist GetArtist(string name, DtoOptions options)
 1290        {
 01291            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1292        }
 1293
 1294        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1295            where T : BaseItem, new()
 1296        {
 01297            if (typeof(T) == typeof(MusicArtist))
 1298            {
 01299                var existing = GetItemList(new InternalItemsQuery
 01300                {
 01301                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01302                    Name = name,
 01303                    UseRawName = true,
 01304                    DtoOptions = options
 01305                }).Cast<MusicArtist>()
 01306                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01307                .Cast<T>()
 01308                .FirstOrDefault();
 1309
 01310                if (existing is not null)
 1311                {
 01312                    return existing;
 1313                }
 1314            }
 1315
 01316            var path = getPathFn(name);
 01317            var id = GetItemByNameId<T>(path);
 01318            var item = GetItemById(id) as T;
 01319            if (item is null)
 1320            {
 01321                var info = Directory.CreateDirectory(path);
 01322                item = new T
 01323                {
 01324                    Name = name,
 01325                    Id = id,
 01326                    DateCreated = info.CreationTimeUtc,
 01327                    DateModified = info.LastWriteTimeUtc,
 01328                    Path = path
 01329                };
 1330
 01331                CreateItem(item, null);
 1332            }
 1333
 01334            return item;
 1335        }
 1336
 1337        private Guid GetItemByNameId<T>(string path)
 1338              where T : BaseItem, new()
 1339        {
 11340            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11341            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1342        }
 1343
 1344        /// <inheritdoc />
 1345        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1346        {
 1347            // Ensure the location is available.
 01348            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1349
 01350            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1351        }
 1352
 1353        /// <summary>
 1354        /// Reloads the root media folder.
 1355        /// </summary>
 1356        /// <param name="progress">The progress.</param>
 1357        /// <param name="cancellationToken">The cancellation token.</param>
 1358        /// <returns>Task.</returns>
 1359        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1360        {
 1361            // Just run the scheduled task so that the user can see it
 31362            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1363
 31364            return Task.CompletedTask;
 1365        }
 1366
 1367        /// <summary>
 1368        /// Validates the media library internal.
 1369        /// </summary>
 1370        /// <param name="progress">The progress.</param>
 1371        /// <param name="cancellationToken">The cancellation token.</param>
 1372        /// <returns>Task.</returns>
 1373        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1374        {
 201375            IsScanRunning = true;
 201376            ClearIgnoreRuleCache();
 201377            LibraryMonitor.Stop();
 1378
 1379            try
 1380            {
 201381                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 171382            }
 1383            finally
 1384            {
 201385                ClearIgnoreRuleCache();
 201386                LibraryMonitor.Start();
 191387                IsScanRunning = false;
 1388            }
 171389        }
 1390
 1391        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1392        {
 231393            ClearIgnoreRuleCache();
 231394            RootFolder.Children = null;
 231395            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1396
 1397            // Start by just validating the children of the root, but go no further
 221398            await RootFolder.ValidateChildren(
 221399                new Progress<double>(),
 221400                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 221401                recursive: false,
 221402                allowRemoveRoot: removeRoot,
 221403                cancellationToken: cancellationToken).ConfigureAwait(false);
 1404
 211405            var rootFolder = GetUserRootFolder();
 211406            rootFolder.Children = null;
 1407
 211408            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1409
 211410            await rootFolder.ValidateChildren(
 211411                new Progress<double>(),
 211412                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 211413                recursive: false,
 211414                allowRemoveRoot: removeRoot,
 211415                cancellationToken: cancellationToken).ConfigureAwait(false);
 1416
 1417            // Quickly scan CollectionFolders for changes
 211418            var toDelete = new List<Guid>();
 581419            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1420            {
 1421                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 81422                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1423                {
 11424                    toDelete.Add(collectionFolder.Id);
 1425                }
 1426                else
 1427                {
 71428                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1429                }
 1430            }
 1431
 211432            if (toDelete.Count > 0)
 1433            {
 11434                _persistenceService.DeleteItem(toDelete.ToArray());
 1435            }
 1436
 211437            ClearIgnoreRuleCache();
 211438        }
 1439
 1440        /// <inheritdoc />
 1441        public void ClearIgnoreRuleCache()
 1442        {
 841443            _dotIgnoreIgnoreRule.ClearDirectoryCache();
 841444        }
 1445
 1446        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1447        {
 201448            _logger.LogInformation("Validating media library");
 1449
 201450            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1451
 181452            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1453
 1454            // Validate the entire media library
 181455            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1456
 181457            progress.Report(96);
 1458
 181459            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1460
 181461            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1462
 171463            progress.Report(100);
 171464        }
 1465
 1466        /// <summary>
 1467        /// Runs the post scan tasks.
 1468        /// </summary>
 1469        /// <param name="progress">The progress.</param>
 1470        /// <param name="cancellationToken">The cancellation token.</param>
 1471        /// <returns>Task.</returns>
 1472        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1473        {
 181474            var tasks = PostScanTasks.ToList();
 1475
 181476            var numComplete = 0;
 181477            var numTasks = tasks.Count;
 1478
 3231479            foreach (var task in tasks)
 1480            {
 1481                // Prevent access to modified closure
 1441482                var currentNumComplete = numComplete;
 1483
 1441484                var innerProgress = new Progress<double>(pct =>
 1441485                {
 1441486                    double innerPercent = pct;
 1441487                    innerPercent /= 100;
 1441488                    innerPercent += currentNumComplete;
 1441489
 1441490                    innerPercent /= numTasks;
 1441491                    innerPercent *= 100;
 1441492
 1441493                    progress.Report(innerPercent);
 1441494                });
 1495
 1441496                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1497
 1498                try
 1499                {
 1441500                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1431501                }
 11502                catch (OperationCanceledException)
 1503                {
 11504                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 11505                    throw;
 1506                }
 01507                catch (Exception ex)
 1508                {
 01509                    _logger.LogError(ex, "Error running post-scan task");
 01510                }
 1511
 1431512                numComplete++;
 1431513                double percent = numComplete;
 1431514                percent /= numTasks;
 1431515                progress.Report(percent * 100);
 1431516            }
 1517
 171518            _persistenceService.UpdateInheritedValues();
 1519
 171520            progress.Report(100);
 171521        }
 1522
 1523        /// <summary>
 1524        /// Gets the default view.
 1525        /// </summary>
 1526        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1527        public List<VirtualFolderInfo> GetVirtualFolders()
 1528        {
 241529            return GetVirtualFolders(false);
 1530        }
 1531
 1532        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1533        {
 251534            _logger.LogDebug("Getting topLibraryFolders");
 251535            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1536
 251537            _logger.LogDebug("Getting refreshQueue");
 251538            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1539
 251540            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 251541                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 251542                .ToList();
 1543        }
 1544
 1545        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1546        {
 11547            var info = new VirtualFolderInfo
 11548            {
 11549                Name = Path.GetFileName(dir),
 11550
 11551                Locations = _fileSystem.GetFilePaths(dir, false)
 11552                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11553                    .Select(i =>
 11554                    {
 11555                        try
 11556                        {
 11557                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11558                        }
 11559                        catch (Exception ex)
 11560                        {
 11561                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11562                            return null;
 11563                        }
 11564                    })
 11565                    .Where(i => i is not null)
 11566                    .Order()
 11567                    .ToArray(),
 11568
 11569                CollectionType = GetCollectionType(dir)
 11570            };
 1571
 11572            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11573            if (libraryFolder is not null)
 1574            {
 11575                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11576                info.ItemId = libraryFolderId;
 11577                if (libraryFolder.HasImage(ImageType.Primary))
 1578                {
 01579                    info.PrimaryImageItemId = libraryFolderId;
 1580                }
 1581
 11582                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1583
 11584                if (refreshQueue is not null)
 1585                {
 11586                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1587
 11588                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1589                }
 1590            }
 1591
 11592            return info;
 1593        }
 1594
 1595        private CollectionTypeOptions? GetCollectionType(string path)
 1596        {
 11597            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21598            foreach (ReadOnlySpan<char> file in files)
 1599            {
 01600                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1601                {
 01602                    return res;
 1603                }
 1604            }
 1605
 11606            return null;
 01607        }
 1608
 1609        /// <inheritdoc />
 1610        public BaseItem? GetItemById(Guid id)
 1611        {
 5821612            if (id.IsEmpty())
 1613            {
 01614                throw new ArgumentException("Guid can't be empty", nameof(id));
 1615            }
 1616
 5821617            if (_cache.TryGet(id, out var item))
 1618            {
 4561619                return item;
 1620            }
 1621
 1261622            item = RetrieveItem(id);
 1623
 1261624            if (item is not null)
 1625            {
 01626                RegisterItem(item);
 1627            }
 1628
 1261629            return item;
 1630        }
 1631
 1632        /// <inheritdoc />
 1633        public T? GetItemById<T>(Guid id)
 1634            where T : BaseItem
 1635        {
 241636            var item = GetItemById(id);
 241637            if (item is T typedItem)
 1638            {
 11639                return typedItem;
 1640            }
 1641
 231642            return null;
 1643        }
 1644
 1645        /// <inheritdoc />
 1646        public T? GetItemById<T>(Guid id, Guid userId)
 1647            where T : BaseItem
 1648        {
 11649            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11650            return GetItemById<T>(id, user);
 1651        }
 1652
 1653        /// <inheritdoc />
 1654        public T? GetItemById<T>(Guid id, User? user)
 1655            where T : BaseItem
 1656        {
 221657            var item = GetItemById<T>(id);
 221658            return ItemIsVisible(item, user) ? item : null;
 1659        }
 1660
 1661        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1662        {
 1831663            if (query.Recursive && !query.ParentId.IsEmpty())
 1664            {
 451665                var parent = GetItemById(query.ParentId);
 451666                if (parent is not null)
 1667                {
 451668                    SetTopParentIdsOrAncestors(query, [parent]);
 1669                }
 1670            }
 1671
 1831672            if (query.User is not null)
 1673            {
 11674                AddUserToQuery(query, query.User, allowExternalContent);
 1675            }
 1676
 1831677            return _itemRepository.GetItemList(query);
 1678        }
 1679
 1680        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1681        {
 1831682            return GetItemList(query, true);
 1683        }
 1684
 1685        public int GetCount(InternalItemsQuery query)
 1686        {
 01687            if (query.Recursive && !query.ParentId.IsEmpty())
 1688            {
 01689                var parent = GetItemById(query.ParentId);
 01690                if (parent is not null)
 1691                {
 01692                    SetTopParentIdsOrAncestors(query, [parent]);
 1693                }
 1694            }
 1695
 01696            if (query.User is not null)
 1697            {
 01698                AddUserToQuery(query, query.User);
 1699            }
 1700
 01701            return _countService.GetCount(query);
 1702        }
 1703
 1704        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1705        {
 01706            if (query.Recursive && !query.ParentId.IsEmpty())
 1707            {
 01708                var parent = GetItemById(query.ParentId);
 01709                if (parent is not null)
 1710                {
 01711                    SetTopParentIdsOrAncestors(query, [parent]);
 1712                }
 1713            }
 1714
 01715            if (query.User is not null)
 1716            {
 01717                AddUserToQuery(query, query.User);
 1718            }
 1719
 01720            return _countService.GetItemCounts(query);
 1721        }
 1722
 1723        /// <inheritdoc/>
 1724        public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? us
 1725        {
 01726            var query = new InternalItemsQuery(user);
 01727            if (user is not null)
 1728            {
 01729                AddUserToQuery(query, user);
 1730            }
 1731
 01732            return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
 1733        }
 1734
 1735        public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
 1736        {
 01737            return _countService.GetChildCountBatch(parentIds, userId);
 1738        }
 1739
 1740        /// <inheritdoc/>
 1741        public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User
 1742        {
 01743            return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
 1744        }
 1745
 1746        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1747        {
 01748            SetTopParentIdsOrAncestors(query, parents);
 1749
 01750            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1751            {
 01752                if (query.User is not null)
 1753                {
 01754                    AddUserToQuery(query, query.User);
 1755                }
 1756            }
 1757
 01758            return _itemRepository.GetItemList(query);
 1759        }
 1760
 1761        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1762        {
 01763            SetTopParentIdsOrAncestors(query, parents);
 1764
 01765            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1766            {
 01767                if (query.User is not null)
 1768                {
 01769                    AddUserToQuery(query, query.User);
 1770                }
 1771            }
 1772
 01773            return _itemRepository.GetLatestItemList(query, collectionType);
 1774        }
 1775
 1776        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1777        {
 01778            SetTopParentIdsOrAncestors(query, parents);
 1779
 01780            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1781            {
 01782                if (query.User is not null)
 1783                {
 01784                    AddUserToQuery(query, query.User);
 1785                }
 1786            }
 1787
 01788            return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
 1789        }
 1790
 1791        /// <inheritdoc />
 1792        public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisod
 1793            InternalItemsQuery query,
 1794            IReadOnlyList<string> seriesKeys,
 1795            bool includeSpecials,
 1796            bool includeWatchedForRewatching)
 1797        {
 01798            return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching
 1799        }
 1800
 1801        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1802        {
 01803            if (query.User is not null)
 1804            {
 01805                AddUserToQuery(query, query.User);
 1806            }
 1807
 01808            if (query.EnableTotalRecordCount)
 1809            {
 01810                return _itemRepository.GetItems(query);
 1811            }
 1812
 01813            return new QueryResult<BaseItem>(
 01814                query.StartIndex,
 01815                null,
 01816                _itemRepository.GetItemList(query));
 1817        }
 1818
 1819        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1820        {
 901821            if (query.User is not null)
 1822            {
 01823                AddUserToQuery(query, query.User);
 1824            }
 1825
 901826            return _itemRepository.GetItemIdsList(query);
 1827        }
 1828
 1829        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1830        {
 01831            if (query.User is not null)
 1832            {
 01833                AddUserToQuery(query, query.User);
 1834            }
 1835
 01836            SetTopParentOrAncestorIds(query);
 01837            return _itemRepository.GetStudios(query);
 1838        }
 1839
 1840        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1841        {
 01842            if (query.User is not null)
 1843            {
 01844                AddUserToQuery(query, query.User);
 1845            }
 1846
 01847            SetTopParentOrAncestorIds(query);
 01848            return _itemRepository.GetGenres(query);
 1849        }
 1850
 1851        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1852        {
 01853            if (query.User is not null)
 1854            {
 01855                AddUserToQuery(query, query.User);
 1856            }
 1857
 01858            SetTopParentOrAncestorIds(query);
 01859            return _itemRepository.GetMusicGenres(query);
 1860        }
 1861
 1862        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1863        {
 01864            if (query.User is not null)
 1865            {
 01866                AddUserToQuery(query, query.User);
 1867            }
 1868
 01869            SetTopParentOrAncestorIds(query);
 01870            return _itemRepository.GetAllArtists(query);
 1871        }
 1872
 1873        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1874        {
 01875            if (query.User is not null)
 1876            {
 01877                AddUserToQuery(query, query.User);
 1878            }
 1879
 01880            SetTopParentOrAncestorIds(query);
 01881            return _itemRepository.GetArtists(query);
 1882        }
 1883
 1884        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1885        {
 01886            var ancestorIds = query.AncestorIds;
 01887            int len = ancestorIds.Length;
 01888            if (len == 0)
 1889            {
 01890                return;
 1891            }
 1892
 01893            var parents = new BaseItem[len];
 01894            for (int i = 0; i < len; i++)
 1895            {
 01896                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01897                if (parents[i] is not (ICollectionFolder or UserView))
 1898                {
 01899                    return;
 1900                }
 1901            }
 1902
 1903            // Optimize by querying against top level views
 01904            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01905            query.AncestorIds = [];
 1906
 1907            // Prevent searching in all libraries due to empty filter
 01908            if (query.TopParentIds.Length == 0)
 1909            {
 01910                query.TopParentIds = [Guid.NewGuid()];
 1911            }
 01912        }
 1913
 1914        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1915        {
 01916            if (query.User is not null)
 1917            {
 01918                AddUserToQuery(query, query.User);
 1919            }
 1920
 01921            SetTopParentOrAncestorIds(query);
 01922            return _itemRepository.GetAlbumArtists(query);
 1923        }
 1924
 1925        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1926        {
 181927            if (query.Recursive && !query.ParentId.IsEmpty())
 1928            {
 171929                var parent = GetItemById(query.ParentId);
 171930                if (parent is not null)
 1931                {
 171932                    SetTopParentIdsOrAncestors(query, [parent]);
 1933                }
 1934            }
 1935
 181936            if (query.User is not null)
 1937            {
 11938                AddUserToQuery(query, query.User);
 1939            }
 1940
 181941            if (query.EnableTotalRecordCount)
 1942            {
 11943                return _itemRepository.GetItems(query);
 1944            }
 1945
 171946            return new QueryResult<BaseItem>(
 171947                query.StartIndex,
 171948                null,
 171949                _itemRepository.GetItemList(query));
 1950        }
 1951
 1952        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1953        {
 621954            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1955            {
 1956                // Optimize by querying against top level views
 171957                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1958
 1959                // Prevent searching in all libraries due to empty filter
 171960                if (query.TopParentIds.Length == 0)
 1961                {
 171962                    query.TopParentIds = [Guid.NewGuid()];
 1963                }
 1964            }
 451965            else if (parents.Count == 1 && parents.First() is Folder folder
 451966                && (folder is Playlist || folder is BoxSet)
 451967                && folder.LinkedChildren.Length > 0)
 1968            {
 1969                // Playlists and BoxSets store their contents in LinkedChildren and never
 1970                // populate AncestorIds for those items, so a recursive AncestorIds query
 1971                // would return zero rows. Resolve to the linked child IDs up front and
 1972                // route through the existing indexed ItemIds filter.
 01973                query.ItemIds = folder.LinkedChildren
 01974                    .Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
 01975                    .Select(lc => lc.ItemId!.Value)
 01976                    .ToArray();
 1977
 1978                // Empty linked-children should still return empty rather than scanning everything.
 01979                if (query.ItemIds.Length == 0)
 1980                {
 01981                    query.ItemIds = [Guid.NewGuid()];
 1982                }
 1983            }
 1984            else
 1985            {
 1986                // We need to be able to query from any arbitrary ancestor up the tree
 451987                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1988
 1989                // Prevent searching in all libraries due to empty filter
 451990                if (query.AncestorIds.Length == 0)
 1991                {
 01992                    query.AncestorIds = [Guid.NewGuid()];
 1993                }
 1994            }
 1995
 621996            query.Parent = null;
 621997        }
 1998
 1999        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 2000        {
 22001            if (query.User is null)
 2002            {
 02003                query.SetUser(user);
 2004            }
 2005
 22006            if (query.AncestorIds.Length == 0 &&
 22007                query.ParentId.IsEmpty() &&
 22008                query.ChannelIds.Count == 0 &&
 22009                query.TopParentIds.Length == 0 &&
 22010                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 22011                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 22012                query.ItemIds.Length == 0 &&
 22013                query.OwnerIds.Length == 0)
 2014            {
 12015                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 12016                {
 12017                    User = user,
 12018                    IncludeHidden = true,
 12019                    IncludeExternalContent = allowExternalContent
 12020                });
 2021
 12022                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 2023
 2024                // Prevent searching in all libraries due to empty filter
 12025                if (query.TopParentIds.Length == 0)
 2026                {
 12027                    query.TopParentIds = [Guid.NewGuid()];
 2028                }
 2029            }
 22030        }
 2031
 2032        /// <inheritdoc/>
 2033        public void ConfigureUserAccess(InternalItemsQuery query, User user)
 2034        {
 02035            ArgumentNullException.ThrowIfNull(query);
 02036            ArgumentNullException.ThrowIfNull(user);
 2037
 02038            AddUserToQuery(query, user);
 02039        }
 2040
 2041        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 2042        {
 172043            if (item is UserView view)
 2044            {
 02045                if (view.ViewType == CollectionType.livetv)
 2046                {
 02047                    return [view.Id];
 2048                }
 2049
 2050                // Translate view into folders
 02051                if (!view.DisplayParentId.IsEmpty())
 2052                {
 02053                    var displayParent = GetItemById(view.DisplayParentId);
 02054                    if (displayParent is not null)
 2055                    {
 02056                        return GetTopParentIdsForQuery(displayParent, user);
 2057                    }
 2058
 02059                    return [];
 2060                }
 2061
 02062                if (!view.ParentId.IsEmpty())
 2063                {
 02064                    var displayParent = GetItemById(view.ParentId);
 02065                    if (displayParent is not null)
 2066                    {
 02067                        return GetTopParentIdsForQuery(displayParent, user);
 2068                    }
 2069
 02070                    return [];
 2071                }
 2072
 2073                // Handle grouping
 02074                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 02075                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 2076                {
 02077                    return GetUserRootFolder()
 02078                        .GetChildren(user, true)
 02079                        .OfType<CollectionFolder>()
 02080                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 02081                        .Where(i => user.IsFolderGrouped(i.Id))
 02082                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 2083                }
 2084
 02085                return [];
 2086            }
 2087
 172088            if (item is CollectionFolder collectionFolder)
 2089            {
 172090                return collectionFolder.PhysicalFolderIds;
 2091            }
 2092
 02093            var topParent = item.GetTopParent();
 02094            if (topParent is not null)
 2095            {
 02096                return [topParent.Id];
 2097            }
 2098
 02099            return [];
 2100        }
 2101
 2102        /// <summary>
 2103        /// Gets the intros.
 2104        /// </summary>
 2105        /// <param name="item">The item.</param>
 2106        /// <param name="user">The user.</param>
 2107        /// <returns>IEnumerable{System.String}.</returns>
 2108        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 2109        {
 02110            if (IntroProviders.Length == 0)
 2111            {
 02112                return [];
 2113            }
 2114
 02115            var tasks = IntroProviders
 02116                .Select(i => GetIntros(i, item, user));
 2117
 02118            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 2119
 02120            return items
 02121                .SelectMany(i => i)
 02122                .Select(ResolveIntro)
 02123                .Where(i => i is not null)!; // null values got filtered out
 02124        }
 2125
 2126        /// <summary>
 2127        /// Gets the intros.
 2128        /// </summary>
 2129        /// <param name="provider">The provider.</param>
 2130        /// <param name="item">The item.</param>
 2131        /// <param name="user">The user.</param>
 2132        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 2133        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 2134        {
 2135            try
 2136            {
 02137                return await provider.GetIntros(item, user).ConfigureAwait(false);
 2138            }
 02139            catch (Exception ex)
 2140            {
 02141                _logger.LogError(ex, "Error getting intros");
 2142
 02143                return [];
 2144            }
 02145        }
 2146
 2147        /// <summary>
 2148        /// Resolves the intro.
 2149        /// </summary>
 2150        /// <param name="info">The info.</param>
 2151        /// <returns>Video.</returns>
 2152        private Video? ResolveIntro(IntroInfo info)
 2153        {
 02154            Video? video = null;
 2155
 02156            if (info.ItemId.HasValue)
 2157            {
 2158                // Get an existing item by Id
 02159                video = GetItemById(info.ItemId.Value) as Video;
 2160
 02161                if (video is null)
 2162                {
 02163                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 2164                }
 2165            }
 02166            else if (!string.IsNullOrEmpty(info.Path))
 2167            {
 2168                try
 2169                {
 2170                    // Try to resolve the path into a video
 02171                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 2172
 02173                    if (video is null)
 2174                    {
 02175                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 2176                    }
 2177                    else
 2178                    {
 2179                        // Pull the saved db item that will include metadata
 02180                        var dbItem = GetItemById(video.Id) as Video;
 2181
 02182                        if (dbItem is not null)
 2183                        {
 02184                            video = dbItem;
 2185                        }
 2186                        else
 2187                        {
 02188                            return null;
 2189                        }
 2190                    }
 02191                }
 02192                catch (Exception ex)
 2193                {
 02194                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 02195                }
 2196            }
 2197            else
 2198            {
 02199                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 2200            }
 2201
 02202            return video;
 02203        }
 2204
 2205        /// <inheritdoc />
 2206        public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
 2207        {
 02208            ArgumentNullException.ThrowIfNull(video);
 2209
 02210            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02211            if (linkedIds.Count > 0)
 2212            {
 02213                return linkedIds;
 2214            }
 2215
 02216            return [];
 2217        }
 2218
 2219        /// <inheritdoc />
 2220        public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
 2221        {
 02222            ArgumentNullException.ThrowIfNull(video);
 2223
 02224            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02225            if (linkedIds.Count > 0)
 2226            {
 02227                return linkedIds
 02228                    .Select(id => GetItemById(id))
 02229                    .Where(i => i is not null)
 02230                    .OfType<Video>()
 02231                    .OrderBy(i => i.SortName);
 2232            }
 2233
 02234            return [];
 2235        }
 2236
 2237        /// <inheritdoc />
 2238        public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType chil
 2239        {
 02240            _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
 02241        }
 2242
 2243        /// <inheritdoc />
 2244        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 2245        {
 12246            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2247
 42248            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 2249            {
 12250                if (orderBy is RandomComparer)
 2251                {
 02252                    var randomItems = items.ToArray();
 02253                    Random.Shared.Shuffle(randomItems);
 02254                    items = randomItems;
 2255                    // Items are no longer ordered at this point, so set orderedItems back to null
 02256                    orderedItems = null;
 2257                }
 12258                else if (orderedItems is null)
 2259                {
 12260                    orderedItems = sortOrder == SortOrder.Descending
 12261                        ? items.OrderByDescending(i => i, orderBy)
 12262                        : items.OrderBy(i => i, orderBy);
 2263                }
 2264                else
 2265                {
 02266                    orderedItems = sortOrder == SortOrder.Descending
 02267                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 02268                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 2269                }
 2270            }
 2271
 12272            return orderedItems ?? items;
 2273        }
 2274
 2275        /// <inheritdoc />
 2276        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 2277        {
 22278            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2279
 72280            foreach (var (name, sortOrder) in orderBy)
 2281            {
 22282                var comparer = GetComparer(name, user);
 12283                if (comparer is null)
 2284                {
 2285                    continue;
 2286                }
 2287
 12288                if (comparer is RandomComparer)
 2289                {
 02290                    var randomItems = items.ToArray();
 02291                    Random.Shared.Shuffle(randomItems);
 02292                    items = randomItems;
 2293                    // Items are no longer ordered at this point, so set orderedItems back to null
 02294                    orderedItems = null;
 2295                }
 12296                else if (orderedItems is null)
 2297                {
 12298                    orderedItems = sortOrder == SortOrder.Descending
 12299                        ? items.OrderByDescending(i => i, comparer)
 12300                        : items.OrderBy(i => i, comparer);
 2301                }
 2302                else
 2303                {
 02304                    orderedItems = sortOrder == SortOrder.Descending
 02305                        ? orderedItems!.ThenByDescending(i => i, comparer)
 02306                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 2307                }
 2308            }
 2309
 12310            return orderedItems ?? items;
 2311        }
 2312
 2313        /// <summary>
 2314        /// Gets the comparer.
 2315        /// </summary>
 2316        /// <param name="name">The name.</param>
 2317        /// <param name="user">The user.</param>
 2318        /// <returns>IBaseItemComparer.</returns>
 2319        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 2320        {
 32321            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 2322
 32323            if (comparer is IUserBaseItemComparer)
 2324            {
 12325                if (user is null)
 2326                {
 12327                    throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
 2328                }
 2329
 02330                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 2331
 02332                userComparer.User = user;
 02333                userComparer.UserManager = _userManager;
 02334                userComparer.UserDataManager = _userDataManager;
 2335
 02336                return userComparer;
 2337            }
 2338
 22339            return comparer;
 2340        }
 2341
 2342        /// <inheritdoc />
 2343        public void CreateItem(BaseItem item, BaseItem? parent)
 2344        {
 02345            CreateItems([item], parent, CancellationToken.None);
 02346        }
 2347
 2348        /// <inheritdoc />
 2349        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 2350        {
 2351            // Resolve and add any local alternate version items that don't exist yet
 2352            // This ensures they exist in the database when LinkedChildren are processed
 22353            var allItems = new List<BaseItem>(items);
 22354            var parentFolder = parent as Folder;
 22355            var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
 82356            foreach (var item in items)
 2357            {
 22358                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2359                {
 02360                    var videoType = video.GetType();
 02361                    foreach (var path in video.LocalAlternateVersions)
 2362                    {
 02363                        if (string.IsNullOrEmpty(path))
 2364                        {
 2365                            continue;
 2366                        }
 2367
 2368                        // Use the primary video's type for ID calculation to ensure consistency
 02369                        var altId = GetNewItemId(path, videoType);
 02370                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2371                        {
 2372                            // Alternate version doesn't exist, resolve and create it
 2373                            // ensuring it has the same type as the primary video
 02374                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02375                            if (altVideo is not null)
 2376                            {
 02377                                altVideo.OwnerId = video.Id;
 02378                                altVideo.SetPrimaryVersionId(video.Id);
 02379                                altVideo.IsInMixedFolder = video.IsInMixedFolder;
 2380                                // ResolveAlternateVersion only sees the alternate's primary file.
 2381                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2382                                // detect its parts from sibling files so its AdditionalParts persist.
 02383                                SetAdditionalPartsFromStack(altVideo, path);
 02384                                allItems.Add(altVideo);
 2385                            }
 2386                        }
 2387                    }
 2388                }
 2389            }
 2390
 22391            _persistenceService.SaveItems(allItems, cancellationToken);
 2392
 82393            foreach (var item in allItems)
 2394            {
 22395                RegisterItem(item);
 2396            }
 2397
 22398            if (parent is Folder folder)
 2399            {
 22400                folder.Children = null;
 22401                folder.UserData = null;
 2402            }
 2403
 22404            if (ItemAdded is not null)
 2405            {
 82406                foreach (var item in items)
 2407                {
 2408                    // With the live tv guide this just creates too much noise
 22409                    if (item.SourceType != SourceType.Library)
 2410                    {
 2411                        continue;
 2412                    }
 2413
 2414                    try
 2415                    {
 22416                        ItemAdded(
 22417                            this,
 22418                            new ItemChangeEventArgs
 22419                            {
 22420                                Item = item,
 22421                                Parent = parent ?? item.GetParent()
 22422                            });
 22423                    }
 02424                    catch (Exception ex)
 2425                    {
 02426                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02427                    }
 2428                }
 2429            }
 22430        }
 2431
 2432        private bool ImageNeedsRefresh(ItemImageInfo image)
 2433        {
 02434            if (image.Path is not null && image.IsLocalFile)
 2435            {
 02436                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2437                {
 02438                    return true;
 2439                }
 2440
 2441                try
 2442                {
 02443                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2444                }
 02445                catch (Exception ex)
 2446                {
 02447                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02448                    return false;
 2449                }
 2450            }
 2451
 02452            return image.Path is not null && !image.IsLocalFile;
 02453        }
 2454
 2455        /// <inheritdoc />
 2456        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2457        {
 1582458            ArgumentNullException.ThrowIfNull(item);
 2459
 1582460            var outdated = forceUpdate
 1582461                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1582462                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2463
 1582464            var parentItem = item.GetParent();
 1582465            var isLiveTvShow = item.SourceType != SourceType.Library &&
 1582466                               parentItem is not null &&
 1582467                               parentItem.SourceType != SourceType.Library; // not a channel
 2468
 2469            // Skip image processing if current or live tv show
 1582470            if (outdated.Length == 0 || isLiveTvShow)
 2471            {
 1582472                RegisterItem(item);
 1582473                return;
 2474            }
 2475
 02476            foreach (var img in outdated)
 2477            {
 02478                var image = img;
 02479                if (!img.IsLocalFile)
 2480                {
 2481                    try
 2482                    {
 02483                        var index = item.GetImageIndex(img);
 02484                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 02485                    }
 02486                    catch (ArgumentException)
 2487                    {
 02488                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 02489                        continue;
 2490                    }
 02491                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2492                    {
 02493                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 02494                        continue;
 2495                    }
 02496                    catch (HttpRequestException ex)
 2497                    {
 02498                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 02499                        continue;
 2500                    }
 2501                }
 2502
 02503                if (!File.Exists(image.Path))
 2504                {
 02505                    _logger.LogWarning("Image not found at {ImagePath}", image.Path);
 02506                    continue;
 2507                }
 2508
 2509                ImageDimensions size;
 2510                try
 2511                {
 02512                    size = _imageProcessor.GetImageDimensions(item, image);
 02513                    image.Width = size.Width;
 02514                    image.Height = size.Height;
 02515                }
 02516                catch (Exception ex)
 2517                {
 02518                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 02519                    size = default;
 02520                    image.Width = 0;
 02521                    image.Height = 0;
 02522                }
 2523
 2524                try
 2525                {
 02526                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 02527                    image.BlurHash = blurhash;
 02528                }
 02529                catch (Exception ex)
 2530                {
 02531                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 02532                    image.BlurHash = string.Empty;
 02533                }
 2534
 2535                try
 2536                {
 02537                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 02538                    image.DateModified = modifiedDate;
 02539                }
 02540                catch (Exception ex)
 2541                {
 02542                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 02543                }
 02544            }
 2545
 02546            item.ValidateImages();
 2547
 02548            await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
 2549
 02550            RegisterItem(item);
 1582551        }
 2552
 2553        /// <inheritdoc />
 2554        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2555        {
 4602556            foreach (var item in items)
 2557            {
 1152558                item.DateLastSaved = DateTime.UtcNow;
 1152559                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2560
 2561                // Modify again, so saved value is after write time of externally saved metadata
 1152562                item.DateLastSaved = DateTime.UtcNow;
 1152563            }
 2564
 1152565            ForgetDroppedLocalAlternateVersions(items);
 2566
 2567            // Resolve and add any local alternate version items that don't exist yet
 2568            // This ensures they exist in the database when LinkedChildren are processed
 1152569            var allItems = new List<BaseItem>(items);
 1152570            var parentFolder = parent as Folder;
 1152571            var parentCollectionType = GetTopFolderContentType(parent);
 4602572            foreach (var item in items)
 2573            {
 1152574                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2575                {
 02576                    var videoType = video.GetType();
 02577                    foreach (var path in video.LocalAlternateVersions)
 2578                    {
 02579                        if (string.IsNullOrEmpty(path))
 2580                        {
 2581                            continue;
 2582                        }
 2583
 2584                        // Use the primary video's type for ID calculation to ensure consistency
 02585                        var altId = GetNewItemId(path, videoType);
 02586                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2587                        {
 2588                            // Alternate version doesn't exist, resolve and create it
 2589                            // ensuring it has the same type as the primary video
 02590                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02591                            if (altVideo is not null)
 2592                            {
 02593                                altVideo.OwnerId = video.Id;
 02594                                altVideo.SetPrimaryVersionId(video.Id);
 02595                                altVideo.IsInMixedFolder = video.IsInMixedFolder;
 2596                                // ResolveAlternateVersion only sees the alternate's primary file.
 2597                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2598                                // detect its parts from sibling files so its AdditionalParts persist.
 02599                                SetAdditionalPartsFromStack(altVideo, path);
 02600                                allItems.Add(altVideo);
 2601                            }
 2602                        }
 2603                    }
 2604                }
 2605            }
 2606
 1152607            _persistenceService.SaveItems(allItems, cancellationToken);
 2608
 4602609            foreach (var item in allItems)
 2610            {
 1152611                if (!items.Contains(item))
 2612                {
 02613                    RegisterItem(item);
 2614                }
 2615            }
 2616
 1152617            if (parent is Folder folder)
 2618            {
 262619                folder.Children = null;
 262620                folder.UserData = null;
 2621            }
 2622
 1152623            if (ItemUpdated is not null)
 2624            {
 4562625                foreach (var item in items)
 2626                {
 2627                    // With the live tv guide this just creates too much noise
 1142628                    if (item.SourceType != SourceType.Library)
 2629                    {
 2630                        continue;
 2631                    }
 2632
 2633                    try
 2634                    {
 1142635                        ItemUpdated(
 1142636                            this,
 1142637                            new ItemChangeEventArgs
 1142638                            {
 1142639                                Item = item,
 1142640                                Parent = parent,
 1142641                                UpdateReason = updateReason
 1142642                            });
 1142643                    }
 02644                    catch (Exception ex)
 2645                    {
 02646                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 02647                    }
 2648                }
 2649            }
 1152650        }
 2651
 2652        /// <inheritdoc />
 2653        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 1152654            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2655
 2656        /// <summary>
 2657        /// Forgets the cached local alternate versions of the supplied items that they no longer list.
 2658        /// </summary>
 2659        /// <param name="items">The items about to be saved.</param>
 2660        private void ForgetDroppedLocalAlternateVersions(IReadOnlyList<BaseItem> items)
 2661        {
 2302662            foreach (var video in items.OfType<Video>())
 2663            {
 02664                var videoType = video.GetType();
 02665                var keptIds = video.LocalAlternateVersions
 02666                    .Where(path => !string.IsNullOrEmpty(path))
 02667                    .Select(path => GetNewItemId(path, videoType))
 02668                    .ToHashSet();
 2669
 02670                foreach (var versionId in GetLocalAlternateVersionIds(video))
 2671                {
 02672                    if (!keptIds.Contains(versionId))
 2673                    {
 02674                        _cache.TryRemove(versionId, out _);
 2675                    }
 2676                }
 2677            }
 1152678        }
 2679
 2680        /// <inheritdoc />
 2681        public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
 2682        {
 352683            await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
 342684        }
 2685
 2686        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2687        {
 1152688            if (item.IsFileProtocol)
 2689            {
 1152690                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2691            }
 2692
 1152693            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 1152694        }
 2695
 2696        /// <summary>
 2697        /// Reports the item removed.
 2698        /// </summary>
 2699        /// <param name="item">The item.</param>
 2700        /// <param name="parent">The parent item.</param>
 2701        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2702        {
 02703            if (ItemRemoved is not null)
 2704            {
 2705                try
 2706                {
 02707                    ItemRemoved(
 02708                        this,
 02709                        new ItemChangeEventArgs
 02710                        {
 02711                            Item = item,
 02712                            Parent = parent
 02713                        });
 02714                }
 02715                catch (Exception ex)
 2716                {
 02717                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02718                }
 2719            }
 02720        }
 2721
 2722        /// <summary>
 2723        /// Retrieves the item.
 2724        /// </summary>
 2725        /// <param name="id">The id.</param>
 2726        /// <returns>BaseItem.</returns>
 2727        public BaseItem RetrieveItem(Guid id)
 2728        {
 1262729            return _itemRepository.RetrieveItem(id);
 2730        }
 2731
 2732        public List<Folder> GetCollectionFolders(BaseItem item)
 2733        {
 7402734            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2735        }
 2736
 2737        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2738        {
 7562739            while (item is not null)
 2740            {
 7562741                var parent = item.GetParent();
 2742
 7562743                if (parent is AggregateFolder)
 2744                {
 2745                    break;
 2746                }
 2747
 6722748                if (parent is null)
 2749                {
 6562750                    var owner = item.GetOwner();
 2751
 6562752                    if (owner is null)
 2753                    {
 2754                        break;
 2755                    }
 2756
 02757                    item = owner;
 2758                }
 2759                else
 2760                {
 162761                    item = parent;
 2762                }
 2763            }
 2764
 7402765            if (item is null)
 2766            {
 02767                return [];
 2768            }
 2769
 7402770            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2771        }
 2772
 2773        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2774        {
 7402775            return allUserRootChildren
 7402776                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 7402777                .ToList();
 2778        }
 2779
 2780        public LibraryOptions GetLibraryOptions(BaseItem item)
 2781        {
 4772782            if (item is CollectionFolder collectionFolder)
 2783            {
 432784                return collectionFolder.GetLibraryOptions();
 2785            }
 2786
 2787            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 4342788            return GetCollectionFolders(item)
 4342789                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 4342790                ? collectionFolder2.GetLibraryOptions()
 4342791                : new LibraryOptions();
 2792        }
 2793
 2794        public CollectionType? GetContentType(BaseItem item)
 2795        {
 612796            var configuredContentType = GetConfiguredContentType(item, false);
 612797            if (configuredContentType is not null)
 2798            {
 02799                return configuredContentType;
 2800            }
 2801
 612802            configuredContentType = GetConfiguredContentType(item, true);
 612803            if (configuredContentType is not null)
 2804            {
 02805                return configuredContentType;
 2806            }
 2807
 612808            return GetInheritedContentType(item);
 2809        }
 2810
 2811        public CollectionType? GetInheritedContentType(BaseItem item)
 2812        {
 612813            var type = GetTopFolderContentType(item);
 2814
 612815            if (type is not null)
 2816            {
 02817                return type;
 2818            }
 2819
 612820            return item.GetParents()
 612821                .Select(GetConfiguredContentType)
 612822                .LastOrDefault(i => i is not null);
 2823        }
 2824
 2825        public CollectionType? GetConfiguredContentType(BaseItem item)
 2826        {
 02827            return GetConfiguredContentType(item, false);
 2828        }
 2829
 2830        public CollectionType? GetConfiguredContentType(string path)
 2831        {
 02832            return GetContentTypeOverride(path, false);
 2833        }
 2834
 2835        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2836        {
 1222837            if (item is ICollectionFolder collectionFolder)
 2838            {
 02839                return collectionFolder.CollectionType;
 2840            }
 2841
 1222842            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2843        }
 2844
 2845        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2846        {
 1392847            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1392848                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1392849                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1392850                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1392851            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2852            {
 02853                return collectionType;
 2854            }
 2855
 1392856            return null;
 2857        }
 2858
 2859        private CollectionType? GetTopFolderContentType(BaseItem item)
 2860        {
 1782861            if (item is null)
 2862            {
 892863                return null;
 2864            }
 2865
 892866            while (!item.ParentId.IsEmpty())
 2867            {
 02868                var parent = item.GetParent();
 02869                if (parent is null || parent is AggregateFolder)
 2870                {
 2871                    break;
 2872                }
 2873
 02874                item = parent;
 2875            }
 2876
 892877            return GetUserRootFolder().Children
 892878                .OfType<ICollectionFolder>()
 892879                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 892880                .Select(i => i.CollectionType)
 892881                .FirstOrDefault(i => i is not null);
 2882        }
 2883
 2884        public UserView GetNamedView(
 2885            User user,
 2886            string name,
 2887            CollectionType? viewType,
 2888            string sortName)
 2889        {
 02890            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2891        }
 2892
 2893        public UserView GetNamedView(
 2894            string name,
 2895            CollectionType viewType,
 2896            string sortName)
 2897        {
 02898            var path = Path.Combine(
 02899                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02900                "views",
 02901                _fileSystem.GetValidFilename(viewType.ToString()));
 2902
 02903            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2904
 02905            var item = GetItemById(id) as UserView;
 2906
 02907            var refresh = false;
 2908
 02909            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2910            {
 02911                var info = Directory.CreateDirectory(path);
 02912                item = new UserView
 02913                {
 02914                    Path = path,
 02915                    Id = id,
 02916                    DateCreated = info.CreationTimeUtc,
 02917                    DateModified = info.LastWriteTimeUtc,
 02918                    Name = name,
 02919                    ViewType = viewType,
 02920                    ForcedSortName = sortName
 02921                };
 2922
 02923                CreateItem(item, null);
 2924
 02925                refresh = true;
 2926            }
 2927
 02928            if (refresh)
 2929            {
 02930                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02931                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2932            }
 2933
 02934            return item;
 2935        }
 2936
 2937        public UserView GetNamedView(
 2938            User user,
 2939            string name,
 2940            Guid parentId,
 2941            CollectionType? viewType,
 2942            string sortName)
 2943        {
 02944            var parentIdString = parentId.IsEmpty()
 02945                ? null
 02946                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02947            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2948
 02949            var id = GetNewItemId(idValues, typeof(UserView));
 2950
 02951            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2952
 02953            var item = GetItemById(id) as UserView;
 2954
 02955            var isNew = false;
 2956
 02957            if (item is null)
 2958            {
 02959                var info = Directory.CreateDirectory(path);
 02960                item = new UserView
 02961                {
 02962                    Path = path,
 02963                    Id = id,
 02964                    DateCreated = info.CreationTimeUtc,
 02965                    DateModified = info.LastWriteTimeUtc,
 02966                    Name = name,
 02967                    ViewType = viewType,
 02968                    ForcedSortName = sortName,
 02969                    UserId = user.Id,
 02970                    DisplayParentId = parentId
 02971                };
 2972
 02973                CreateItem(item, null);
 2974
 02975                isNew = true;
 2976            }
 2977
 02978            var lastRefreshedUtc = item.DateLastRefreshed;
 02979            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2980
 02981            if (!refresh && !item.DisplayParentId.IsEmpty())
 2982            {
 02983                var displayParent = GetItemById(item.DisplayParentId);
 02984                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2985            }
 2986
 02987            if (refresh)
 2988            {
 02989                ProviderManager.QueueRefresh(
 02990                    item.Id,
 02991                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02992                    {
 02993                        // Need to force save to increment DateLastSaved
 02994                        ForceSave = true
 02995                    },
 02996                    RefreshPriority.Normal);
 2997            }
 2998
 02999            return item;
 3000        }
 3001
 3002        public UserView GetShadowView(
 3003            BaseItem parent,
 3004            CollectionType? viewType,
 3005            string sortName)
 3006        {
 03007            ArgumentNullException.ThrowIfNull(parent);
 3008
 03009            var name = parent.Name;
 03010            var parentId = parent.Id;
 3011
 03012            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 3013
 03014            var id = GetNewItemId(idValues, typeof(UserView));
 3015
 03016            var path = parent.Path;
 3017
 03018            var item = GetItemById(id) as UserView;
 3019
 03020            var isNew = false;
 3021
 03022            if (item is null)
 3023            {
 03024                var info = Directory.CreateDirectory(path);
 03025                item = new UserView
 03026                {
 03027                    Path = path,
 03028                    Id = id,
 03029                    DateCreated = info.CreationTimeUtc,
 03030                    DateModified = info.LastWriteTimeUtc,
 03031                    Name = name,
 03032                    ViewType = viewType,
 03033                    ForcedSortName = sortName,
 03034                    DisplayParentId = parentId
 03035                };
 3036
 03037                CreateItem(item, null);
 3038
 03039                isNew = true;
 3040            }
 3041
 03042            var lastRefreshedUtc = item.DateLastRefreshed;
 03043            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3044
 03045            if (!refresh && !item.DisplayParentId.IsEmpty())
 3046            {
 03047                var displayParent = GetItemById(item.DisplayParentId);
 03048                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3049            }
 3050
 03051            if (refresh)
 3052            {
 03053                ProviderManager.QueueRefresh(
 03054                    item.Id,
 03055                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03056                    {
 03057                        // Need to force save to increment DateLastSaved
 03058                        ForceSave = true
 03059                    },
 03060                    RefreshPriority.Normal);
 3061            }
 3062
 03063            return item;
 3064        }
 3065
 3066        public UserView GetNamedView(
 3067            string name,
 3068            Guid parentId,
 3069            CollectionType? viewType,
 3070            string sortName,
 3071            string uniqueId)
 3072        {
 03073            ArgumentException.ThrowIfNullOrEmpty(name);
 3074
 03075            var parentIdString = parentId.IsEmpty()
 03076                ? null
 03077                : parentId.ToString("N", CultureInfo.InvariantCulture);
 03078            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 03079            if (!string.IsNullOrEmpty(uniqueId))
 3080            {
 03081                idValues += uniqueId;
 3082            }
 3083
 03084            var id = GetNewItemId(idValues, typeof(UserView));
 3085
 03086            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 3087
 03088            var item = GetItemById(id) as UserView;
 3089
 03090            var isNew = false;
 3091
 03092            if (item is null)
 3093            {
 03094                var info = Directory.CreateDirectory(path);
 03095                item = new UserView
 03096                {
 03097                    Path = path,
 03098                    Id = id,
 03099                    DateCreated = info.CreationTimeUtc,
 03100                    DateModified = info.LastWriteTimeUtc,
 03101                    Name = name,
 03102                    ViewType = viewType,
 03103                    ForcedSortName = sortName,
 03104                    DisplayParentId = parentId
 03105                };
 3106
 03107                CreateItem(item, null);
 3108
 03109                isNew = true;
 3110            }
 3111
 03112            if (viewType != item.ViewType)
 3113            {
 03114                item.ViewType = viewType;
 03115                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 3116            }
 3117
 03118            var lastRefreshedUtc = item.DateLastRefreshed;
 03119            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3120
 03121            if (!refresh && !item.DisplayParentId.IsEmpty())
 3122            {
 03123                var displayParent = GetItemById(item.DisplayParentId);
 03124                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3125            }
 3126
 03127            if (refresh)
 3128            {
 03129                ProviderManager.QueueRefresh(
 03130                    item.Id,
 03131                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03132                    {
 03133                        // Need to force save to increment DateLastSaved
 03134                        ForceSave = true
 03135                    },
 03136                    RefreshPriority.Normal);
 3137            }
 3138
 03139            return item;
 3140        }
 3141
 3142        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 3143        {
 33144            if (parentId.HasValue)
 3145            {
 03146                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 3147            }
 3148
 33149            if (!userId.IsNullOrEmpty())
 3150            {
 33151                return GetUserRootFolder();
 3152            }
 3153
 03154            return RootFolder;
 3155        }
 3156
 3157        /// <inheritdoc />
 3158        public void QueueLibraryScan()
 3159        {
 03160            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 03161        }
 3162
 3163        /// <inheritdoc />
 3164        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 3165        {
 03166            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 03167            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 3168        }
 3169
 3170        /// <inheritdoc />
 3171        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 3172        {
 03173            var series = episode.Series;
 03174            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 03175            if (!isAbsoluteNaming.Value)
 3176            {
 3177                // In other words, no filter applied
 03178                isAbsoluteNaming = null;
 3179            }
 3180
 03181            var resolver = new EpisodeResolver(_namingOptions);
 3182
 03183            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 3184
 03185            EpisodeInfo? episodeInfo = null;
 03186            if (episode.IsFileProtocol)
 3187            {
 03188                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 3189                // Resolve from parent folder if it's not the Season folder
 03190                var parent = episode.GetParent();
 03191                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 3192                {
 03193                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 03194                    if (episodeInfo is not null)
 3195                    {
 3196                        // add the container
 03197                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 3198                    }
 3199                }
 3200            }
 3201
 03202            var changed = false;
 03203            if (episodeInfo is null)
 3204            {
 03205                return changed;
 3206            }
 3207
 03208            if (episodeInfo.IsByDate)
 3209            {
 03210                if (episode.IndexNumber.HasValue)
 3211                {
 03212                    episode.IndexNumber = null;
 03213                    changed = true;
 3214                }
 3215
 03216                if (episode.IndexNumberEnd.HasValue)
 3217                {
 03218                    episode.IndexNumberEnd = null;
 03219                    changed = true;
 3220                }
 3221
 03222                if (!episode.PremiereDate.HasValue)
 3223                {
 03224                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 3225                    {
 03226                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 3227                    }
 3228
 03229                    if (episode.PremiereDate.HasValue)
 3230                    {
 03231                        changed = true;
 3232                    }
 3233                }
 3234
 03235                if (episode.ProductionYear is null)
 3236                {
 03237                    episode.ProductionYear = episodeInfo.Year;
 3238
 03239                    if (episode.ProductionYear is not null)
 3240                    {
 03241                        changed = true;
 3242                    }
 3243                }
 3244            }
 3245            else
 3246            {
 03247                if (!episode.IndexNumber.HasValue || forceRefresh)
 3248                {
 03249                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 3250                    {
 03251                        changed = true;
 3252                    }
 3253
 03254                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 3255                }
 3256
 03257                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 3258                {
 03259                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 3260                    {
 03261                        changed = true;
 3262                    }
 3263
 03264                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 3265                }
 3266
 03267                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 3268                {
 03269                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 3270                    {
 03271                        changed = true;
 3272                    }
 3273
 03274                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 3275                }
 3276            }
 3277
 03278            if (!episode.ParentIndexNumber.HasValue)
 3279            {
 03280                var season = episode.Season;
 3281
 03282                if (season is not null)
 3283                {
 03284                    episode.ParentIndexNumber = season.IndexNumber;
 3285                }
 3286
 03287                if (episode.ParentIndexNumber.HasValue)
 3288                {
 03289                    changed = true;
 3290                }
 3291            }
 3292
 03293            return changed;
 3294        }
 3295
 3296        public ItemLookupInfo ParseName(string name)
 3297        {
 03298            var namingOptions = _namingOptions;
 03299            var result = VideoResolver.CleanDateTime(name, namingOptions);
 3300
 03301            return new ItemLookupInfo
 03302            {
 03303                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 03304                Year = result.Year
 03305            };
 3306        }
 3307
 3308        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 3309        {
 3310            // Apply .ignore rules
 143311            var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
 143312            var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.Vide
 143313            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.Containi
 143314            if (ownerVideoInfo is null)
 3315            {
 03316                return [];
 3317            }
 3318
 143319            var candidates = new List<ExtraCandidate>();
 3320
 143321            var count = filtered.Count;
 1423322            for (var i = 0; i < count; i++)
 3323            {
 573324                var current = filtered[i];
 573325                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 3326                {
 83327                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 83328                    var filesInSubFolderList = filesInSubFolder.ToList();
 3329
 83330                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 3331
 383332                    foreach (var file in filesInSubFolderList)
 3333                    {
 113334                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType, ou
 3335                        {
 3336                            continue;
 3337                        }
 3338
 103339                        AddCandidate(file, extraType.Value, extraRule, subFolderIsMixedFolder);
 3340                    }
 3341                }
 493342                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 3343                {
 263344                    AddCandidate(current, extraType.Value, extraRule, false);
 3345                }
 3346            }
 3347
 143348            var extras = new List<BaseItem>();
 143349            var typeCounters = new Dictionary<ExtraType, int>();
 3350
 3351            // Order by path so that the numbering handed out below does not depend on the
 3352            // order the file system happened to list the folder in
 1003353            foreach (var candidate in candidates.OrderBy(c => c.Extra.Path, StringComparer.Ordinal))
 3354            {
 363355                var extra = PrepareExtra(candidate);
 363356                if (extra is not null)
 3357                {
 363358                    extras.Add(extra);
 3359                }
 3360            }
 3361
 143362            return extras;
 3363
 3364            void AddCandidate(FileSystemMetadata file, ExtraType extraType, ExtraRule extraRule, bool isInMixedFolder)
 3365            {
 3366                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 3367                if (extra is Video or Audio)
 3368                {
 3369                    candidates.Add(new ExtraCandidate(extra, extraType, extraRule, isInMixedFolder));
 3370                }
 3371            }
 3372
 3373            BaseItem? PrepareExtra(ExtraCandidate candidate)
 3374            {
 3375                var resolved = candidate.Extra;
 3376                var extra = resolved;
 3377                var name = GetExtraName(candidate, ownerVideoInfo, typeCounters);
 3378
 3379                // Try to retrieve it from the db. If we don't find it, use the resolved version
 3380                var itemById = GetItemById(extra.Id);
 3381                if (itemById is not null)
 3382                {
 3383                    extra = itemById;
 3384                }
 3385
 3386                // An extra is named after its file, so the file is the source of truth. Items created
 3387                // by older versions, or renamed by a metadata provider, are corrected here;
 3388                // RefreshExtras persists the change.
 3389                if (!string.IsNullOrEmpty(name) && extra.LockedFields?.Contains(MetadataField.Name) != true)
 3390                {
 3391                    extra.Name = name;
 3392                }
 3393
 3394                // Only update extra type if it is more specific then the currently known extra type
 3395                if (extra.ExtraType is null or ExtraType.Unknown || candidate.ExtraType != ExtraType.Unknown)
 3396                {
 3397                    extra.ExtraType = candidate.ExtraType;
 3398                }
 3399
 3400                // Only return items that are actual extras (have ExtraType set)
 3401                // Note: OwnerId and ParentId are set by RefreshExtras, not here,
 3402                // so that RefreshExtras can detect when they need updating and set ForceSave.
 3403                if (extra.ExtraType is not null)
 3404                {
 3405                    extra.IsInMixedFolder = candidate.IsInMixedFolder;
 3406                    return extra;
 3407                }
 3408
 3409                return null;
 3410            }
 3411        }
 3412
 3413        /// <summary>
 3414        /// Gets the name to give an extra.
 3415        /// </summary>
 3416        /// <param name="candidate">The resolved extra.</param>
 3417        /// <param name="ownerVideoInfo">The naming info of the owner.</param>
 3418        /// <param name="typeCounters">Number of extras named after their type so far, per type.</param>
 3419        /// <returns>The name.</returns>
 3420        private string GetExtraName(ExtraCandidate candidate, VideoFileInfo ownerVideoInfo, Dictionary<ExtraType, int> t
 3421        {
 363422            var isNamedAfterOwner = candidate.ExtraRule.RuleType switch
 363423            {
 43424                ExtraRuleType.Filename => true,
 203425                ExtraRuleType.Suffix => string.Equals(candidate.Extra.Name, ownerVideoInfo.Name, StringComparison.Ordina
 123426                _ => false
 363427            };
 3428
 363429            if (!isNamedAfterOwner)
 3430            {
 193431                return candidate.Extra.Name;
 3432            }
 3433
 173434            typeCounters.TryGetValue(candidate.ExtraType, out var seen);
 173435            typeCounters[candidate.ExtraType] = seen + 1;
 3436
 173437            var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(candidate.ExtraType));
 3438
 173439            return seen == 0
 173440                ? typeName
 173441                : string.Format(
 173442                    CultureInfo.InvariantCulture,
 173443                    _localization.GetServerLocalizedString("NameExtraNumbered"),
 173444                    typeName,
 173445                    seen + 1);
 3446        }
 3447
 173448        private static string GetExtraTypeNameKey(ExtraType extraType) => extraType switch
 173449        {
 03450            ExtraType.Clip => "NameExtraClip",
 123451            ExtraType.Trailer => "NameExtraTrailer",
 33452            ExtraType.BehindTheScenes => "NameExtraBehindTheScenes",
 03453            ExtraType.DeletedScene => "NameExtraDeletedScene",
 03454            ExtraType.Interview => "NameExtraInterview",
 03455            ExtraType.Scene => "NameExtraScene",
 03456            ExtraType.Sample => "NameExtraSample",
 13457            ExtraType.ThemeSong => "NameExtraThemeSong",
 03458            ExtraType.ThemeVideo => "NameExtraThemeVideo",
 03459            ExtraType.Featurette => "NameExtraFeaturette",
 03460            ExtraType.Short => "NameExtraShort",
 13461            _ => "NameExtraUnknown"
 173462        };
 3463
 3464        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 3465        {
 123466            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 3467            {
 03468                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 3469                {
 03470                    return newPath;
 3471                }
 3472            }
 3473
 63474            return path;
 3475        }
 3476
 3477        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 3478        {
 03479            return _peopleRepository.GetPeople(query).Items;
 3480        }
 3481
 3482        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 3483        {
 63484            if (item.SupportsPeople)
 3485            {
 03486                var people = GetPeople(new InternalPeopleQuery
 03487                {
 03488                    ItemId = item.Id
 03489                });
 3490
 03491                if (people.Count > 0)
 3492                {
 03493                    return people;
 3494                }
 3495            }
 3496
 63497            return [];
 3498        }
 3499
 3500        public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
 3501        {
 03502            var queryResult = _peopleRepository.GetPeople(query);
 03503            var baseItems = queryResult.Items.Select(i =>
 03504                {
 03505                    try
 03506                    {
 03507                        return GetPerson(i.Name);
 03508                    }
 03509                    catch (Exception ex)
 03510                    {
 03511                        _logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
 03512                        return null;
 03513                    }
 03514                })
 03515                .Where(i => i is not null)
 03516                .Where(i => query.User is null || i!.IsVisible(query.User))
 03517                .OfType<BaseItem>()
 03518                .ToList()
 03519                .AsReadOnly();
 3520
 03521            return new QueryResult<BaseItem>
 03522            {
 03523                StartIndex = queryResult.StartIndex,
 03524                TotalRecordCount = queryResult.TotalRecordCount,
 03525                Items = baseItems,
 03526            };
 3527        }
 3528
 3529        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 3530        {
 03531            return _peopleRepository.GetPeopleNames(query);
 3532        }
 3533
 3534        /// <inheritdoc/>
 3535        public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IRead
 3536        {
 03537            return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
 3538        }
 3539
 3540        /// <inheritdoc/>
 3541        public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
 3542        {
 03543            return _peopleRepository.GetPeopleByItems(itemIds);
 3544        }
 3545
 3546        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 3547        {
 03548            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 03549        }
 3550
 3551        /// <inheritdoc />
 3552        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 3553        {
 03554            if (!item.SupportsPeople)
 3555            {
 03556                return;
 3557            }
 3558
 03559            if (people is not null)
 3560            {
 03561                people = people.Where(e => e is not null).ToArray();
 03562                _peopleRepository.UpdatePeople(item.Id, people);
 03563                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 3564            }
 03565        }
 3566
 3567        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 3568        {
 03569            foreach (var url in image.Path.Split('|'))
 3570            {
 3571                try
 3572                {
 03573                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 3574
 03575                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 3576
 03577                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 3578
 03579                    return item.GetImageInfo(image.Type, imageIndex);
 3580                }
 03581                catch (HttpRequestException ex)
 3582                {
 03583                    if (ex.StatusCode.HasValue
 03584                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 3585                    {
 03586                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 03587                        continue;
 3588                    }
 3589
 03590                    throw;
 3591                }
 3592            }
 3593
 03594            if (removeOnFailure)
 3595            {
 3596                // Remove this image to prevent it from retrying over and over
 03597                item.RemoveImage(image);
 03598                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 3599            }
 3600
 03601            throw new InvalidOperationException("Unable to convert any images to local");
 03602        }
 3603
 3604        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3605        {
 23606            if (string.IsNullOrWhiteSpace(name))
 3607            {
 03608                throw new ArgumentNullException(nameof(name));
 3609            }
 3610
 23611            name = _fileSystem.GetValidFilename(name.Trim());
 3612
 23613            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3614
 23615            var existingNameCount = 1; // first numbered name will be 2
 23616            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 23617            var originalName = name;
 33618            while (Directory.Exists(virtualFolderPath))
 3619            {
 13620                existingNameCount++;
 13621                name = originalName + existingNameCount;
 13622                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3623            }
 3624
 23625            var mediaPathInfos = options.PathInfos;
 23626            if (mediaPathInfos is not null)
 3627            {
 23628                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 23629                if (invalidpath is not null)
 3630                {
 03631                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3632                }
 3633            }
 3634
 23635            LibraryMonitor.Stop();
 3636
 3637            try
 3638            {
 23639                Directory.CreateDirectory(virtualFolderPath);
 3640
 23641                if (collectionType is not null)
 3642                {
 03643                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3644
 03645                    FileHelper.CreateEmpty(path);
 3646                }
 3647
 23648                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3649
 23650                if (mediaPathInfos is not null)
 3651                {
 43652                    foreach (var path in mediaPathInfos)
 3653                    {
 03654                        AddMediaPathInternal(name, path, false);
 3655                    }
 3656                }
 3657            }
 3658            finally
 3659            {
 23660                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3661
 23662                if (refreshLibrary)
 3663                {
 23664                    StartScanInBackground();
 3665                }
 3666                else
 3667                {
 3668                    // Need to add a delay here or directory watchers may still pick up the changes
 03669                    await Task.Delay(1000).ConfigureAwait(false);
 03670                    LibraryMonitor.Start();
 3671                }
 3672            }
 23673        }
 3674
 3675        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3676        {
 03677            foreach (var person in people)
 3678            {
 03679                cancellationToken.ThrowIfCancellationRequested();
 3680
 03681                var itemUpdateType = ItemUpdateType.MetadataDownload;
 03682                var saveEntity = false;
 03683                var createEntity = false;
 03684                var personEntity = GetPerson(person.Name);
 3685
 03686                if (personEntity is null)
 3687                {
 3688                    try
 3689                    {
 03690                        var path = Person.GetPath(person.Name);
 03691                        var info = Directory.CreateDirectory(path);
 03692                        personEntity = new Person()
 03693                        {
 03694                            Name = person.Name,
 03695                            Id = GetItemByNameId<Person>(path),
 03696                            DateCreated = info.CreationTimeUtc,
 03697                            DateModified = info.LastWriteTimeUtc,
 03698                            Path = path
 03699                        };
 3700
 03701                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 03702                        saveEntity = true;
 03703                        createEntity = true;
 03704                    }
 03705                    catch (Exception ex)
 3706                    {
 03707                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 03708                        continue;
 3709                    }
 3710                }
 3711
 03712                foreach (var id in person.ProviderIds)
 3713                {
 03714                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3715                    {
 03716                        personEntity.SetProviderId(id.Key, id.Value);
 03717                        saveEntity = true;
 3718                    }
 3719                }
 3720
 03721                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3722                {
 03723                    personEntity.SetImage(
 03724                        new ItemImageInfo
 03725                        {
 03726                            Path = person.ImageUrl,
 03727                            Type = ImageType.Primary
 03728                        },
 03729                        0);
 3730
 03731                    saveEntity = true;
 03732                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3733                }
 3734
 03735                if (saveEntity)
 3736                {
 03737                    if (createEntity)
 3738                    {
 03739                        CreateItems([personEntity], null, CancellationToken.None);
 3740                    }
 3741
 03742                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 03743                    personEntity.DateLastSaved = DateTime.UtcNow;
 3744
 03745                    CreateItems([personEntity], null, CancellationToken.None);
 3746                }
 03747            }
 03748        }
 3749
 3750        private void StartScanInBackground()
 3751        {
 33752            Task.Run(() =>
 33753            {
 33754                // No need to start if scanning the library because it will handle it
 33755                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33756            });
 33757        }
 3758
 3759        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3760        {
 13761            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03762        }
 3763
 3764        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3765        {
 13766            ArgumentNullException.ThrowIfNull(pathInfo);
 3767
 13768            var path = pathInfo.Path;
 3769
 13770            if (string.IsNullOrWhiteSpace(path))
 3771            {
 03772                throw new ArgumentException(nameof(path));
 3773            }
 3774
 13775            if (!Directory.Exists(path))
 3776            {
 13777                throw new FileNotFoundException("The path does not exist.");
 3778            }
 3779
 03780            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03781            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3782
 03783            CreateShortcut(virtualFolderPath, pathInfo);
 3784
 03785            if (saveLibraryOptions)
 3786            {
 03787                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3788
 03789                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3790
 03791                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3792
 03793                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3794            }
 03795        }
 3796
 3797        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3798        {
 03799            ArgumentNullException.ThrowIfNull(mediaPath);
 3800
 03801            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03802            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3803
 03804            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3805
 03806            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3807
 03808            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03809        }
 3810
 3811        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3812        {
 03813            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03814            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3815
 03816            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3817            {
 03818                var list = options.PathInfos.ToList();
 3819
 03820                foreach (var location in info.Locations)
 3821                {
 03822                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3823                    {
 03824                        list.Add(new MediaPathInfo(location));
 3825                    }
 3826                }
 3827
 03828                options.PathInfos = list.ToArray();
 3829            }
 03830        }
 3831
 3832        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3833        {
 23834            if (string.IsNullOrWhiteSpace(name))
 3835            {
 03836                throw new ArgumentNullException(nameof(name));
 3837            }
 3838
 23839            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3840
 23841            var path = Path.Combine(rootFolderPath, name);
 3842
 23843            if (!Directory.Exists(path))
 3844            {
 13845                throw new FileNotFoundException("The media folder does not exist");
 3846            }
 3847
 13848            LibraryMonitor.Stop();
 3849
 3850            try
 3851            {
 13852                Directory.Delete(path, true);
 3853            }
 3854            finally
 3855            {
 13856                CollectionFolder.OnCollectionFolderChange();
 3857
 13858                if (refreshLibrary)
 3859                {
 13860                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3861
 13862                    StartScanInBackground();
 3863                }
 3864                else
 3865                {
 3866                    // Need to add a delay here or directory watchers may still pick up the changes
 03867                    await Task.Delay(1000).ConfigureAwait(false);
 03868                    LibraryMonitor.Start();
 3869                }
 3870            }
 13871        }
 3872
 3873        private void RemoveContentTypeOverrides(string path)
 3874        {
 03875            if (string.IsNullOrWhiteSpace(path))
 3876            {
 03877                throw new ArgumentNullException(nameof(path));
 3878            }
 3879
 03880            List<NameValuePair>? removeList = null;
 3881
 03882            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3883            {
 03884                if (string.IsNullOrWhiteSpace(contentType.Name)
 03885                    || _fileSystem.AreEqual(path, contentType.Name)
 03886                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3887                {
 03888                    (removeList ??= new()).Add(contentType);
 3889                }
 3890            }
 3891
 03892            if (removeList is not null)
 3893            {
 03894                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03895                    .Except(removeList)
 03896                    .ToArray();
 3897
 03898                _configurationManager.SaveConfiguration();
 3899            }
 03900        }
 3901
 3902        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3903        {
 13904            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3905
 13906            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13907            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3908
 13909            if (!Directory.Exists(virtualFolderPath))
 3910            {
 13911                throw new FileNotFoundException(
 13912                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3913            }
 3914
 03915            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03916                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03917                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3918
 03919            if (!string.IsNullOrEmpty(shortcut))
 3920            {
 03921                _fileSystem.DeleteFile(shortcut);
 3922            }
 3923
 03924            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3925
 03926            libraryOptions.PathInfos = libraryOptions
 03927                .PathInfos
 03928                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03929                .ToArray();
 3930
 03931            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03932        }
 3933
 3934        private static bool ItemIsVisible(BaseItem? item, User? user)
 3935        {
 223936            if (item is null)
 3937            {
 223938                return false;
 3939            }
 3940
 03941            if (user is null)
 3942            {
 03943                return true;
 3944            }
 3945
 03946            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3947        }
 3948
 3949        public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
 3950        {
 03951            var path = pathInfo.Path;
 03952            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3953
 03954            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3955
 03956            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3957
 03958            while (File.Exists(lnk))
 3959            {
 03960                shortcutFilename += "1";
 03961                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3962            }
 3963
 03964            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 03965            RemoveContentTypeOverrides(path);
 03966        }
 3967
 3968        /// <inheritdoc />
 3969        public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
 3970        {
 03971            var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
 3972
 3973            // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
 03974            foreach (var parentId in affectedParentIds)
 3975            {
 03976                if (GetItemById(parentId) is Folder parent)
 3977                {
 03978                    foreach (var lc in parent.LinkedChildren)
 3979                    {
 03980                        if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
 3981                        {
 03982                            lc.ItemId = toChildId;
 3983                        }
 3984                    }
 3985
 03986                    await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
 3987                }
 3988            }
 03989        }
 3990
 3991        /// <inheritdoc />
 3992        public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
 3993        {
 03994            if (query.User is not null)
 3995            {
 03996                AddUserToQuery(query, query.User);
 3997            }
 3998
 03999            SetTopParentOrAncestorIds(query);
 04000            return _itemRepository.GetQueryFiltersLegacy(query);
 4001        }
 4002
 4003        /// <inheritdoc />
 4004        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
 4005        {
 04006            return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
 4007        }
 4008
 4009        /// <inheritdoc />
 4010        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType, InternalItemsQuery query)
 4011        {
 04012            if (query.User is not null)
 4013            {
 04014                AddUserToQuery(query, query.User);
 4015            }
 4016
 04017            SetTopParentOrAncestorIds(query);
 04018            return _itemRepository.GetMediaStreamLanguages(query, mediaStreamType);
 4019        }
 4020
 4021        private sealed record ExtraCandidate(BaseItem Extra, ExtraType ExtraType, ExtraRule ExtraRule, bool IsInMixedFol
 4022    }
 4023}

Methods/Properties

.ctor(MediaBrowser.Controller.IServerApplicationHost,Microsoft.Extensions.Logging.ILoggerFactory,MediaBrowser.Model.Tasks.ITaskManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Library.IUserDataManager,System.Lazy`1<MediaBrowser.Controller.Library.ILibraryMonitor>,MediaBrowser.Model.IO.IFileSystem,System.Lazy`1<MediaBrowser.Controller.Providers.IProviderManager>,System.Lazy`1<MediaBrowser.Controller.Library.IUserViewManager>,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,MediaBrowser.Controller.Persistence.IItemRepository,MediaBrowser.Controller.Persistence.IItemPersistenceService,MediaBrowser.Controller.Persistence.INextUpService,MediaBrowser.Controller.Persistence.IItemCountService,MediaBrowser.Controller.Persistence.ILinkedChildrenService,MediaBrowser.Controller.Drawing.IImageProcessor,Emby.Naming.Common.NamingOptions,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Persistence.IPeopleRepository,MediaBrowser.Controller.IO.IPathManager,Emby.Server.Implementations.Library.DotIgnoreIgnoreRule,MediaBrowser.Model.Globalization.ILocalizationManager,MediaBrowser.Controller.Persistence.IMediaStreamRepository,System.Lazy`1<MediaBrowser.Controller.IO.IExternalDataManager>)
get_RootFolder()
get_LibraryMonitor()
get_ProviderManager()
get_UserViewManager()
AddParts(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Resolvers.IResolverIgnoreRule>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Resolvers.IItemResolver>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Library.IIntroProvider>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Sorting.IBaseItemComparer>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Library.ILibraryPostScanTask>)
RecordConfigurationValues(MediaBrowser.Model.Configuration.ServerConfiguration)
ConfigurationUpdated(System.Object,System.EventArgs)
RegisterItem(MediaBrowser.Controller.Entities.BaseItem)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions,System.Boolean)
DeleteItemsUnsafeFast(System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Controller.Entities.BaseItem>,System.Boolean)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
DeleteItemPath(MediaBrowser.Controller.Entities.BaseItem,System.Boolean,MediaBrowser.Model.IO.FileSystemMetadata)
IsInternalItem(MediaBrowser.Controller.Entities.BaseItem)
GetMetadataPaths(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>)
GetInternalMetadataPaths(MediaBrowser.Controller.Entities.BaseItem)
ResolveItem(MediaBrowser.Controller.Library.ItemResolveArgs,MediaBrowser.Controller.Resolvers.IItemResolver[])
Resolve(MediaBrowser.Controller.Library.ItemResolveArgs,MediaBrowser.Controller.Resolvers.IItemResolver)
GetNewItemId(System.String,System.Type)
GetNewItemIdInternal(System.String,System.Type,System.Boolean)
ResolvePath(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Controller.Entities.Folder,MediaBrowser.Controller.Providers.IDirectoryService,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
SetAdditionalPartsFromStack(MediaBrowser.Controller.Entities.Video,System.String)
ResolveAlternateVersion(System.String,System.Type,MediaBrowser.Controller.Entities.Folder,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
ResolvePath(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Resolvers.IItemResolver[],MediaBrowser.Controller.Entities.Folder,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,MediaBrowser.Model.Configuration.LibraryOptions)
IgnoreFile(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Controller.Entities.BaseItem)
NormalizeRootPathList(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>)
ResolvePaths(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Entities.Folder,MediaBrowser.Model.Configuration.LibraryOptions,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
ResolvePaths(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Entities.Folder,MediaBrowser.Model.Configuration.LibraryOptions,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,MediaBrowser.Controller.Resolvers.IItemResolver[])
ResolveFileList()
CreateRootFolder()
GetUserRootFolder()
FindByPath(System.String,System.Nullable`1<System.Boolean>)
GetPerson(System.String)
GetStudio(System.String)
GetStudioId(System.String)
GetGenreId(System.String)
GetMusicGenreId(System.String)
GetGenre(System.String)
GetMusicGenre(System.String)
GetYear(System.Int32)
GetArtist(System.String)
GetArtists(System.Collections.Generic.IReadOnlyList`1<System.String>)
GetArtist(System.String,MediaBrowser.Controller.Dto.DtoOptions)
CreateItemByName(System.Func`2<System.String,System.String>,System.String,MediaBrowser.Controller.Dto.DtoOptions)
GetItemByNameId(System.String)
ValidatePeopleAsync(System.IProgress`1<System.Double>,System.Threading.CancellationToken)
ValidateMediaLibrary(System.IProgress`1<System.Double>,System.Threading.CancellationToken)
ValidateMediaLibraryInternal()
ValidateTopLibraryFolders()
ClearIgnoreRuleCache()
PerformLibraryValidation()
RunPostScanTasks()
GetVirtualFolders()
GetVirtualFolders(System.Boolean)
GetVirtualFolderInfo(System.String,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.HashSet`1<System.Guid>)
GetCollectionType(System.String)
GetItemById(System.Guid)
GetItemById(System.Guid)
GetItemById(System.Guid,System.Guid)
GetItemById(System.Guid,Jellyfin.Database.Implementations.Entities.User)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Boolean)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCount(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemCounts(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemCountsForNameItem(Jellyfin.Data.Enums.BaseItemKind,System.Guid,Jellyfin.Data.Enums.BaseItemKind[],Jellyfin.Database.Implementations.Entities.User)
GetChildCountBatch(System.Collections.Generic.IReadOnlyList`1<System.Guid>,System.Nullable`1<System.Guid>)
GetPlayedAndTotalCountBatch(System.Collections.Generic.IReadOnlyList`1<System.Guid>,Jellyfin.Database.Implementations.Entities.User)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>)
GetLatestItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Data.Enums.CollectionType)
GetNextUpSeriesKeys(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Controller.Entities.BaseItem>,System.DateTime)
GetNextUpEpisodesBatch(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Boolean,System.Boolean)
QueryItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemIds(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetStudios(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetMusicGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAllArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
SetTopParentOrAncestorIds(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAlbumArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemsResult(MediaBrowser.Controller.Entities.InternalItemsQuery)
SetTopParentIdsOrAncestors(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Controller.Entities.BaseItem>)
AddUserToQuery(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.Entities.User,System.Boolean)
ConfigureUserAccess(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.Entities.User)
GetTopParentIdsForQuery(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
GetIntros()
GetIntros()
ResolveIntro(MediaBrowser.Controller.Library.IntroInfo)
GetLocalAlternateVersionIds(MediaBrowser.Controller.Entities.Video)
GetLinkedAlternateVersions(MediaBrowser.Controller.Entities.Video)
UpsertLinkedChild(System.Guid,System.Guid,MediaBrowser.Controller.Entities.LinkedChildType)
Sort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<Jellyfin.Data.Enums.ItemSortBy>,Jellyfin.Database.Implementations.Enums.SortOrder)
Sort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Enums.SortOrder>>)
GetComparer(Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Entities.User)
CreateItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
CreateItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.BaseItem,System.Threading.CancellationToken)
ImageNeedsRefresh(MediaBrowser.Controller.Entities.ItemImageInfo)
UpdateImagesAsync()
UpdateItemsAsync()
UpdateItemAsync(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.ItemUpdateType,System.Threading.CancellationToken)
ForgetDroppedLocalAlternateVersions(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
ReattachUserDataAsync()
RunMetadataSavers()
ReportItemRemoved(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
RetrieveItem(System.Guid)
GetCollectionFolders(MediaBrowser.Controller.Entities.BaseItem)
GetCollectionFolders(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.Folder>)
GetCollectionFoldersInternal(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.Folder>)
GetLibraryOptions(MediaBrowser.Controller.Entities.BaseItem)
GetContentType(MediaBrowser.Controller.Entities.BaseItem)
GetInheritedContentType(MediaBrowser.Controller.Entities.BaseItem)
GetConfiguredContentType(MediaBrowser.Controller.Entities.BaseItem)
GetConfiguredContentType(System.String)
GetConfiguredContentType(MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
GetContentTypeOverride(System.String,System.Boolean)
GetTopFolderContentType(MediaBrowser.Controller.Entities.BaseItem)
GetNamedView(Jellyfin.Database.Implementations.Entities.User,System.String,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetNamedView(System.String,Jellyfin.Data.Enums.CollectionType,System.String)
GetNamedView(Jellyfin.Database.Implementations.Entities.User,System.String,System.Guid,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetShadowView(MediaBrowser.Controller.Entities.BaseItem,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetNamedView(System.String,System.Guid,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String,System.String)
GetParentItem(System.Nullable`1<System.Guid>,System.Nullable`1<System.Guid>)
QueueLibraryScan()
GetSeasonNumberFromPath(System.String,System.Nullable`1<System.Guid>)
FillMissingEpisodeNumbersFromPath(MediaBrowser.Controller.Entities.TV.Episode,System.Boolean)
ParseName(System.String)
FindExtras(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService)
GetExtraName(Emby.Server.Implementations.Library.LibraryManager/ExtraCandidate,Emby.Naming.Video.VideoFileInfo,System.Collections.Generic.Dictionary`2<MediaBrowser.Model.Entities.ExtraType,System.Int32>)
GetExtraTypeNameKey(MediaBrowser.Model.Entities.ExtraType)
GetPathAfterNetworkSubstitution(System.String,MediaBrowser.Controller.Entities.BaseItem)
GetPeople(MediaBrowser.Controller.Entities.InternalPeopleQuery)
GetPeople(MediaBrowser.Controller.Entities.BaseItem)
GetPeopleItems(MediaBrowser.Controller.Entities.InternalPeopleQuery)
GetPeopleNames(MediaBrowser.Controller.Entities.InternalPeopleQuery)
GetPeopleNamesByItems(System.Collections.Generic.IReadOnlyList`1<System.Guid>,System.Collections.Generic.IReadOnlyList`1<System.String>)
GetPeopleByItems(System.Collections.Generic.IReadOnlyList`1<System.Guid>)
UpdatePeople(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.PersonInfo>)
UpdatePeopleAsync()
ConvertImageToLocal()
AddVirtualFolder()
SavePeopleMetadataAsync()
StartScanInBackground()
AddMediaPath(System.String,MediaBrowser.Model.Configuration.MediaPathInfo)
AddMediaPathInternal(System.String,MediaBrowser.Model.Configuration.MediaPathInfo,System.Boolean)
UpdateMediaPath(System.String,MediaBrowser.Model.Configuration.MediaPathInfo)
SyncLibraryOptionsToLocations(System.String,MediaBrowser.Model.Configuration.LibraryOptions)
RemoveVirtualFolder()
RemoveContentTypeOverrides(System.String)
RemoveMediaPath(System.String,System.String)
ItemIsVisible(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
CreateShortcut(System.String,MediaBrowser.Model.Configuration.MediaPathInfo)
RerouteLinkedChildReferencesAsync()
GetQueryFiltersLegacy(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetMediaStreamLanguages(MediaBrowser.Model.Entities.MediaStreamType)
GetMediaStreamLanguages(MediaBrowser.Model.Entities.MediaStreamType,MediaBrowser.Controller.Entities.InternalItemsQuery)