< 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: 630
Uncovered lines: 1088
Coverable lines: 1718
Total lines: 4042
Line coverage: 36.6%
Branch coverage
29%
Covered branches: 268
Total branches: 922
Branch coverage: 29%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/3/2026 - 12:15:13 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: 40238/11/2026 - 12:17:00 AM Line coverage: 36.6% (630/1718) Branch coverage: 29% (268/922) Total lines: 4042 5/3/2026 - 12:15:13 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: 40238/11/2026 - 12:17:00 AM Line coverage: 36.6% (630/1718) Branch coverage: 29% (268/922) Total lines: 4042

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(...)5.55%281186.66%
DeleteItem(...)0%4970700%
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%2281.81%
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%
GetItemIdsWithAlternateVersions(...)100%210%
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            {
 193225                if (_rootFolder is null)
 22226                {
 227                    lock (_rootFolderSyncLock)
 228                    {
 22229                        _rootFolder ??= CreateRootFolder();
 22230                    }
 231                }
 232
 193233                return _rootFolder;
 234            }
 235        }
 236
 43237        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 238
 118239        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        {
 194330            ArgumentNullException.ThrowIfNull(item);
 331
 194332            if (item is IItemByName)
 333            {
 0334                if (item is not MusicArtist)
 335                {
 0336                    return;
 337                }
 338            }
 194339            else if (!item.IsFolder)
 340            {
 0341                if (item is not Video && item is not LiveTvChannel)
 342                {
 0343                    return;
 344                }
 345            }
 346
 194347            _cache.AddOrUpdate(item.Id, item);
 194348        }
 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)]);
 415
 416            // Evict the deleted items from the cache and announce each removal.
 0417            foreach (var (item, _, _) in pathMaps)
 418            {
 0419                _cache.TryRemove(item.Id, out _);
 0420                ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
 421            }
 0422        }
 423
 424        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 425        {
 0426            ArgumentNullException.ThrowIfNull(item);
 427
 0428            if (item.SourceType == SourceType.Channel)
 429            {
 0430                if (options.DeleteFromExternalProvider)
 431                {
 432                    try
 433                    {
 0434                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0435                    }
 0436                    catch (ArgumentException)
 437                    {
 438                        // channel no longer installed
 0439                    }
 440                }
 441
 0442                options.DeleteFileLocation = false;
 443            }
 444
 0445            if (item is LiveTvProgram)
 446            {
 0447                _logger.LogDebug(
 0448                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0449                    item.GetType().Name,
 0450                    item.Name ?? "Unknown name",
 0451                    item.Path ?? string.Empty,
 0452                    item.Id);
 453            }
 454            else
 455            {
 0456                _logger.LogInformation(
 0457                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0458                    item.GetType().Name,
 0459                    item.Name ?? "Unknown name",
 0460                    item.Path ?? string.Empty,
 0461                    item.Id);
 462            }
 463
 464            // If deleting a primary version video, clear PrimaryVersionId from alternate versions
 465            // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
 0466            if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
 467            {
 0468                var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
 0469                var allAlternateVersions = localAlternateIds
 0470                    .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
 0471                    .Distinct()
 0472                    .Select(id => GetItemById(id))
 0473                    .OfType<Video>()
 0474                    .ToList();
 475
 476                // Partition alternates by whether their files still exist on disk
 0477                var alternateVersions = new List<Video>();
 0478                var missingAlternates = new List<Video>();
 0479                foreach (var alt in allAlternateVersions)
 480                {
 0481                    if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
 482                    {
 0483                        missingAlternates.Add(alt);
 484                    }
 485                    else
 486                    {
 0487                        alternateVersions.Add(alt);
 488                    }
 489                }
 490
 491                // Delete alternates whose files no longer exist to avoid ghost items.
 492                // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
 0493                foreach (var missing in missingAlternates)
 494                {
 0495                    _logger.LogInformation(
 0496                        "Deleting missing alternate version {Name} ({Path})",
 0497                        missing.Name ?? "Unknown name",
 0498                        missing.Path ?? string.Empty);
 0499                    missing.SetPrimaryVersionId(null);
 0500                    missing.OwnerId = Guid.Empty;
 0501                    missing.LocalAlternateVersions = [];
 0502                    missing.LinkedAlternateVersions = [];
 0503                    DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
 504                }
 505
 0506                if (alternateVersions.Count > 0)
 507                {
 0508                    _logger.LogInformation(
 0509                        "Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
 0510                        alternateVersions.Count,
 0511                        item.Name ?? "Unknown name");
 512
 513                    // Promote the first alternate version to be the new primary
 0514                    var newPrimary = alternateVersions[0];
 0515                    newPrimary.SetPrimaryVersionId(null);
 0516                    newPrimary.OwnerId = Guid.Empty;
 517
 518                    // Transfer alternate version arrays from old primary to new primary
 519                    // so UpdateToRepositoryAsync creates correct LinkedChildren entries
 0520                    newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
 0521                        .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
 0522                        .ToArray();
 0523                    newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
 0524                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
 0525                        .ToArray();
 526
 0527                    newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter()
 528
 529                    // Re-route playlist/collection references from deleted primary to new primary
 0530                    RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
 531
 532                    // Update remaining alternates to point to new primary
 0533                    foreach (var alternate in alternateVersions.Skip(1))
 534                    {
 0535                        alternate.SetPrimaryVersionId(newPrimary.Id);
 536                        // Only set OwnerId for local alternates; linked alternates are independent items
 0537                        alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
 0538                        alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaite
 539                    }
 540                }
 541            }
 0542            else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
 543            {
 544                // If deleting an alternate version, re-route references to its primary
 0545                RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter()
 546
 547                // Remove deleted alternate from primary's LinkedAlternateVersions
 0548                if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
 549                {
 0550                    primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
 0551                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
 0552                        .ToArray();
 0553                    primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter
 554                }
 555            }
 556
 0557            var children = item.IsFolder
 0558                ? ((Folder)item).GetRecursiveChildren(false)
 0559                : [];
 560
 0561            foreach (var metadataPath in GetMetadataPaths(item, children))
 562            {
 0563                if (!Directory.Exists(metadataPath))
 564                {
 565                    continue;
 566                }
 567
 0568                _logger.LogDebug(
 0569                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0570                    item.GetType().Name,
 0571                    item.Name ?? "Unknown name",
 0572                    metadataPath,
 0573                    item.Id);
 574
 575                try
 576                {
 0577                    Directory.Delete(metadataPath, true);
 0578                }
 0579                catch (Exception ex)
 580                {
 0581                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0582                }
 583            }
 584
 0585            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 586            {
 587                // Assume only the first is required
 588                // Add this flag to GetDeletePaths if required in the future
 0589                var isRequiredForDelete = true;
 590
 0591                foreach (var fileSystemInfo in item.GetDeletePaths())
 592                {
 0593                    DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
 594
 0595                    isRequiredForDelete = false;
 596                }
 597            }
 598
 0599            item.SetParent(null);
 600
 0601            var externalDataManager = _externalDataManagerFactory.Value;
 0602            externalDataManager.DeleteExternalItemFiles(item);
 0603            foreach (var child in children)
 604            {
 0605                externalDataManager.DeleteExternalItemFiles(child);
 606            }
 607
 0608            _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
 0609            _cache.TryRemove(item.Id, out _);
 0610            foreach (var child in children)
 611            {
 0612                _cache.TryRemove(child.Id, out _);
 613            }
 614
 0615            if (parent is Folder folder)
 616            {
 0617                folder.Children = null;
 0618                folder.UserData = null;
 619            }
 620
 621            // Announce the descendants before the item itself.
 0622            foreach (var child in children)
 623            {
 0624                ReportItemRemoved(child, item);
 625            }
 626
 0627            ReportItemRemoved(item, parent);
 0628        }
 629
 630        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 631        {
 0632            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 633            {
 634                try
 635                {
 0636                    _logger.LogInformation(
 0637                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0638                        item.GetType().Name,
 0639                        item.Name ?? "Unknown name",
 0640                        fileSystemInfo.FullName,
 0641                        item.Id);
 642
 0643                    if (fileSystemInfo.IsDirectory)
 644                    {
 0645                        Directory.Delete(fileSystemInfo.FullName, true);
 646                    }
 647                    else
 648                    {
 0649                        File.Delete(fileSystemInfo.FullName);
 650                    }
 0651                }
 0652                catch (DirectoryNotFoundException)
 653                {
 0654                    _logger.LogInformation(
 0655                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0656                        item.GetType().Name,
 0657                        item.Name ?? "Unknown name",
 0658                        fileSystemInfo.FullName,
 0659                        item.Id);
 0660                }
 0661                catch (FileNotFoundException)
 662                {
 0663                    _logger.LogInformation(
 0664                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0665                        item.GetType().Name,
 0666                        item.Name ?? "Unknown name",
 0667                        fileSystemInfo.FullName,
 0668                        item.Id);
 0669                }
 0670                catch (IOException)
 671                {
 0672                    if (isRequiredForDelete)
 673                    {
 0674                        throw;
 675                    }
 0676                }
 0677                catch (UnauthorizedAccessException)
 678                {
 0679                    if (isRequiredForDelete)
 680                    {
 0681                        throw;
 682                    }
 0683                }
 684            }
 0685        }
 686
 687        private bool IsInternalItem(BaseItem item)
 688        {
 0689            if (!item.IsFileProtocol)
 690            {
 0691                return false;
 692            }
 693
 0694            var pathToCheck = item switch
 0695            {
 0696                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0697                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0698                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0699                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0700                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0701                Year => _configurationManager.ApplicationPaths.YearPath,
 0702                _ => null
 0703            };
 704
 0705            var itemPath = item.Path;
 0706            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 707            {
 0708                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0709                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 710
 0711                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 712            }
 713
 0714            return false;
 715        }
 716
 717        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 718        {
 0719            var list = GetInternalMetadataPaths(item);
 0720            foreach (var child in children)
 721            {
 0722                list.AddRange(GetInternalMetadataPaths(child));
 723            }
 724
 0725            return list;
 726        }
 727
 728        private List<string> GetInternalMetadataPaths(BaseItem item)
 729        {
 0730            var list = new List<string>
 0731            {
 0732                item.GetInternalMetadataPath()
 0733            };
 734
 0735            if (item is Video video)
 736            {
 737                // Trickplay
 0738                list.Add(_pathManager.GetTrickplayDirectory(video));
 739
 740                // Chapter Images
 0741                list.Add(_pathManager.GetChapterImageFolderPath(video));
 742
 743                // Subtitles and attachments
 0744                foreach (var mediaSource in item.GetMediaSources(false))
 745                {
 0746                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0747                    if (subtitleFolder is not null)
 748                    {
 0749                        list.Add(subtitleFolder);
 750                    }
 751
 0752                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0753                    if (attachmentFolder is not null)
 754                    {
 0755                        list.Add(attachmentFolder);
 756                    }
 757                }
 758            }
 759
 0760            return list;
 761        }
 762
 763        /// <summary>
 764        /// Resolves the item.
 765        /// </summary>
 766        /// <param name="args">The args.</param>
 767        /// <param name="resolvers">The resolvers.</param>
 768        /// <returns>BaseItem.</returns>
 769        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 770        {
 98771            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 98772                .FirstOrDefault(i => i is not null);
 773
 98774            if (item is not null)
 775            {
 87776                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 777            }
 778
 98779            return item;
 780        }
 781
 782        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 783        {
 784            try
 785            {
 343786                return resolver.ResolvePath(args);
 787            }
 0788            catch (Exception ex)
 789            {
 0790                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0791                return null;
 792            }
 343793        }
 794
 795        public Guid GetNewItemId(string key, Type type)
 796        {
 153797            return GetNewItemIdInternal(key, type, false);
 798        }
 799
 800        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 801        {
 154802            ArgumentException.ThrowIfNullOrEmpty(key);
 154803            ArgumentNullException.ThrowIfNull(type);
 804
 154805            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 154806            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 807            {
 808                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 118809                key = key.Substring(programDataPath.Length)
 118810                    .TrimStart('/', '\\')
 118811                    .Replace('/', '\\');
 812            }
 813
 154814            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 815            {
 1816                key = key.ToLowerInvariant();
 817            }
 818
 154819            key = type.FullName + key;
 820
 154821            return key.GetMD5();
 822        }
 823
 824        public BaseItem? ResolvePath(
 825            FileSystemMetadata fileInfo,
 826            Folder? parent = null,
 827            IDirectoryService? directoryService = null,
 828            CollectionType? collectionType = null)
 44829            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType
 830
 831        private void SetAdditionalPartsFromStack(Video altVideo, string path)
 832        {
 0833            if (altVideo.AdditionalParts is { Length: > 0 })
 834            {
 0835                return;
 836            }
 837
 0838            var directory = Path.GetDirectoryName(path);
 0839            if (string.IsNullOrEmpty(directory))
 840            {
 0841                return;
 842            }
 843
 844            IEnumerable<FileSystemMetadata> siblings;
 845            try
 846            {
 0847                siblings = _fileSystem.GetFiles(directory);
 0848            }
 0849            catch (Exception ex)
 850            {
 0851                _logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
 0852                return;
 853            }
 854
 0855            var stacks = StackResolver.Resolve(siblings, _namingOptions);
 0856            foreach (var stack in stacks)
 857            {
 0858                if (stack.Files.Count > 1
 0859                    && string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
 860                {
 0861                    altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
 0862                    return;
 863                }
 864            }
 0865        }
 866
 867        /// <inheritdoc />
 868        public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? colle
 869        {
 870            // Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
 871            // This happens when items were previously resolved without proper type context
 872            // in mixed-content libraries where collectionType is null.
 0873            var expectedId = GetNewItemId(path, expectedVideoType);
 0874            if (expectedVideoType != typeof(Video))
 875            {
 0876                var wrongTypeId = GetNewItemId(path, typeof(Video));
 0877                if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
 878                {
 0879                    _logger.LogInformation(
 0880                        "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
 0881                        wrongTypeItem.GetType().Name,
 0882                        expectedVideoType.Name,
 0883                        path);
 0884                    DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
 885                }
 886            }
 887
 0888            var resolved = ResolvePath(
 0889                _fileSystem.GetFileSystemInfo(path),
 0890                parent,
 0891                collectionType: collectionType) as Video;
 892
 0893            if (resolved is null)
 894            {
 0895                return null;
 896            }
 897
 898            // Ensure the alternate version has the same concrete type as the primary video.
 899            // ResolvePath may return a generic Video for files in mixed-content libraries
 900            // where collectionType is null, even though the primary is a Movie/Episode/etc.
 0901            if (resolved.GetType() != expectedVideoType)
 902            {
 0903                if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
 904                {
 0905                    correctVideo.Path = resolved.Path;
 0906                    correctVideo.Name = resolved.Name;
 0907                    correctVideo.VideoType = resolved.VideoType;
 0908                    correctVideo.ProductionYear = resolved.ProductionYear;
 0909                    correctVideo.ExtraType = resolved.ExtraType;
 0910                    resolved = correctVideo;
 911                }
 912            }
 913
 0914            resolved.Id = expectedId;
 0915            return resolved;
 916        }
 917
 918        private BaseItem? ResolvePath(
 919            FileSystemMetadata fileInfo,
 920            IDirectoryService directoryService,
 921            IItemResolver[]? resolvers,
 922            Folder? parent = null,
 923            CollectionType? collectionType = null,
 924            LibraryOptions? libraryOptions = null)
 925        {
 98926            ArgumentNullException.ThrowIfNull(fileInfo);
 927
 98928            var fullPath = fileInfo.FullName;
 929
 98930            if (collectionType is null && parent is not null)
 931            {
 18932                collectionType = GetContentTypeOverride(fullPath, true);
 933            }
 934
 98935            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 98936            {
 98937                Parent = parent,
 98938                FileInfo = fileInfo,
 98939                CollectionType = collectionType,
 98940                LibraryOptions = libraryOptions
 98941            };
 942
 943            // Return null if ignore rules deem that we should do so
 98944            if (IgnoreFile(args.FileInfo, args.Parent))
 945            {
 0946                return null;
 947            }
 948
 949            // Gather child folder and files
 98950            if (args.IsDirectory)
 951            {
 51952                var isPhysicalRoot = args.IsPhysicalRoot;
 953
 954                // When resolving the root, we need it's grandchildren (children of user views)
 51955                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 956
 957                FileSystemMetadata[] files;
 51958                var isVf = args.IsVf;
 959
 960                try
 961                {
 51962                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 51963                }
 0964                catch (Exception ex)
 965                {
 0966                    if (parent is not null && parent.IsPhysicalRoot)
 967                    {
 0968                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 969
 0970                        files = [];
 971                    }
 972                    else
 973                    {
 0974                        throw;
 975                    }
 0976                }
 977
 978                // Need to remove sub-paths that may have been resolved from shortcuts
 979                // Example: if \\server\movies exists, then strip out \\server\movies\action
 51980                if (isPhysicalRoot)
 981                {
 22982                    files = NormalizeRootPathList(files).ToArray();
 983                }
 984
 51985                args.FileSystemChildren = files;
 986            }
 987
 988            // Filter content based on ignore rules
 98989            if (args.IsDirectory)
 990            {
 51991                var filtered = args.GetActualFileSystemChildren().ToArray();
 51992                args.FileSystemChildren = filtered ?? [];
 993            }
 994
 98995            return ResolveItem(args, resolvers);
 996        }
 997
 998        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 123999            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 1000
 1001        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 1002        {
 851003            var originalList = paths.ToList();
 1004
 851005            var list = originalList.Where(i => i.IsDirectory)
 851006                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 851007                .Distinct()
 851008                .ToList();
 1009
 851010            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 851011                .ToList();
 1012
 1701013            foreach (var dupe in dupes)
 1014            {
 01015                _logger.LogInformation("Found duplicate path: {0}", dupe);
 1016            }
 1017
 851018            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 851019            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 851020            return newList;
 1021        }
 1022
 1023        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 1024        {
 621025            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 1026        }
 1027
 1028        public IEnumerable<BaseItem> ResolvePaths(
 1029            IEnumerable<FileSystemMetadata> files,
 1030            IDirectoryService directoryService,
 1031            Folder parent,
 1032            LibraryOptions libraryOptions,
 1033            CollectionType? collectionType,
 1034            IItemResolver[] resolvers)
 1035        {
 621036            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 1037
 621038            if (parent is not null)
 1039            {
 621040                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 1041
 3721042                foreach (var resolver in multiItemResolvers)
 1043                {
 1241044                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 1045
 1241046                    if (result?.Items.Count > 0)
 1047                    {
 01048                        var items = result.Items;
 01049                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 01050                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 01051                        return items;
 1052                    }
 1053                }
 1054            }
 1055
 621056            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 01057        }
 1058
 1059        private IEnumerable<BaseItem> ResolveFileList(
 1060            IReadOnlyList<FileSystemMetadata> fileList,
 1061            IDirectoryService directoryService,
 1062            Folder? parent,
 1063            CollectionType? collectionType,
 1064            IItemResolver[]? resolvers,
 1065            LibraryOptions libraryOptions)
 1066        {
 1067            // Given that fileList is a list we can save enumerator allocations by indexing
 1601068            for (var i = 0; i < fileList.Count; i++)
 1069            {
 181070                var file = fileList[i];
 181071                BaseItem? result = null;
 1072                try
 1073                {
 181074                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 181075                }
 01076                catch (Exception ex)
 1077                {
 01078                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 01079                }
 1080
 181081                if (result is not null)
 1082                {
 71083                    yield return result;
 1084                }
 1085            }
 621086        }
 1087
 1088        /// <summary>
 1089        /// Creates the root media folder.
 1090        /// </summary>
 1091        /// <returns>AggregateFolder.</returns>
 1092        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 1093        public AggregateFolder CreateRootFolder()
 1094        {
 221095            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 1096
 221097            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 221098                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 221099                             .DeepCopy<Folder, AggregateFolder>();
 1100
 1101            // In case program data folder was moved
 221102            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 1103            {
 01104                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 01105                rootFolder.Path = rootFolderPath;
 1106            }
 1107
 1108            // Add in the plug-in folders
 221109            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 1110
 221111            var info = Directory.CreateDirectory(path);
 221112            Folder folder = new PlaylistsFolder
 221113            {
 221114                Path = path,
 221115                DateCreated = info.CreationTimeUtc,
 221116                DateModified = info.LastWriteTimeUtc,
 221117            };
 1118
 221119            if (folder.Id.IsEmpty())
 1120            {
 221121                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 1122            }
 1123
 221124            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 1125
 221126            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 1127            {
 01128                folder = dbItem;
 1129            }
 1130
 221131            if (!folder.ParentId.Equals(rootFolder.Id))
 1132            {
 221133                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 221134                folder.ParentId = rootFolder.Id;
 221135                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 1136            }
 1137
 221138            rootFolder.AddVirtualChild(folder);
 1139
 221140            RegisterItem(folder);
 1141
 221142            return rootFolder;
 1143        }
 1144
 1145        public Folder GetUserRootFolder()
 1146        {
 9311147            if (_userRootFolder is null)
 221148            {
 1149                lock (_userRootFolderSyncLock)
 1150                {
 221151                    if (_userRootFolder is null)
 1152                    {
 221153                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 1154
 221155                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 221156                        Directory.CreateDirectory(userRootPath);
 1157
 221158                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 221159                        UserRootFolder? tmpItem = null;
 1160                        try
 1161                        {
 221162                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 221163                        }
 01164                        catch (Exception ex)
 1165                        {
 01166                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 01167                        }
 1168
 221169                        if (tmpItem is null)
 1170                        {
 221171                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 221172                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 221173                                        .DeepCopy<Folder, UserRootFolder>();
 1174                        }
 1175
 1176                        // In case program data folder was moved
 221177                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 1178                        {
 01179                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 01180                            tmpItem.Path = userRootPath;
 1181                        }
 1182
 221183                        _userRootFolder = tmpItem;
 221184                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 1185                    }
 221186                }
 1187            }
 1188
 9311189            return _userRootFolder;
 1190        }
 1191
 1192        /// <inheritdoc />
 1193        public BaseItem? FindByPath(string path, bool? isFolder)
 1194        {
 1195            // If this returns multiple items it could be tricky figuring out which one is correct.
 1196            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 01197            ArgumentException.ThrowIfNullOrEmpty(path);
 1198
 01199            var query = new InternalItemsQuery
 01200            {
 01201                Path = path,
 01202                IsFolder = isFolder,
 01203                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 01204                Limit = 1,
 01205                DtoOptions = new DtoOptions(true)
 01206            };
 1207
 01208            return GetItemList(query)
 01209                .FirstOrDefault();
 1210        }
 1211
 1212        /// <inheritdoc />
 1213        public Person? GetPerson(string name)
 1214        {
 11215            var path = Person.GetPath(name);
 11216            var id = GetItemByNameId<Person>(path);
 11217            if (GetItemById(id) is Person item)
 1218            {
 01219                return item;
 1220            }
 1221
 11222            return null;
 1223        }
 1224
 1225        /// <summary>
 1226        /// Gets the studio.
 1227        /// </summary>
 1228        /// <param name="name">The name.</param>
 1229        /// <returns>Task{Studio}.</returns>
 1230        public Studio GetStudio(string name)
 1231        {
 01232            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 1233        }
 1234
 1235        public Guid GetStudioId(string name)
 1236        {
 01237            return GetItemByNameId<Studio>(Studio.GetPath(name));
 1238        }
 1239
 1240        public Guid GetGenreId(string name)
 1241        {
 01242            return GetItemByNameId<Genre>(Genre.GetPath(name));
 1243        }
 1244
 1245        public Guid GetMusicGenreId(string name)
 1246        {
 01247            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 1248        }
 1249
 1250        /// <summary>
 1251        /// Gets the genre.
 1252        /// </summary>
 1253        /// <param name="name">The name.</param>
 1254        /// <returns>Task{Genre}.</returns>
 1255        public Genre GetGenre(string name)
 1256        {
 01257            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 1258        }
 1259
 1260        /// <summary>
 1261        /// Gets the music genre.
 1262        /// </summary>
 1263        /// <param name="name">The name.</param>
 1264        /// <returns>Task{MusicGenre}.</returns>
 1265        public MusicGenre GetMusicGenre(string name)
 1266        {
 01267            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1268        }
 1269
 1270        /// <summary>
 1271        /// Gets the year.
 1272        /// </summary>
 1273        /// <param name="value">The value.</param>
 1274        /// <returns>Task{Year}.</returns>
 1275        public Year GetYear(int value)
 1276        {
 01277            if (value <= 0)
 1278            {
 01279                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1280            }
 1281
 01282            var name = value.ToString(CultureInfo.InvariantCulture);
 1283
 01284            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1285        }
 1286
 1287        /// <summary>
 1288        /// Gets a Genre.
 1289        /// </summary>
 1290        /// <param name="name">The name.</param>
 1291        /// <returns>Task{Genre}.</returns>
 1292        public MusicArtist GetArtist(string name)
 1293        {
 01294            return GetArtist(name, new DtoOptions(true));
 1295        }
 1296
 1297        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1298        {
 181299            return _linkedChildrenService.FindArtists(names);
 1300        }
 1301
 1302        public MusicArtist GetArtist(string name, DtoOptions options)
 1303        {
 01304            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1305        }
 1306
 1307        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1308            where T : BaseItem, new()
 1309        {
 01310            if (typeof(T) == typeof(MusicArtist))
 1311            {
 01312                var existing = GetItemList(new InternalItemsQuery
 01313                {
 01314                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01315                    Name = name,
 01316                    UseRawName = true,
 01317                    DtoOptions = options
 01318                }).Cast<MusicArtist>()
 01319                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01320                .Cast<T>()
 01321                .FirstOrDefault();
 1322
 01323                if (existing is not null)
 1324                {
 01325                    return existing;
 1326                }
 1327            }
 1328
 01329            var path = getPathFn(name);
 01330            var id = GetItemByNameId<T>(path);
 01331            var item = GetItemById(id) as T;
 01332            if (item is null)
 1333            {
 01334                var info = Directory.CreateDirectory(path);
 01335                item = new T
 01336                {
 01337                    Name = name,
 01338                    Id = id,
 01339                    DateCreated = info.CreationTimeUtc,
 01340                    DateModified = info.LastWriteTimeUtc,
 01341                    Path = path
 01342                };
 1343
 01344                CreateItem(item, null);
 1345            }
 1346
 01347            return item;
 1348        }
 1349
 1350        private Guid GetItemByNameId<T>(string path)
 1351              where T : BaseItem, new()
 1352        {
 11353            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11354            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1355        }
 1356
 1357        /// <inheritdoc />
 1358        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1359        {
 1360            // Ensure the location is available.
 01361            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1362
 01363            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1364        }
 1365
 1366        /// <summary>
 1367        /// Reloads the root media folder.
 1368        /// </summary>
 1369        /// <param name="progress">The progress.</param>
 1370        /// <param name="cancellationToken">The cancellation token.</param>
 1371        /// <returns>Task.</returns>
 1372        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1373        {
 1374            // Just run the scheduled task so that the user can see it
 31375            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1376
 31377            return Task.CompletedTask;
 1378        }
 1379
 1380        /// <summary>
 1381        /// Validates the media library internal.
 1382        /// </summary>
 1383        /// <param name="progress">The progress.</param>
 1384        /// <param name="cancellationToken">The cancellation token.</param>
 1385        /// <returns>Task.</returns>
 1386        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1387        {
 201388            IsScanRunning = true;
 201389            ClearIgnoreRuleCache();
 201390            LibraryMonitor.Stop();
 1391
 1392            try
 1393            {
 201394                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 181395            }
 1396            finally
 1397            {
 201398                ClearIgnoreRuleCache();
 201399                LibraryMonitor.Start();
 201400                IsScanRunning = false;
 1401            }
 181402        }
 1403
 1404        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1405        {
 231406            ClearIgnoreRuleCache();
 231407            RootFolder.Children = null;
 231408            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1409
 1410            // Start by just validating the children of the root, but go no further
 231411            await RootFolder.ValidateChildren(
 231412                new Progress<double>(),
 231413                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 231414                recursive: false,
 231415                allowRemoveRoot: removeRoot,
 231416                cancellationToken: cancellationToken).ConfigureAwait(false);
 1417
 231418            var rootFolder = GetUserRootFolder();
 231419            rootFolder.Children = null;
 1420
 231421            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1422
 221423            await rootFolder.ValidateChildren(
 221424                new Progress<double>(),
 221425                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 221426                recursive: false,
 221427                allowRemoveRoot: removeRoot,
 221428                cancellationToken: cancellationToken).ConfigureAwait(false);
 1429
 1430            // Quickly scan CollectionFolders for changes
 211431            var toDelete = new List<Guid>();
 581432            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1433            {
 1434                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 81435                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1436                {
 11437                    toDelete.Add(collectionFolder.Id);
 1438                }
 1439                else
 1440                {
 71441                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1442                }
 1443            }
 1444
 211445            if (toDelete.Count > 0)
 1446            {
 11447                _persistenceService.DeleteItem(toDelete.ToArray());
 1448            }
 1449
 211450            ClearIgnoreRuleCache();
 211451        }
 1452
 1453        /// <inheritdoc />
 1454        public void ClearIgnoreRuleCache()
 1455        {
 841456            _dotIgnoreIgnoreRule.ClearDirectoryCache();
 841457        }
 1458
 1459        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1460        {
 201461            _logger.LogInformation("Validating media library");
 1462
 201463            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1464
 181465            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1466
 1467            // Validate the entire media library
 181468            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1469
 181470            progress.Report(96);
 1471
 181472            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1473
 181474            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1475
 181476            progress.Report(100);
 181477        }
 1478
 1479        /// <summary>
 1480        /// Runs the post scan tasks.
 1481        /// </summary>
 1482        /// <param name="progress">The progress.</param>
 1483        /// <param name="cancellationToken">The cancellation token.</param>
 1484        /// <returns>Task.</returns>
 1485        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1486        {
 181487            var tasks = PostScanTasks.ToList();
 1488
 181489            var numComplete = 0;
 181490            var numTasks = tasks.Count;
 1491
 3241492            foreach (var task in tasks)
 1493            {
 1494                // Prevent access to modified closure
 1441495                var currentNumComplete = numComplete;
 1496
 1441497                var innerProgress = new Progress<double>(pct =>
 1441498                {
 1441499                    double innerPercent = pct;
 1441500                    innerPercent /= 100;
 1441501                    innerPercent += currentNumComplete;
 1441502
 1441503                    innerPercent /= numTasks;
 1441504                    innerPercent *= 100;
 1441505
 1441506                    progress.Report(innerPercent);
 1441507                });
 1508
 1441509                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1510
 1511                try
 1512                {
 1441513                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1441514                }
 01515                catch (OperationCanceledException)
 1516                {
 01517                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 01518                    throw;
 1519                }
 01520                catch (Exception ex)
 1521                {
 01522                    _logger.LogError(ex, "Error running post-scan task");
 01523                }
 1524
 1441525                numComplete++;
 1441526                double percent = numComplete;
 1441527                percent /= numTasks;
 1441528                progress.Report(percent * 100);
 1441529            }
 1530
 181531            _persistenceService.UpdateInheritedValues();
 1532
 181533            progress.Report(100);
 181534        }
 1535
 1536        /// <summary>
 1537        /// Gets the default view.
 1538        /// </summary>
 1539        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1540        public List<VirtualFolderInfo> GetVirtualFolders()
 1541        {
 241542            return GetVirtualFolders(false);
 1543        }
 1544
 1545        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1546        {
 251547            _logger.LogDebug("Getting topLibraryFolders");
 251548            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1549
 251550            _logger.LogDebug("Getting refreshQueue");
 251551            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1552
 251553            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 251554                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 251555                .ToList();
 1556        }
 1557
 1558        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1559        {
 11560            var info = new VirtualFolderInfo
 11561            {
 11562                Name = Path.GetFileName(dir),
 11563
 11564                Locations = _fileSystem.GetFilePaths(dir, false)
 11565                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11566                    .Select(i =>
 11567                    {
 11568                        try
 11569                        {
 11570                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11571                        }
 11572                        catch (Exception ex)
 11573                        {
 11574                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11575                            return null;
 11576                        }
 11577                    })
 11578                    .Where(i => i is not null)
 11579                    .Order()
 11580                    .ToArray(),
 11581
 11582                CollectionType = GetCollectionType(dir)
 11583            };
 1584
 11585            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11586            if (libraryFolder is not null)
 1587            {
 11588                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11589                info.ItemId = libraryFolderId;
 11590                if (libraryFolder.HasImage(ImageType.Primary))
 1591                {
 01592                    info.PrimaryImageItemId = libraryFolderId;
 1593                }
 1594
 11595                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1596
 11597                if (refreshQueue is not null)
 1598                {
 11599                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1600
 11601                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1602                }
 1603            }
 1604
 11605            return info;
 1606        }
 1607
 1608        private CollectionTypeOptions? GetCollectionType(string path)
 1609        {
 11610            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21611            foreach (ReadOnlySpan<char> file in files)
 1612            {
 01613                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1614                {
 01615                    return res;
 1616                }
 1617            }
 1618
 11619            return null;
 01620        }
 1621
 1622        /// <inheritdoc />
 1623        public BaseItem? GetItemById(Guid id)
 1624        {
 5841625            if (id.IsEmpty())
 1626            {
 01627                throw new ArgumentException("Guid can't be empty", nameof(id));
 1628            }
 1629
 5841630            if (_cache.TryGet(id, out var item))
 1631            {
 4581632                return item;
 1633            }
 1634
 1261635            item = RetrieveItem(id);
 1636
 1261637            if (item is not null)
 1638            {
 01639                RegisterItem(item);
 1640            }
 1641
 1261642            return item;
 1643        }
 1644
 1645        /// <inheritdoc />
 1646        public T? GetItemById<T>(Guid id)
 1647            where T : BaseItem
 1648        {
 241649            var item = GetItemById(id);
 241650            if (item is T typedItem)
 1651            {
 11652                return typedItem;
 1653            }
 1654
 231655            return null;
 1656        }
 1657
 1658        /// <inheritdoc />
 1659        public T? GetItemById<T>(Guid id, Guid userId)
 1660            where T : BaseItem
 1661        {
 11662            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11663            return GetItemById<T>(id, user);
 1664        }
 1665
 1666        /// <inheritdoc />
 1667        public T? GetItemById<T>(Guid id, User? user)
 1668            where T : BaseItem
 1669        {
 221670            var item = GetItemById<T>(id);
 221671            return ItemIsVisible(item, user) ? item : null;
 1672        }
 1673
 1674        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1675        {
 1861676            if (query.Recursive && !query.ParentId.IsEmpty())
 1677            {
 471678                var parent = GetItemById(query.ParentId);
 471679                if (parent is not null)
 1680                {
 471681                    SetTopParentIdsOrAncestors(query, [parent]);
 1682                }
 1683            }
 1684
 1861685            if (query.User is not null)
 1686            {
 11687                AddUserToQuery(query, query.User, allowExternalContent);
 1688            }
 1689
 1861690            return _itemRepository.GetItemList(query);
 1691        }
 1692
 1693        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1694        {
 1861695            return GetItemList(query, true);
 1696        }
 1697
 1698        public int GetCount(InternalItemsQuery query)
 1699        {
 01700            if (query.Recursive && !query.ParentId.IsEmpty())
 1701            {
 01702                var parent = GetItemById(query.ParentId);
 01703                if (parent is not null)
 1704                {
 01705                    SetTopParentIdsOrAncestors(query, [parent]);
 1706                }
 1707            }
 1708
 01709            if (query.User is not null)
 1710            {
 01711                AddUserToQuery(query, query.User);
 1712            }
 1713
 01714            return _countService.GetCount(query);
 1715        }
 1716
 1717        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1718        {
 01719            if (query.Recursive && !query.ParentId.IsEmpty())
 1720            {
 01721                var parent = GetItemById(query.ParentId);
 01722                if (parent is not null)
 1723                {
 01724                    SetTopParentIdsOrAncestors(query, [parent]);
 1725                }
 1726            }
 1727
 01728            if (query.User is not null)
 1729            {
 01730                AddUserToQuery(query, query.User);
 1731            }
 1732
 01733            return _countService.GetItemCounts(query);
 1734        }
 1735
 1736        /// <inheritdoc/>
 1737        public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? us
 1738        {
 01739            var query = new InternalItemsQuery(user);
 01740            if (user is not null)
 1741            {
 01742                AddUserToQuery(query, user);
 1743            }
 1744
 01745            return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
 1746        }
 1747
 1748        public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
 1749        {
 01750            return _countService.GetChildCountBatch(parentIds, userId);
 1751        }
 1752
 1753        /// <inheritdoc/>
 1754        public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User
 1755        {
 01756            return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
 1757        }
 1758
 1759        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1760        {
 01761            SetTopParentIdsOrAncestors(query, parents);
 1762
 01763            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1764            {
 01765                if (query.User is not null)
 1766                {
 01767                    AddUserToQuery(query, query.User);
 1768                }
 1769            }
 1770
 01771            return _itemRepository.GetItemList(query);
 1772        }
 1773
 1774        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1775        {
 01776            SetTopParentIdsOrAncestors(query, parents);
 1777
 01778            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1779            {
 01780                if (query.User is not null)
 1781                {
 01782                    AddUserToQuery(query, query.User);
 1783                }
 1784            }
 1785
 01786            return _itemRepository.GetLatestItemList(query, collectionType);
 1787        }
 1788
 1789        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1790        {
 01791            SetTopParentIdsOrAncestors(query, parents);
 1792
 01793            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1794            {
 01795                if (query.User is not null)
 1796                {
 01797                    AddUserToQuery(query, query.User);
 1798                }
 1799            }
 1800
 01801            return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
 1802        }
 1803
 1804        /// <inheritdoc />
 1805        public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisod
 1806            InternalItemsQuery query,
 1807            IReadOnlyList<string> seriesKeys,
 1808            bool includeSpecials,
 1809            bool includeWatchedForRewatching)
 1810        {
 01811            return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching
 1812        }
 1813
 1814        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1815        {
 01816            if (query.User is not null)
 1817            {
 01818                AddUserToQuery(query, query.User);
 1819            }
 1820
 01821            if (query.EnableTotalRecordCount)
 1822            {
 01823                return _itemRepository.GetItems(query);
 1824            }
 1825
 01826            return new QueryResult<BaseItem>(
 01827                query.StartIndex,
 01828                null,
 01829                _itemRepository.GetItemList(query));
 1830        }
 1831
 1832        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1833        {
 901834            if (query.User is not null)
 1835            {
 01836                AddUserToQuery(query, query.User);
 1837            }
 1838
 901839            return _itemRepository.GetItemIdsList(query);
 1840        }
 1841
 1842        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1843        {
 01844            if (query.User is not null)
 1845            {
 01846                AddUserToQuery(query, query.User);
 1847            }
 1848
 01849            SetTopParentOrAncestorIds(query);
 01850            return _itemRepository.GetStudios(query);
 1851        }
 1852
 1853        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1854        {
 01855            if (query.User is not null)
 1856            {
 01857                AddUserToQuery(query, query.User);
 1858            }
 1859
 01860            SetTopParentOrAncestorIds(query);
 01861            return _itemRepository.GetGenres(query);
 1862        }
 1863
 1864        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1865        {
 01866            if (query.User is not null)
 1867            {
 01868                AddUserToQuery(query, query.User);
 1869            }
 1870
 01871            SetTopParentOrAncestorIds(query);
 01872            return _itemRepository.GetMusicGenres(query);
 1873        }
 1874
 1875        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1876        {
 01877            if (query.User is not null)
 1878            {
 01879                AddUserToQuery(query, query.User);
 1880            }
 1881
 01882            SetTopParentOrAncestorIds(query);
 01883            return _itemRepository.GetAllArtists(query);
 1884        }
 1885
 1886        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1887        {
 01888            if (query.User is not null)
 1889            {
 01890                AddUserToQuery(query, query.User);
 1891            }
 1892
 01893            SetTopParentOrAncestorIds(query);
 01894            return _itemRepository.GetArtists(query);
 1895        }
 1896
 1897        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1898        {
 01899            var ancestorIds = query.AncestorIds;
 01900            int len = ancestorIds.Length;
 01901            if (len == 0)
 1902            {
 01903                return;
 1904            }
 1905
 01906            var parents = new BaseItem[len];
 01907            for (int i = 0; i < len; i++)
 1908            {
 01909                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01910                if (parents[i] is not (ICollectionFolder or UserView))
 1911                {
 01912                    return;
 1913                }
 1914            }
 1915
 1916            // Optimize by querying against top level views
 01917            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01918            query.AncestorIds = [];
 1919
 1920            // Prevent searching in all libraries due to empty filter
 01921            if (query.TopParentIds.Length == 0)
 1922            {
 01923                query.TopParentIds = [Guid.NewGuid()];
 1924            }
 01925        }
 1926
 1927        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1928        {
 01929            if (query.User is not null)
 1930            {
 01931                AddUserToQuery(query, query.User);
 1932            }
 1933
 01934            SetTopParentOrAncestorIds(query);
 01935            return _itemRepository.GetAlbumArtists(query);
 1936        }
 1937
 1938        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1939        {
 181940            if (query.Recursive && !query.ParentId.IsEmpty())
 1941            {
 171942                var parent = GetItemById(query.ParentId);
 171943                if (parent is not null)
 1944                {
 171945                    SetTopParentIdsOrAncestors(query, [parent]);
 1946                }
 1947            }
 1948
 181949            if (query.User is not null)
 1950            {
 11951                AddUserToQuery(query, query.User);
 1952            }
 1953
 181954            if (query.EnableTotalRecordCount)
 1955            {
 11956                return _itemRepository.GetItems(query);
 1957            }
 1958
 171959            return new QueryResult<BaseItem>(
 171960                query.StartIndex,
 171961                null,
 171962                _itemRepository.GetItemList(query));
 1963        }
 1964
 1965        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1966        {
 641967            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1968            {
 1969                // Optimize by querying against top level views
 171970                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1971
 1972                // Prevent searching in all libraries due to empty filter
 171973                if (query.TopParentIds.Length == 0)
 1974                {
 171975                    query.TopParentIds = [Guid.NewGuid()];
 1976                }
 1977            }
 471978            else if (parents.Count == 1 && parents.First() is Folder folder
 471979                && (folder is Playlist || folder is BoxSet)
 471980                && folder.LinkedChildren.Length > 0)
 1981            {
 1982                // Playlists and BoxSets store their contents in LinkedChildren and never
 1983                // populate AncestorIds for those items, so a recursive AncestorIds query
 1984                // would return zero rows. Resolve to the linked child IDs up front and
 1985                // route through the existing indexed ItemIds filter.
 01986                query.ItemIds = folder.LinkedChildren
 01987                    .Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
 01988                    .Select(lc => lc.ItemId!.Value)
 01989                    .ToArray();
 1990
 1991                // Empty linked-children should still return empty rather than scanning everything.
 01992                if (query.ItemIds.Length == 0)
 1993                {
 01994                    query.ItemIds = [Guid.NewGuid()];
 1995                }
 1996            }
 1997            else
 1998            {
 1999                // We need to be able to query from any arbitrary ancestor up the tree
 472000                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 2001
 2002                // Prevent searching in all libraries due to empty filter
 472003                if (query.AncestorIds.Length == 0)
 2004                {
 02005                    query.AncestorIds = [Guid.NewGuid()];
 2006                }
 2007            }
 2008
 642009            query.Parent = null;
 642010        }
 2011
 2012        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 2013        {
 22014            if (query.User is null)
 2015            {
 02016                query.SetUser(user);
 2017            }
 2018
 22019            if (query.AncestorIds.Length == 0 &&
 22020                query.ParentId.IsEmpty() &&
 22021                query.ChannelIds.Count == 0 &&
 22022                query.TopParentIds.Length == 0 &&
 22023                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 22024                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 22025                query.ItemIds.Length == 0 &&
 22026                query.OwnerIds.Length == 0)
 2027            {
 12028                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 12029                {
 12030                    User = user,
 12031                    IncludeHidden = true,
 12032                    IncludeExternalContent = allowExternalContent
 12033                });
 2034
 12035                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 2036
 2037                // Prevent searching in all libraries due to empty filter
 12038                if (query.TopParentIds.Length == 0)
 2039                {
 12040                    query.TopParentIds = [Guid.NewGuid()];
 2041                }
 2042            }
 22043        }
 2044
 2045        /// <inheritdoc/>
 2046        public void ConfigureUserAccess(InternalItemsQuery query, User user)
 2047        {
 02048            ArgumentNullException.ThrowIfNull(query);
 02049            ArgumentNullException.ThrowIfNull(user);
 2050
 02051            AddUserToQuery(query, user);
 02052        }
 2053
 2054        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 2055        {
 172056            if (item is UserView view)
 2057            {
 02058                if (view.ViewType == CollectionType.livetv)
 2059                {
 02060                    return [view.Id];
 2061                }
 2062
 2063                // Translate view into folders
 02064                if (!view.DisplayParentId.IsEmpty())
 2065                {
 02066                    var displayParent = GetItemById(view.DisplayParentId);
 02067                    if (displayParent is not null)
 2068                    {
 02069                        return GetTopParentIdsForQuery(displayParent, user);
 2070                    }
 2071
 02072                    return [];
 2073                }
 2074
 02075                if (!view.ParentId.IsEmpty())
 2076                {
 02077                    var displayParent = GetItemById(view.ParentId);
 02078                    if (displayParent is not null)
 2079                    {
 02080                        return GetTopParentIdsForQuery(displayParent, user);
 2081                    }
 2082
 02083                    return [];
 2084                }
 2085
 2086                // Handle grouping
 02087                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 02088                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 2089                {
 02090                    return GetUserRootFolder()
 02091                        .GetChildren(user, true)
 02092                        .OfType<CollectionFolder>()
 02093                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 02094                        .Where(i => user.IsFolderGrouped(i.Id))
 02095                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 2096                }
 2097
 02098                return [];
 2099            }
 2100
 172101            if (item is CollectionFolder collectionFolder)
 2102            {
 172103                return collectionFolder.PhysicalFolderIds;
 2104            }
 2105
 02106            var topParent = item.GetTopParent();
 02107            if (topParent is not null)
 2108            {
 02109                return [topParent.Id];
 2110            }
 2111
 02112            return [];
 2113        }
 2114
 2115        /// <summary>
 2116        /// Gets the intros.
 2117        /// </summary>
 2118        /// <param name="item">The item.</param>
 2119        /// <param name="user">The user.</param>
 2120        /// <returns>IEnumerable{System.String}.</returns>
 2121        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 2122        {
 02123            if (IntroProviders.Length == 0)
 2124            {
 02125                return [];
 2126            }
 2127
 02128            var tasks = IntroProviders
 02129                .Select(i => GetIntros(i, item, user));
 2130
 02131            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 2132
 02133            return items
 02134                .SelectMany(i => i)
 02135                .Select(ResolveIntro)
 02136                .Where(i => i is not null)!; // null values got filtered out
 02137        }
 2138
 2139        /// <summary>
 2140        /// Gets the intros.
 2141        /// </summary>
 2142        /// <param name="provider">The provider.</param>
 2143        /// <param name="item">The item.</param>
 2144        /// <param name="user">The user.</param>
 2145        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 2146        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 2147        {
 2148            try
 2149            {
 02150                return await provider.GetIntros(item, user).ConfigureAwait(false);
 2151            }
 02152            catch (Exception ex)
 2153            {
 02154                _logger.LogError(ex, "Error getting intros");
 2155
 02156                return [];
 2157            }
 02158        }
 2159
 2160        /// <summary>
 2161        /// Resolves the intro.
 2162        /// </summary>
 2163        /// <param name="info">The info.</param>
 2164        /// <returns>Video.</returns>
 2165        private Video? ResolveIntro(IntroInfo info)
 2166        {
 02167            Video? video = null;
 2168
 02169            if (info.ItemId.HasValue)
 2170            {
 2171                // Get an existing item by Id
 02172                video = GetItemById(info.ItemId.Value) as Video;
 2173
 02174                if (video is null)
 2175                {
 02176                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 2177                }
 2178            }
 02179            else if (!string.IsNullOrEmpty(info.Path))
 2180            {
 2181                try
 2182                {
 2183                    // Try to resolve the path into a video
 02184                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 2185
 02186                    if (video is null)
 2187                    {
 02188                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 2189                    }
 2190                    else
 2191                    {
 2192                        // Pull the saved db item that will include metadata
 02193                        var dbItem = GetItemById(video.Id) as Video;
 2194
 02195                        if (dbItem is not null)
 2196                        {
 02197                            video = dbItem;
 2198                        }
 2199                        else
 2200                        {
 02201                            return null;
 2202                        }
 2203                    }
 02204                }
 02205                catch (Exception ex)
 2206                {
 02207                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 02208                }
 2209            }
 2210            else
 2211            {
 02212                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 2213            }
 2214
 02215            return video;
 02216        }
 2217
 2218        /// <inheritdoc />
 2219        public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
 2220        {
 02221            ArgumentNullException.ThrowIfNull(video);
 2222
 02223            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02224            if (linkedIds.Count > 0)
 2225            {
 02226                return linkedIds;
 2227            }
 2228
 02229            return [];
 2230        }
 2231
 2232        /// <inheritdoc />
 2233        public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
 2234        {
 02235            ArgumentNullException.ThrowIfNull(video);
 2236
 02237            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02238            if (linkedIds.Count > 0)
 2239            {
 02240                return linkedIds
 02241                    .Select(id => GetItemById(id))
 02242                    .Where(i => i is not null)
 02243                    .OfType<Video>()
 02244                    .OrderBy(i => i.SortName);
 2245            }
 2246
 02247            return [];
 2248        }
 2249
 2250        /// <inheritdoc />
 2251        public IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds)
 2252        {
 02253            return _linkedChildrenService.GetItemIdsWithAlternateVersions(itemIds);
 2254        }
 2255
 2256        /// <inheritdoc />
 2257        public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType chil
 2258        {
 02259            _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
 02260        }
 2261
 2262        /// <inheritdoc />
 2263        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 2264        {
 12265            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2266
 42267            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 2268            {
 12269                if (orderBy is RandomComparer)
 2270                {
 02271                    var randomItems = items.ToArray();
 02272                    Random.Shared.Shuffle(randomItems);
 02273                    items = randomItems;
 2274                    // Items are no longer ordered at this point, so set orderedItems back to null
 02275                    orderedItems = null;
 2276                }
 12277                else if (orderedItems is null)
 2278                {
 12279                    orderedItems = sortOrder == SortOrder.Descending
 12280                        ? items.OrderByDescending(i => i, orderBy)
 12281                        : items.OrderBy(i => i, orderBy);
 2282                }
 2283                else
 2284                {
 02285                    orderedItems = sortOrder == SortOrder.Descending
 02286                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 02287                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 2288                }
 2289            }
 2290
 12291            return orderedItems ?? items;
 2292        }
 2293
 2294        /// <inheritdoc />
 2295        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 2296        {
 22297            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2298
 72299            foreach (var (name, sortOrder) in orderBy)
 2300            {
 22301                var comparer = GetComparer(name, user);
 12302                if (comparer is null)
 2303                {
 2304                    continue;
 2305                }
 2306
 12307                if (comparer is RandomComparer)
 2308                {
 02309                    var randomItems = items.ToArray();
 02310                    Random.Shared.Shuffle(randomItems);
 02311                    items = randomItems;
 2312                    // Items are no longer ordered at this point, so set orderedItems back to null
 02313                    orderedItems = null;
 2314                }
 12315                else if (orderedItems is null)
 2316                {
 12317                    orderedItems = sortOrder == SortOrder.Descending
 12318                        ? items.OrderByDescending(i => i, comparer)
 12319                        : items.OrderBy(i => i, comparer);
 2320                }
 2321                else
 2322                {
 02323                    orderedItems = sortOrder == SortOrder.Descending
 02324                        ? orderedItems!.ThenByDescending(i => i, comparer)
 02325                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 2326                }
 2327            }
 2328
 12329            return orderedItems ?? items;
 2330        }
 2331
 2332        /// <summary>
 2333        /// Gets the comparer.
 2334        /// </summary>
 2335        /// <param name="name">The name.</param>
 2336        /// <param name="user">The user.</param>
 2337        /// <returns>IBaseItemComparer.</returns>
 2338        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 2339        {
 32340            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 2341
 32342            if (comparer is IUserBaseItemComparer)
 2343            {
 12344                if (user is null)
 2345                {
 12346                    throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
 2347                }
 2348
 02349                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 2350
 02351                userComparer.User = user;
 02352                userComparer.UserManager = _userManager;
 02353                userComparer.UserDataManager = _userDataManager;
 2354
 02355                return userComparer;
 2356            }
 2357
 22358            return comparer;
 2359        }
 2360
 2361        /// <inheritdoc />
 2362        public void CreateItem(BaseItem item, BaseItem? parent)
 2363        {
 02364            CreateItems([item], parent, CancellationToken.None);
 02365        }
 2366
 2367        /// <inheritdoc />
 2368        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 2369        {
 2370            // Resolve and add any local alternate version items that don't exist yet
 2371            // This ensures they exist in the database when LinkedChildren are processed
 22372            var allItems = new List<BaseItem>(items);
 22373            var parentFolder = parent as Folder;
 22374            var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
 82375            foreach (var item in items)
 2376            {
 22377                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2378                {
 02379                    var videoType = video.GetType();
 02380                    foreach (var path in video.LocalAlternateVersions)
 2381                    {
 02382                        if (string.IsNullOrEmpty(path))
 2383                        {
 2384                            continue;
 2385                        }
 2386
 2387                        // Use the primary video's type for ID calculation to ensure consistency
 02388                        var altId = GetNewItemId(path, videoType);
 02389                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2390                        {
 2391                            // Alternate version doesn't exist, resolve and create it
 2392                            // ensuring it has the same type as the primary video
 02393                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02394                            if (altVideo is not null)
 2395                            {
 02396                                altVideo.OwnerId = video.Id;
 02397                                altVideo.SetPrimaryVersionId(video.Id);
 02398                                altVideo.IsInMixedFolder = video.IsInMixedFolder;
 2399                                // ResolveAlternateVersion only sees the alternate's primary file.
 2400                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2401                                // detect its parts from sibling files so its AdditionalParts persist.
 02402                                SetAdditionalPartsFromStack(altVideo, path);
 02403                                allItems.Add(altVideo);
 2404                            }
 2405                        }
 2406                    }
 2407                }
 2408            }
 2409
 22410            _persistenceService.SaveItems(allItems, cancellationToken);
 2411
 82412            foreach (var item in allItems)
 2413            {
 22414                RegisterItem(item);
 2415            }
 2416
 22417            if (parent is Folder folder)
 2418            {
 22419                folder.Children = null;
 22420                folder.UserData = null;
 2421            }
 2422
 22423            if (ItemAdded is not null)
 2424            {
 82425                foreach (var item in items)
 2426                {
 2427                    // With the live tv guide this just creates too much noise
 22428                    if (item.SourceType != SourceType.Library)
 2429                    {
 2430                        continue;
 2431                    }
 2432
 2433                    try
 2434                    {
 22435                        ItemAdded(
 22436                            this,
 22437                            new ItemChangeEventArgs
 22438                            {
 22439                                Item = item,
 22440                                Parent = parent ?? item.GetParent()
 22441                            });
 22442                    }
 02443                    catch (Exception ex)
 2444                    {
 02445                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02446                    }
 2447                }
 2448            }
 22449        }
 2450
 2451        private bool ImageNeedsRefresh(ItemImageInfo image)
 2452        {
 02453            if (image.Path is not null && image.IsLocalFile)
 2454            {
 02455                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2456                {
 02457                    return true;
 2458                }
 2459
 2460                try
 2461                {
 02462                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2463                }
 02464                catch (Exception ex)
 2465                {
 02466                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02467                    return false;
 2468                }
 2469            }
 2470
 02471            return image.Path is not null && !image.IsLocalFile;
 02472        }
 2473
 2474        /// <inheritdoc />
 2475        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2476        {
 1622477            ArgumentNullException.ThrowIfNull(item);
 2478
 1622479            var outdated = forceUpdate
 1622480                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1622481                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2482
 1622483            var parentItem = item.GetParent();
 1622484            var isLiveTvShow = item.SourceType != SourceType.Library &&
 1622485                               parentItem is not null &&
 1622486                               parentItem.SourceType != SourceType.Library; // not a channel
 2487
 2488            // Skip image processing if current or live tv show
 1622489            if (outdated.Length == 0 || isLiveTvShow)
 2490            {
 1622491                RegisterItem(item);
 1622492                return;
 2493            }
 2494
 02495            foreach (var img in outdated)
 2496            {
 02497                var image = img;
 02498                if (!img.IsLocalFile)
 2499                {
 2500                    try
 2501                    {
 02502                        var index = item.GetImageIndex(img);
 02503                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 02504                    }
 02505                    catch (ArgumentException)
 2506                    {
 02507                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 02508                        continue;
 2509                    }
 02510                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2511                    {
 02512                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 02513                        continue;
 2514                    }
 02515                    catch (HttpRequestException ex)
 2516                    {
 02517                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 02518                        continue;
 2519                    }
 2520                }
 2521
 02522                if (!File.Exists(image.Path))
 2523                {
 02524                    _logger.LogWarning("Image not found at {ImagePath}", image.Path);
 02525                    continue;
 2526                }
 2527
 2528                ImageDimensions size;
 2529                try
 2530                {
 02531                    size = _imageProcessor.GetImageDimensions(item, image);
 02532                    image.Width = size.Width;
 02533                    image.Height = size.Height;
 02534                }
 02535                catch (Exception ex)
 2536                {
 02537                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 02538                    size = default;
 02539                    image.Width = 0;
 02540                    image.Height = 0;
 02541                }
 2542
 2543                try
 2544                {
 02545                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 02546                    image.BlurHash = blurhash;
 02547                }
 02548                catch (Exception ex)
 2549                {
 02550                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 02551                    image.BlurHash = string.Empty;
 02552                }
 2553
 2554                try
 2555                {
 02556                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 02557                    image.DateModified = modifiedDate;
 02558                }
 02559                catch (Exception ex)
 2560                {
 02561                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 02562                }
 02563            }
 2564
 02565            item.ValidateImages();
 2566
 02567            await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
 2568
 02569            RegisterItem(item);
 1622570        }
 2571
 2572        /// <inheritdoc />
 2573        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2574        {
 4682575            foreach (var item in items)
 2576            {
 1172577                item.DateLastSaved = DateTime.UtcNow;
 1172578                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2579
 2580                // Modify again, so saved value is after write time of externally saved metadata
 1172581                item.DateLastSaved = DateTime.UtcNow;
 1172582            }
 2583
 1172584            ForgetDroppedLocalAlternateVersions(items);
 2585
 2586            // Resolve and add any local alternate version items that don't exist yet
 2587            // This ensures they exist in the database when LinkedChildren are processed
 1172588            var allItems = new List<BaseItem>(items);
 1172589            var parentFolder = parent as Folder;
 1172590            var parentCollectionType = GetTopFolderContentType(parent);
 4682591            foreach (var item in items)
 2592            {
 1172593                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2594                {
 02595                    var videoType = video.GetType();
 02596                    foreach (var path in video.LocalAlternateVersions)
 2597                    {
 02598                        if (string.IsNullOrEmpty(path))
 2599                        {
 2600                            continue;
 2601                        }
 2602
 2603                        // Use the primary video's type for ID calculation to ensure consistency
 02604                        var altId = GetNewItemId(path, videoType);
 02605                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2606                        {
 2607                            // Alternate version doesn't exist, resolve and create it
 2608                            // ensuring it has the same type as the primary video
 02609                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02610                            if (altVideo is not null)
 2611                            {
 02612                                altVideo.OwnerId = video.Id;
 02613                                altVideo.SetPrimaryVersionId(video.Id);
 02614                                altVideo.IsInMixedFolder = video.IsInMixedFolder;
 2615                                // ResolveAlternateVersion only sees the alternate's primary file.
 2616                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2617                                // detect its parts from sibling files so its AdditionalParts persist.
 02618                                SetAdditionalPartsFromStack(altVideo, path);
 02619                                allItems.Add(altVideo);
 2620                            }
 2621                        }
 2622                    }
 2623                }
 2624            }
 2625
 1172626            _persistenceService.SaveItems(allItems, cancellationToken);
 2627
 4682628            foreach (var item in allItems)
 2629            {
 1172630                if (!items.Contains(item))
 2631                {
 02632                    RegisterItem(item);
 2633                }
 2634            }
 2635
 1172636            if (parent is Folder folder)
 2637            {
 262638                folder.Children = null;
 262639                folder.UserData = null;
 2640            }
 2641
 1172642            if (ItemUpdated is not null)
 2643            {
 4642644                foreach (var item in items)
 2645                {
 2646                    // With the live tv guide this just creates too much noise
 1162647                    if (item.SourceType != SourceType.Library)
 2648                    {
 2649                        continue;
 2650                    }
 2651
 2652                    try
 2653                    {
 1162654                        ItemUpdated(
 1162655                            this,
 1162656                            new ItemChangeEventArgs
 1162657                            {
 1162658                                Item = item,
 1162659                                Parent = parent,
 1162660                                UpdateReason = updateReason
 1162661                            });
 1162662                    }
 02663                    catch (Exception ex)
 2664                    {
 02665                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 02666                    }
 2667                }
 2668            }
 1172669        }
 2670
 2671        /// <inheritdoc />
 2672        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 1172673            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2674
 2675        /// <summary>
 2676        /// Forgets the cached local alternate versions of the supplied items that they no longer list.
 2677        /// </summary>
 2678        /// <param name="items">The items about to be saved.</param>
 2679        private void ForgetDroppedLocalAlternateVersions(IReadOnlyList<BaseItem> items)
 2680        {
 2342681            foreach (var video in items.OfType<Video>())
 2682            {
 02683                var videoType = video.GetType();
 02684                var keptIds = video.LocalAlternateVersions
 02685                    .Where(path => !string.IsNullOrEmpty(path))
 02686                    .Select(path => GetNewItemId(path, videoType))
 02687                    .ToHashSet();
 2688
 02689                foreach (var versionId in GetLocalAlternateVersionIds(video))
 2690                {
 02691                    if (!keptIds.Contains(versionId))
 2692                    {
 02693                        _cache.TryRemove(versionId, out _);
 2694                    }
 2695                }
 2696            }
 1172697        }
 2698
 2699        /// <inheritdoc />
 2700        public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
 2701        {
 362702            await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
 352703        }
 2704
 2705        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2706        {
 1172707            if (item.IsFileProtocol)
 2708            {
 1172709                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2710            }
 2711
 1172712            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 1172713        }
 2714
 2715        /// <summary>
 2716        /// Reports the item removed.
 2717        /// </summary>
 2718        /// <param name="item">The item.</param>
 2719        /// <param name="parent">The parent item.</param>
 2720        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2721        {
 02722            if (ItemRemoved is not null)
 2723            {
 2724                try
 2725                {
 02726                    ItemRemoved(
 02727                        this,
 02728                        new ItemChangeEventArgs
 02729                        {
 02730                            Item = item,
 02731                            Parent = parent
 02732                        });
 02733                }
 02734                catch (Exception ex)
 2735                {
 02736                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02737                }
 2738            }
 02739        }
 2740
 2741        /// <summary>
 2742        /// Retrieves the item.
 2743        /// </summary>
 2744        /// <param name="id">The id.</param>
 2745        /// <returns>BaseItem.</returns>
 2746        public BaseItem RetrieveItem(Guid id)
 2747        {
 1262748            return _itemRepository.RetrieveItem(id);
 2749        }
 2750
 2751        public List<Folder> GetCollectionFolders(BaseItem item)
 2752        {
 7592753            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2754        }
 2755
 2756        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2757        {
 7752758            while (item is not null)
 2759            {
 7752760                var parent = item.GetParent();
 2761
 7752762                if (parent is AggregateFolder)
 2763                {
 2764                    break;
 2765                }
 2766
 6912767                if (parent is null)
 2768                {
 6752769                    var owner = item.GetOwner();
 2770
 6752771                    if (owner is null)
 2772                    {
 2773                        break;
 2774                    }
 2775
 02776                    item = owner;
 2777                }
 2778                else
 2779                {
 162780                    item = parent;
 2781                }
 2782            }
 2783
 7592784            if (item is null)
 2785            {
 02786                return [];
 2787            }
 2788
 7592789            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2790        }
 2791
 2792        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2793        {
 7592794            return allUserRootChildren
 7592795                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 7592796                .ToList();
 2797        }
 2798
 2799        public LibraryOptions GetLibraryOptions(BaseItem item)
 2800        {
 4902801            if (item is CollectionFolder collectionFolder)
 2802            {
 432803                return collectionFolder.GetLibraryOptions();
 2804            }
 2805
 2806            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 4472807            return GetCollectionFolders(item)
 4472808                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 4472809                ? collectionFolder2.GetLibraryOptions()
 4472810                : new LibraryOptions();
 2811        }
 2812
 2813        public CollectionType? GetContentType(BaseItem item)
 2814        {
 622815            var configuredContentType = GetConfiguredContentType(item, false);
 622816            if (configuredContentType is not null)
 2817            {
 02818                return configuredContentType;
 2819            }
 2820
 622821            configuredContentType = GetConfiguredContentType(item, true);
 622822            if (configuredContentType is not null)
 2823            {
 02824                return configuredContentType;
 2825            }
 2826
 622827            return GetInheritedContentType(item);
 2828        }
 2829
 2830        public CollectionType? GetInheritedContentType(BaseItem item)
 2831        {
 622832            var type = GetTopFolderContentType(item);
 2833
 622834            if (type is not null)
 2835            {
 02836                return type;
 2837            }
 2838
 622839            return item.GetParents()
 622840                .Select(GetConfiguredContentType)
 622841                .LastOrDefault(i => i is not null);
 2842        }
 2843
 2844        public CollectionType? GetConfiguredContentType(BaseItem item)
 2845        {
 02846            return GetConfiguredContentType(item, false);
 2847        }
 2848
 2849        public CollectionType? GetConfiguredContentType(string path)
 2850        {
 02851            return GetContentTypeOverride(path, false);
 2852        }
 2853
 2854        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2855        {
 1242856            if (item is ICollectionFolder collectionFolder)
 2857            {
 02858                return collectionFolder.CollectionType;
 2859            }
 2860
 1242861            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2862        }
 2863
 2864        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2865        {
 1422866            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1422867                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1422868                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1422869                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1422870            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2871            {
 02872                return collectionType;
 2873            }
 2874
 1422875            return null;
 2876        }
 2877
 2878        private CollectionType? GetTopFolderContentType(BaseItem item)
 2879        {
 1812880            if (item is null)
 2881            {
 912882                return null;
 2883            }
 2884
 902885            while (!item.ParentId.IsEmpty())
 2886            {
 02887                var parent = item.GetParent();
 02888                if (parent is null || parent is AggregateFolder)
 2889                {
 2890                    break;
 2891                }
 2892
 02893                item = parent;
 2894            }
 2895
 902896            return GetUserRootFolder().Children
 902897                .OfType<ICollectionFolder>()
 902898                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 902899                .Select(i => i.CollectionType)
 902900                .FirstOrDefault(i => i is not null);
 2901        }
 2902
 2903        public UserView GetNamedView(
 2904            User user,
 2905            string name,
 2906            CollectionType? viewType,
 2907            string sortName)
 2908        {
 02909            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2910        }
 2911
 2912        public UserView GetNamedView(
 2913            string name,
 2914            CollectionType viewType,
 2915            string sortName)
 2916        {
 02917            var path = Path.Combine(
 02918                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02919                "views",
 02920                _fileSystem.GetValidFilename(viewType.ToString()));
 2921
 02922            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2923
 02924            var item = GetItemById(id) as UserView;
 2925
 02926            var refresh = false;
 2927
 02928            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2929            {
 02930                var info = Directory.CreateDirectory(path);
 02931                item = new UserView
 02932                {
 02933                    Path = path,
 02934                    Id = id,
 02935                    DateCreated = info.CreationTimeUtc,
 02936                    DateModified = info.LastWriteTimeUtc,
 02937                    Name = name,
 02938                    ViewType = viewType,
 02939                    ForcedSortName = sortName
 02940                };
 2941
 02942                CreateItem(item, null);
 2943
 02944                refresh = true;
 2945            }
 2946
 02947            if (refresh)
 2948            {
 02949                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02950                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2951            }
 2952
 02953            return item;
 2954        }
 2955
 2956        public UserView GetNamedView(
 2957            User user,
 2958            string name,
 2959            Guid parentId,
 2960            CollectionType? viewType,
 2961            string sortName)
 2962        {
 02963            var parentIdString = parentId.IsEmpty()
 02964                ? null
 02965                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02966            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2967
 02968            var id = GetNewItemId(idValues, typeof(UserView));
 2969
 02970            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2971
 02972            var item = GetItemById(id) as UserView;
 2973
 02974            var isNew = false;
 2975
 02976            if (item is null)
 2977            {
 02978                var info = Directory.CreateDirectory(path);
 02979                item = new UserView
 02980                {
 02981                    Path = path,
 02982                    Id = id,
 02983                    DateCreated = info.CreationTimeUtc,
 02984                    DateModified = info.LastWriteTimeUtc,
 02985                    Name = name,
 02986                    ViewType = viewType,
 02987                    ForcedSortName = sortName,
 02988                    UserId = user.Id,
 02989                    DisplayParentId = parentId
 02990                };
 2991
 02992                CreateItem(item, null);
 2993
 02994                isNew = true;
 2995            }
 2996
 02997            var lastRefreshedUtc = item.DateLastRefreshed;
 02998            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2999
 03000            if (!refresh && !item.DisplayParentId.IsEmpty())
 3001            {
 03002                var displayParent = GetItemById(item.DisplayParentId);
 03003                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3004            }
 3005
 03006            if (refresh)
 3007            {
 03008                ProviderManager.QueueRefresh(
 03009                    item.Id,
 03010                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03011                    {
 03012                        // Need to force save to increment DateLastSaved
 03013                        ForceSave = true
 03014                    },
 03015                    RefreshPriority.Normal);
 3016            }
 3017
 03018            return item;
 3019        }
 3020
 3021        public UserView GetShadowView(
 3022            BaseItem parent,
 3023            CollectionType? viewType,
 3024            string sortName)
 3025        {
 03026            ArgumentNullException.ThrowIfNull(parent);
 3027
 03028            var name = parent.Name;
 03029            var parentId = parent.Id;
 3030
 03031            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 3032
 03033            var id = GetNewItemId(idValues, typeof(UserView));
 3034
 03035            var path = parent.Path;
 3036
 03037            var item = GetItemById(id) as UserView;
 3038
 03039            var isNew = false;
 3040
 03041            if (item is null)
 3042            {
 03043                var info = Directory.CreateDirectory(path);
 03044                item = new UserView
 03045                {
 03046                    Path = path,
 03047                    Id = id,
 03048                    DateCreated = info.CreationTimeUtc,
 03049                    DateModified = info.LastWriteTimeUtc,
 03050                    Name = name,
 03051                    ViewType = viewType,
 03052                    ForcedSortName = sortName,
 03053                    DisplayParentId = parentId
 03054                };
 3055
 03056                CreateItem(item, null);
 3057
 03058                isNew = true;
 3059            }
 3060
 03061            var lastRefreshedUtc = item.DateLastRefreshed;
 03062            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3063
 03064            if (!refresh && !item.DisplayParentId.IsEmpty())
 3065            {
 03066                var displayParent = GetItemById(item.DisplayParentId);
 03067                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3068            }
 3069
 03070            if (refresh)
 3071            {
 03072                ProviderManager.QueueRefresh(
 03073                    item.Id,
 03074                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03075                    {
 03076                        // Need to force save to increment DateLastSaved
 03077                        ForceSave = true
 03078                    },
 03079                    RefreshPriority.Normal);
 3080            }
 3081
 03082            return item;
 3083        }
 3084
 3085        public UserView GetNamedView(
 3086            string name,
 3087            Guid parentId,
 3088            CollectionType? viewType,
 3089            string sortName,
 3090            string uniqueId)
 3091        {
 03092            ArgumentException.ThrowIfNullOrEmpty(name);
 3093
 03094            var parentIdString = parentId.IsEmpty()
 03095                ? null
 03096                : parentId.ToString("N", CultureInfo.InvariantCulture);
 03097            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 03098            if (!string.IsNullOrEmpty(uniqueId))
 3099            {
 03100                idValues += uniqueId;
 3101            }
 3102
 03103            var id = GetNewItemId(idValues, typeof(UserView));
 3104
 03105            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 3106
 03107            var item = GetItemById(id) as UserView;
 3108
 03109            var isNew = false;
 3110
 03111            if (item is null)
 3112            {
 03113                var info = Directory.CreateDirectory(path);
 03114                item = new UserView
 03115                {
 03116                    Path = path,
 03117                    Id = id,
 03118                    DateCreated = info.CreationTimeUtc,
 03119                    DateModified = info.LastWriteTimeUtc,
 03120                    Name = name,
 03121                    ViewType = viewType,
 03122                    ForcedSortName = sortName,
 03123                    DisplayParentId = parentId
 03124                };
 3125
 03126                CreateItem(item, null);
 3127
 03128                isNew = true;
 3129            }
 3130
 03131            if (viewType != item.ViewType)
 3132            {
 03133                item.ViewType = viewType;
 03134                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 3135            }
 3136
 03137            var lastRefreshedUtc = item.DateLastRefreshed;
 03138            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3139
 03140            if (!refresh && !item.DisplayParentId.IsEmpty())
 3141            {
 03142                var displayParent = GetItemById(item.DisplayParentId);
 03143                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3144            }
 3145
 03146            if (refresh)
 3147            {
 03148                ProviderManager.QueueRefresh(
 03149                    item.Id,
 03150                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03151                    {
 03152                        // Need to force save to increment DateLastSaved
 03153                        ForceSave = true
 03154                    },
 03155                    RefreshPriority.Normal);
 3156            }
 3157
 03158            return item;
 3159        }
 3160
 3161        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 3162        {
 33163            if (parentId.HasValue)
 3164            {
 03165                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 3166            }
 3167
 33168            if (!userId.IsNullOrEmpty())
 3169            {
 33170                return GetUserRootFolder();
 3171            }
 3172
 03173            return RootFolder;
 3174        }
 3175
 3176        /// <inheritdoc />
 3177        public void QueueLibraryScan()
 3178        {
 03179            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 03180        }
 3181
 3182        /// <inheritdoc />
 3183        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 3184        {
 03185            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 03186            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 3187        }
 3188
 3189        /// <inheritdoc />
 3190        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 3191        {
 03192            var series = episode.Series;
 03193            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 03194            if (!isAbsoluteNaming.Value)
 3195            {
 3196                // In other words, no filter applied
 03197                isAbsoluteNaming = null;
 3198            }
 3199
 03200            var resolver = new EpisodeResolver(_namingOptions);
 3201
 03202            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 3203
 03204            EpisodeInfo? episodeInfo = null;
 03205            if (episode.IsFileProtocol)
 3206            {
 03207                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 3208                // Resolve from parent folder if it's not the Season folder
 03209                var parent = episode.GetParent();
 03210                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 3211                {
 03212                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 03213                    if (episodeInfo is not null)
 3214                    {
 3215                        // add the container
 03216                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 3217                    }
 3218                }
 3219            }
 3220
 03221            var changed = false;
 03222            if (episodeInfo is null)
 3223            {
 03224                return changed;
 3225            }
 3226
 03227            if (episodeInfo.IsByDate)
 3228            {
 03229                if (episode.IndexNumber.HasValue)
 3230                {
 03231                    episode.IndexNumber = null;
 03232                    changed = true;
 3233                }
 3234
 03235                if (episode.IndexNumberEnd.HasValue)
 3236                {
 03237                    episode.IndexNumberEnd = null;
 03238                    changed = true;
 3239                }
 3240
 03241                if (!episode.PremiereDate.HasValue)
 3242                {
 03243                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 3244                    {
 03245                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 3246                    }
 3247
 03248                    if (episode.PremiereDate.HasValue)
 3249                    {
 03250                        changed = true;
 3251                    }
 3252                }
 3253
 03254                if (episode.ProductionYear is null)
 3255                {
 03256                    episode.ProductionYear = episodeInfo.Year;
 3257
 03258                    if (episode.ProductionYear is not null)
 3259                    {
 03260                        changed = true;
 3261                    }
 3262                }
 3263            }
 3264            else
 3265            {
 03266                if (!episode.IndexNumber.HasValue || forceRefresh)
 3267                {
 03268                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 3269                    {
 03270                        changed = true;
 3271                    }
 3272
 03273                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 3274                }
 3275
 03276                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 3277                {
 03278                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 3279                    {
 03280                        changed = true;
 3281                    }
 3282
 03283                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 3284                }
 3285
 03286                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 3287                {
 03288                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 3289                    {
 03290                        changed = true;
 3291                    }
 3292
 03293                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 3294                }
 3295            }
 3296
 03297            if (!episode.ParentIndexNumber.HasValue)
 3298            {
 03299                var season = episode.Season;
 3300
 03301                if (season is not null)
 3302                {
 03303                    episode.ParentIndexNumber = season.IndexNumber;
 3304                }
 3305
 03306                if (episode.ParentIndexNumber.HasValue)
 3307                {
 03308                    changed = true;
 3309                }
 3310            }
 3311
 03312            return changed;
 3313        }
 3314
 3315        public ItemLookupInfo ParseName(string name)
 3316        {
 03317            var namingOptions = _namingOptions;
 03318            var result = VideoResolver.CleanDateTime(name, namingOptions);
 3319
 03320            return new ItemLookupInfo
 03321            {
 03322                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 03323                Year = result.Year
 03324            };
 3325        }
 3326
 3327        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 3328        {
 3329            // Apply .ignore rules
 143330            var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
 143331            var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.Vide
 143332            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.Containi
 143333            if (ownerVideoInfo is null)
 3334            {
 03335                return [];
 3336            }
 3337
 143338            var candidates = new List<ExtraCandidate>();
 3339
 143340            var count = filtered.Count;
 1423341            for (var i = 0; i < count; i++)
 3342            {
 573343                var current = filtered[i];
 573344                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 3345                {
 83346                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 83347                    var filesInSubFolderList = filesInSubFolder.ToList();
 3348
 83349                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 3350
 383351                    foreach (var file in filesInSubFolderList)
 3352                    {
 113353                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType, ou
 3354                        {
 3355                            continue;
 3356                        }
 3357
 103358                        AddCandidate(file, extraType.Value, extraRule, subFolderIsMixedFolder);
 3359                    }
 3360                }
 493361                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 3362                {
 263363                    AddCandidate(current, extraType.Value, extraRule, false);
 3364                }
 3365            }
 3366
 143367            var extras = new List<BaseItem>();
 143368            var typeCounters = new Dictionary<ExtraType, int>();
 3369
 3370            // Order by path so that the numbering handed out below does not depend on the
 3371            // order the file system happened to list the folder in
 1003372            foreach (var candidate in candidates.OrderBy(c => c.Extra.Path, StringComparer.Ordinal))
 3373            {
 363374                var extra = PrepareExtra(candidate);
 363375                if (extra is not null)
 3376                {
 363377                    extras.Add(extra);
 3378                }
 3379            }
 3380
 143381            return extras;
 3382
 3383            void AddCandidate(FileSystemMetadata file, ExtraType extraType, ExtraRule extraRule, bool isInMixedFolder)
 3384            {
 3385                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 3386                if (extra is Video or Audio)
 3387                {
 3388                    candidates.Add(new ExtraCandidate(extra, extraType, extraRule, isInMixedFolder));
 3389                }
 3390            }
 3391
 3392            BaseItem? PrepareExtra(ExtraCandidate candidate)
 3393            {
 3394                var resolved = candidate.Extra;
 3395                var extra = resolved;
 3396                var name = GetExtraName(candidate, ownerVideoInfo, typeCounters);
 3397
 3398                // Try to retrieve it from the db. If we don't find it, use the resolved version
 3399                var itemById = GetItemById(extra.Id);
 3400                if (itemById is not null)
 3401                {
 3402                    extra = itemById;
 3403                }
 3404
 3405                // An extra is named after its file, so the file is the source of truth. Items created
 3406                // by older versions, or renamed by a metadata provider, are corrected here;
 3407                // RefreshExtras persists the change.
 3408                if (!string.IsNullOrEmpty(name) && extra.LockedFields?.Contains(MetadataField.Name) != true)
 3409                {
 3410                    extra.Name = name;
 3411                }
 3412
 3413                // Only update extra type if it is more specific then the currently known extra type
 3414                if (extra.ExtraType is null or ExtraType.Unknown || candidate.ExtraType != ExtraType.Unknown)
 3415                {
 3416                    extra.ExtraType = candidate.ExtraType;
 3417                }
 3418
 3419                // Only return items that are actual extras (have ExtraType set)
 3420                // Note: OwnerId and ParentId are set by RefreshExtras, not here,
 3421                // so that RefreshExtras can detect when they need updating and set ForceSave.
 3422                if (extra.ExtraType is not null)
 3423                {
 3424                    extra.IsInMixedFolder = candidate.IsInMixedFolder;
 3425                    return extra;
 3426                }
 3427
 3428                return null;
 3429            }
 3430        }
 3431
 3432        /// <summary>
 3433        /// Gets the name to give an extra.
 3434        /// </summary>
 3435        /// <param name="candidate">The resolved extra.</param>
 3436        /// <param name="ownerVideoInfo">The naming info of the owner.</param>
 3437        /// <param name="typeCounters">Number of extras named after their type so far, per type.</param>
 3438        /// <returns>The name.</returns>
 3439        private string GetExtraName(ExtraCandidate candidate, VideoFileInfo ownerVideoInfo, Dictionary<ExtraType, int> t
 3440        {
 363441            var isNamedAfterOwner = candidate.ExtraRule.RuleType switch
 363442            {
 43443                ExtraRuleType.Filename => true,
 203444                ExtraRuleType.Suffix => string.Equals(candidate.Extra.Name, ownerVideoInfo.Name, StringComparison.Ordina
 123445                _ => false
 363446            };
 3447
 363448            if (!isNamedAfterOwner)
 3449            {
 193450                return candidate.Extra.Name;
 3451            }
 3452
 173453            typeCounters.TryGetValue(candidate.ExtraType, out var seen);
 173454            typeCounters[candidate.ExtraType] = seen + 1;
 3455
 173456            var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(candidate.ExtraType));
 3457
 173458            return seen == 0
 173459                ? typeName
 173460                : string.Format(
 173461                    CultureInfo.InvariantCulture,
 173462                    _localization.GetServerLocalizedString("NameExtraNumbered"),
 173463                    typeName,
 173464                    seen + 1);
 3465        }
 3466
 173467        private static string GetExtraTypeNameKey(ExtraType extraType) => extraType switch
 173468        {
 03469            ExtraType.Clip => "NameExtraClip",
 123470            ExtraType.Trailer => "NameExtraTrailer",
 33471            ExtraType.BehindTheScenes => "NameExtraBehindTheScenes",
 03472            ExtraType.DeletedScene => "NameExtraDeletedScene",
 03473            ExtraType.Interview => "NameExtraInterview",
 03474            ExtraType.Scene => "NameExtraScene",
 03475            ExtraType.Sample => "NameExtraSample",
 13476            ExtraType.ThemeSong => "NameExtraThemeSong",
 03477            ExtraType.ThemeVideo => "NameExtraThemeVideo",
 03478            ExtraType.Featurette => "NameExtraFeaturette",
 03479            ExtraType.Short => "NameExtraShort",
 13480            _ => "NameExtraUnknown"
 173481        };
 3482
 3483        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 3484        {
 123485            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 3486            {
 03487                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 3488                {
 03489                    return newPath;
 3490                }
 3491            }
 3492
 63493            return path;
 3494        }
 3495
 3496        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 3497        {
 03498            return _peopleRepository.GetPeople(query).Items;
 3499        }
 3500
 3501        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 3502        {
 63503            if (item.SupportsPeople)
 3504            {
 03505                var people = GetPeople(new InternalPeopleQuery
 03506                {
 03507                    ItemId = item.Id
 03508                });
 3509
 03510                if (people.Count > 0)
 3511                {
 03512                    return people;
 3513                }
 3514            }
 3515
 63516            return [];
 3517        }
 3518
 3519        public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
 3520        {
 03521            var queryResult = _peopleRepository.GetPeople(query);
 03522            var baseItems = queryResult.Items.Select(i =>
 03523                {
 03524                    try
 03525                    {
 03526                        return GetPerson(i.Name);
 03527                    }
 03528                    catch (Exception ex)
 03529                    {
 03530                        _logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
 03531                        return null;
 03532                    }
 03533                })
 03534                .Where(i => i is not null)
 03535                .Where(i => query.User is null || i!.IsVisible(query.User))
 03536                .OfType<BaseItem>()
 03537                .ToList()
 03538                .AsReadOnly();
 3539
 03540            return new QueryResult<BaseItem>
 03541            {
 03542                StartIndex = queryResult.StartIndex,
 03543                TotalRecordCount = queryResult.TotalRecordCount,
 03544                Items = baseItems,
 03545            };
 3546        }
 3547
 3548        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 3549        {
 03550            return _peopleRepository.GetPeopleNames(query);
 3551        }
 3552
 3553        /// <inheritdoc/>
 3554        public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IRead
 3555        {
 03556            return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
 3557        }
 3558
 3559        /// <inheritdoc/>
 3560        public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
 3561        {
 03562            return _peopleRepository.GetPeopleByItems(itemIds);
 3563        }
 3564
 3565        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 3566        {
 03567            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 03568        }
 3569
 3570        /// <inheritdoc />
 3571        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 3572        {
 03573            if (!item.SupportsPeople)
 3574            {
 03575                return;
 3576            }
 3577
 03578            if (people is not null)
 3579            {
 03580                people = people.Where(e => e is not null).ToArray();
 03581                _peopleRepository.UpdatePeople(item.Id, people);
 03582                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 3583            }
 03584        }
 3585
 3586        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 3587        {
 03588            foreach (var url in image.Path.Split('|'))
 3589            {
 3590                try
 3591                {
 03592                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 3593
 03594                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 3595
 03596                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 3597
 03598                    return item.GetImageInfo(image.Type, imageIndex);
 3599                }
 03600                catch (HttpRequestException ex)
 3601                {
 03602                    if (ex.StatusCode.HasValue
 03603                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 3604                    {
 03605                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 03606                        continue;
 3607                    }
 3608
 03609                    throw;
 3610                }
 3611            }
 3612
 03613            if (removeOnFailure)
 3614            {
 3615                // Remove this image to prevent it from retrying over and over
 03616                item.RemoveImage(image);
 03617                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 3618            }
 3619
 03620            throw new InvalidOperationException("Unable to convert any images to local");
 03621        }
 3622
 3623        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3624        {
 23625            if (string.IsNullOrWhiteSpace(name))
 3626            {
 03627                throw new ArgumentNullException(nameof(name));
 3628            }
 3629
 23630            name = _fileSystem.GetValidFilename(name.Trim());
 3631
 23632            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3633
 23634            var existingNameCount = 1; // first numbered name will be 2
 23635            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 23636            var originalName = name;
 33637            while (Directory.Exists(virtualFolderPath))
 3638            {
 13639                existingNameCount++;
 13640                name = originalName + existingNameCount;
 13641                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3642            }
 3643
 23644            var mediaPathInfos = options.PathInfos;
 23645            if (mediaPathInfos is not null)
 3646            {
 23647                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 23648                if (invalidpath is not null)
 3649                {
 03650                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3651                }
 3652            }
 3653
 23654            LibraryMonitor.Stop();
 3655
 3656            try
 3657            {
 23658                Directory.CreateDirectory(virtualFolderPath);
 3659
 23660                if (collectionType is not null)
 3661                {
 03662                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3663
 03664                    FileHelper.CreateEmpty(path);
 3665                }
 3666
 23667                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3668
 23669                if (mediaPathInfos is not null)
 3670                {
 43671                    foreach (var path in mediaPathInfos)
 3672                    {
 03673                        AddMediaPathInternal(name, path, false);
 3674                    }
 3675                }
 3676            }
 3677            finally
 3678            {
 23679                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3680
 23681                if (refreshLibrary)
 3682                {
 23683                    StartScanInBackground();
 3684                }
 3685                else
 3686                {
 3687                    // Need to add a delay here or directory watchers may still pick up the changes
 03688                    await Task.Delay(1000).ConfigureAwait(false);
 03689                    LibraryMonitor.Start();
 3690                }
 3691            }
 23692        }
 3693
 3694        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3695        {
 03696            foreach (var person in people)
 3697            {
 03698                cancellationToken.ThrowIfCancellationRequested();
 3699
 03700                var itemUpdateType = ItemUpdateType.MetadataDownload;
 03701                var saveEntity = false;
 03702                var createEntity = false;
 03703                var personEntity = GetPerson(person.Name);
 3704
 03705                if (personEntity is null)
 3706                {
 3707                    try
 3708                    {
 03709                        var path = Person.GetPath(person.Name);
 03710                        var info = Directory.CreateDirectory(path);
 03711                        personEntity = new Person()
 03712                        {
 03713                            Name = person.Name,
 03714                            Id = GetItemByNameId<Person>(path),
 03715                            DateCreated = info.CreationTimeUtc,
 03716                            DateModified = info.LastWriteTimeUtc,
 03717                            Path = path
 03718                        };
 3719
 03720                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 03721                        saveEntity = true;
 03722                        createEntity = true;
 03723                    }
 03724                    catch (Exception ex)
 3725                    {
 03726                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 03727                        continue;
 3728                    }
 3729                }
 3730
 03731                foreach (var id in person.ProviderIds)
 3732                {
 03733                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3734                    {
 03735                        personEntity.SetProviderId(id.Key, id.Value);
 03736                        saveEntity = true;
 3737                    }
 3738                }
 3739
 03740                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3741                {
 03742                    personEntity.SetImage(
 03743                        new ItemImageInfo
 03744                        {
 03745                            Path = person.ImageUrl,
 03746                            Type = ImageType.Primary
 03747                        },
 03748                        0);
 3749
 03750                    saveEntity = true;
 03751                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3752                }
 3753
 03754                if (saveEntity)
 3755                {
 03756                    if (createEntity)
 3757                    {
 03758                        CreateItems([personEntity], null, CancellationToken.None);
 3759                    }
 3760
 03761                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 03762                    personEntity.DateLastSaved = DateTime.UtcNow;
 3763
 03764                    CreateItems([personEntity], null, CancellationToken.None);
 3765                }
 03766            }
 03767        }
 3768
 3769        private void StartScanInBackground()
 3770        {
 33771            Task.Run(() =>
 33772            {
 33773                // No need to start if scanning the library because it will handle it
 33774                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33775            });
 33776        }
 3777
 3778        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3779        {
 13780            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03781        }
 3782
 3783        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3784        {
 13785            ArgumentNullException.ThrowIfNull(pathInfo);
 3786
 13787            var path = pathInfo.Path;
 3788
 13789            if (string.IsNullOrWhiteSpace(path))
 3790            {
 03791                throw new ArgumentException(nameof(path));
 3792            }
 3793
 13794            if (!Directory.Exists(path))
 3795            {
 13796                throw new FileNotFoundException("The path does not exist.");
 3797            }
 3798
 03799            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03800            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3801
 03802            CreateShortcut(virtualFolderPath, pathInfo);
 3803
 03804            if (saveLibraryOptions)
 3805            {
 03806                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3807
 03808                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3809
 03810                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3811
 03812                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3813            }
 03814        }
 3815
 3816        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3817        {
 03818            ArgumentNullException.ThrowIfNull(mediaPath);
 3819
 03820            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03821            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3822
 03823            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3824
 03825            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3826
 03827            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03828        }
 3829
 3830        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3831        {
 03832            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03833            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3834
 03835            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3836            {
 03837                var list = options.PathInfos.ToList();
 3838
 03839                foreach (var location in info.Locations)
 3840                {
 03841                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3842                    {
 03843                        list.Add(new MediaPathInfo(location));
 3844                    }
 3845                }
 3846
 03847                options.PathInfos = list.ToArray();
 3848            }
 03849        }
 3850
 3851        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3852        {
 23853            if (string.IsNullOrWhiteSpace(name))
 3854            {
 03855                throw new ArgumentNullException(nameof(name));
 3856            }
 3857
 23858            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3859
 23860            var path = Path.Combine(rootFolderPath, name);
 3861
 23862            if (!Directory.Exists(path))
 3863            {
 13864                throw new FileNotFoundException("The media folder does not exist");
 3865            }
 3866
 13867            LibraryMonitor.Stop();
 3868
 3869            try
 3870            {
 13871                Directory.Delete(path, true);
 3872            }
 3873            finally
 3874            {
 13875                CollectionFolder.OnCollectionFolderChange();
 3876
 13877                if (refreshLibrary)
 3878                {
 13879                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3880
 13881                    StartScanInBackground();
 3882                }
 3883                else
 3884                {
 3885                    // Need to add a delay here or directory watchers may still pick up the changes
 03886                    await Task.Delay(1000).ConfigureAwait(false);
 03887                    LibraryMonitor.Start();
 3888                }
 3889            }
 13890        }
 3891
 3892        private void RemoveContentTypeOverrides(string path)
 3893        {
 03894            if (string.IsNullOrWhiteSpace(path))
 3895            {
 03896                throw new ArgumentNullException(nameof(path));
 3897            }
 3898
 03899            List<NameValuePair>? removeList = null;
 3900
 03901            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3902            {
 03903                if (string.IsNullOrWhiteSpace(contentType.Name)
 03904                    || _fileSystem.AreEqual(path, contentType.Name)
 03905                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3906                {
 03907                    (removeList ??= new()).Add(contentType);
 3908                }
 3909            }
 3910
 03911            if (removeList is not null)
 3912            {
 03913                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03914                    .Except(removeList)
 03915                    .ToArray();
 3916
 03917                _configurationManager.SaveConfiguration();
 3918            }
 03919        }
 3920
 3921        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3922        {
 13923            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3924
 13925            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13926            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3927
 13928            if (!Directory.Exists(virtualFolderPath))
 3929            {
 13930                throw new FileNotFoundException(
 13931                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3932            }
 3933
 03934            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03935                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03936                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3937
 03938            if (!string.IsNullOrEmpty(shortcut))
 3939            {
 03940                _fileSystem.DeleteFile(shortcut);
 3941            }
 3942
 03943            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3944
 03945            libraryOptions.PathInfos = libraryOptions
 03946                .PathInfos
 03947                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03948                .ToArray();
 3949
 03950            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03951        }
 3952
 3953        private static bool ItemIsVisible(BaseItem? item, User? user)
 3954        {
 223955            if (item is null)
 3956            {
 223957                return false;
 3958            }
 3959
 03960            if (user is null)
 3961            {
 03962                return true;
 3963            }
 3964
 03965            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3966        }
 3967
 3968        public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
 3969        {
 03970            var path = pathInfo.Path;
 03971            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3972
 03973            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3974
 03975            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3976
 03977            while (File.Exists(lnk))
 3978            {
 03979                shortcutFilename += "1";
 03980                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3981            }
 3982
 03983            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 03984            RemoveContentTypeOverrides(path);
 03985        }
 3986
 3987        /// <inheritdoc />
 3988        public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
 3989        {
 03990            var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
 3991
 3992            // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
 03993            foreach (var parentId in affectedParentIds)
 3994            {
 03995                if (GetItemById(parentId) is Folder parent)
 3996                {
 03997                    foreach (var lc in parent.LinkedChildren)
 3998                    {
 03999                        if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
 4000                        {
 04001                            lc.ItemId = toChildId;
 4002                        }
 4003                    }
 4004
 04005                    await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
 4006                }
 4007            }
 04008        }
 4009
 4010        /// <inheritdoc />
 4011        public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
 4012        {
 04013            if (query.User is not null)
 4014            {
 04015                AddUserToQuery(query, query.User);
 4016            }
 4017
 04018            SetTopParentOrAncestorIds(query);
 04019            return _itemRepository.GetQueryFiltersLegacy(query);
 4020        }
 4021
 4022        /// <inheritdoc />
 4023        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
 4024        {
 04025            return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
 4026        }
 4027
 4028        /// <inheritdoc />
 4029        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType, InternalItemsQuery query)
 4030        {
 04031            if (query.User is not null)
 4032            {
 04033                AddUserToQuery(query, query.User);
 4034            }
 4035
 04036            SetTopParentOrAncestorIds(query);
 04037            return _itemRepository.GetMediaStreamLanguages(query, mediaStreamType);
 4038        }
 4039
 4040        private sealed record ExtraCandidate(BaseItem Extra, ExtraType ExtraType, ExtraRule ExtraRule, bool IsInMixedFol
 4041    }
 4042}

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)
GetItemIdsWithAlternateVersions(System.Collections.Generic.IReadOnlyList`1<System.Guid>)
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)