< 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
45%
Covered lines: 828
Uncovered lines: 974
Coverable lines: 1802
Total lines: 4234
Line coverage: 45.9%
Branch coverage
35%
Covered branches: 337
Total branches: 946
Branch coverage: 35.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/12/2026 - 12:15:57 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: 40428/12/2026 - 12:16:50 AM Line coverage: 36.8% (633/1718) Branch coverage: 29% (268/922) Total lines: 40428/15/2026 - 12:17:03 AM Line coverage: 36.6% (630/1718) Branch coverage: 29% (268/922) Total lines: 40428/17/2026 - 12:18:27 AM Line coverage: 36.8% (633/1718) Branch coverage: 29% (268/922) Total lines: 40428/18/2026 - 12:17:16 AM Line coverage: 36.6% (630/1718) Branch coverage: 29% (268/922) Total lines: 404210/29/2026 - 12:17:11 AM Line coverage: 45.9% (828/1802) Branch coverage: 35.7% (338/946) Total lines: 423410/31/2026 - 12:17:46 AM Line coverage: 45.5% (820/1802) Branch coverage: 35.7% (338/946) Total lines: 423411/1/2026 - 12:15:27 AM Line coverage: 45.9% (828/1802) Branch coverage: 35.6% (337/946) Total lines: 4234 5/12/2026 - 12:15:57 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: 40428/12/2026 - 12:16:50 AM Line coverage: 36.8% (633/1718) Branch coverage: 29% (268/922) Total lines: 40428/15/2026 - 12:17:03 AM Line coverage: 36.6% (630/1718) Branch coverage: 29% (268/922) Total lines: 40428/17/2026 - 12:18:27 AM Line coverage: 36.8% (633/1718) Branch coverage: 29% (268/922) Total lines: 40428/18/2026 - 12:17:16 AM Line coverage: 36.6% (630/1718) Branch coverage: 29% (268/922) Total lines: 404210/29/2026 - 12:17:11 AM Line coverage: 45.9% (828/1802) Branch coverage: 35.7% (338/946) Total lines: 423410/31/2026 - 12:17:46 AM Line coverage: 45.5% (820/1802) Branch coverage: 35.7% (338/946) Total lines: 423411/1/2026 - 12:15:27 AM Line coverage: 45.9% (828/1802) Branch coverage: 35.6% (337/946) Total lines: 4234

Coverage delta

Coverage delta 10 -10

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(...)60%141066.66%
DeleteItem(...)100%210%
DeleteItem(...)0%620%
DeleteItemsUnsafeFast(...)66.66%441856.66%
DeleteItem(...)0%4970700%
DeleteItemPath(...)0%272160%
IsInternalItem(...)0%342180%
GetMetadataPaths(...)0%620%
GetInternalMetadataPaths(...)75%8886.66%
ResolveItem(...)100%44100%
Resolve(...)100%1140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)100%66100%
ResolvePath(...)50%22100%
SetAdditionalPartsFromStack(...)0%156120%
ResolveAlternateVersion(...)35.71%221465.51%
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%
GetPersonId(...)100%210%
GetPerson(...)50%2280%
GetOrCreatePerson(...)0%620%
GetStudio(...)100%210%
GetStudioId(...)100%210%
GetGenreId(...)100%210%
GetMusicGenreId(...)100%210%
GetGenre(...)100%210%
GetMusicGenre(...)100%210%
GetYear(...)0%620%
GetArtist(...)100%11100%
GetArtists(...)100%11100%
GetArtist(...)100%11100%
CreateItemByName(...)50%19844.44%
GetItemByNameId(...)100%11100%
ValidateMediaLibrary(...)100%11100%
ValidateMediaLibraryInternal()100%11100%
ValidateTopLibraryFolders()100%88100%
ClearIgnoreRuleCache()100%11100%
PerformLibraryValidation()100%11100%
RunPostScanTasks()100%2285.71%
GetVirtualFolders()100%11100%
GetVirtualFolders(...)100%22100%
GetVirtualFolderInfo(...)50%101097.14%
GetCollectionType(...)50%6450%
GetItemById(...)83.33%6687.5%
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%
GetItemCountsForNameItems(...)0%620%
GetChildCountBatch(...)100%11100%
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(...)71.42%151483.33%
SetAncestorIds(...)50%2275%
AddUserToQuery(...)60%202095%
ConfigureUserAccess(...)100%210%
GetTopParentIdsForQuery(...)8.33%4342410.71%
GetIntros()0%620%
GetIntros()100%210%
ResolveIntro(...)0%110100%
GetLocalAlternateVersionIds(...)50%2280%
GetLinkedAlternateVersions(...)50%3244.44%
GetItemIdsWithAlternateVersions(...)100%210%
UpsertLinkedChild(...)100%210%
Sort(...)50%341453.33%
Sort(...)57.14%281458.82%
GetComparer(...)75%6450%
CreateItem(...)100%11100%
CreateItems(...)63.33%813061.53%
ImageNeedsRefresh(...)0%156120%
UpdateImagesAsync()31.25%1521619.04%
UpdateItemsAsync()60%663065.95%
UpdateItemAsync(...)100%11100%
ForgetDroppedLocalAlternateVersions(...)66.66%6680%
ReattachUserDataAsync()100%11100%
RunMetadataSavers()100%22100%
ReportItemRemoved(...)50%4215.38%
RetrieveItem(...)100%11100%
GetCollectionFolders(...)100%11100%
GetCollectionFolders(...)80%111081.81%
GetCollectionFoldersInternal(...)100%11100%
GetLibraryOptions(...)100%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%7280%
GetNamedView(...)28.57%141486.84%
GetNamedViewId(...)75%88100%
GetShadowView(...)33.33%121294.59%
GetShadowViewId(...)100%44100%
GetNamedView(...)0%702260%
GetParentItem(...)33.33%8660%
QueueLibraryScan()100%11100%
GetSeasonNumberFromPath(...)0%2040%
FillMissingEpisodeNumbersFromPath(...)0%3192560%
ParseName(...)0%620%
FindExtras(...)84.61%262696.15%
GetExtraName(...)100%66100%
GetNumberedExtraName(...)100%22100%
GetExtraTypeNameKey(...)33.33%341246.66%
GetPathAfterNetworkSubstitution(...)25%6450%
GetPeople(...)100%210%
GetPeople(...)50%11425%
GetPeopleItems(...)100%210%
GetPeopleNames(...)100%210%
DeleteOrphanedCredits()100%210%
GetPeopleNamesByItems(...)100%210%
GetPeopleByItems(...)100%210%
UpdatePeople(...)100%210%
UpdatePeopleAsync()0%2040%
ConvertImageToLocal()0%156120%
AddVirtualFolder()77.77%181890.62%
SavePeopleMetadataAsync()0%210140%
StartScanInBackground()100%22100%
AddMediaPath(...)100%1150%
AddMediaPathInternal(...)25%31829.41%
UpdateMediaPath(...)0%620%
SyncLibraryOptionsToLocations(...)0%7280%
RemoveVirtualFolder()80%101093.75%
RemoveContentTypeOverrides(...)0%210140%
RemoveMediaPath(...)33.33%18631.57%
ItemIsVisible(...)16.66%14640%
CreateShortcut(...)0%620%
RerouteLinkedChildReferencesAsync()0%110100%
GetQueryFiltersLegacy(...)0%620%
GetTagNames(...)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.Diagnostics;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Net;
 11using System.Net.Http;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using BitFaster.Caching.Lru;
 15using Emby.Naming.Common;
 16using Emby.Naming.TV;
 17using Emby.Naming.Video;
 18using Emby.Server.Implementations.Library.Resolvers;
 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.Persistence;
 39using MediaBrowser.Controller.Playlists;
 40using MediaBrowser.Controller.Providers;
 41using MediaBrowser.Controller.Resolvers;
 42using MediaBrowser.Controller.Sorting;
 43using MediaBrowser.Model.Configuration;
 44using MediaBrowser.Model.Drawing;
 45using MediaBrowser.Model.Dto;
 46using MediaBrowser.Model.Entities;
 47using MediaBrowser.Model.Globalization;
 48using MediaBrowser.Model.IO;
 49using MediaBrowser.Model.Library;
 50using MediaBrowser.Model.Querying;
 51using MediaBrowser.Model.Tasks;
 52using Microsoft.Extensions.Logging;
 53using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 54using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 55using Genre = MediaBrowser.Controller.Entities.Genre;
 56using Person = MediaBrowser.Controller.Entities.Person;
 57using VideoResolver = Emby.Naming.Video.VideoResolver;
 58
 59namespace Emby.Server.Implementations.Library
 60{
 61    /// <summary>
 62    /// Class LibraryManager.
 63    /// </summary>
 64    public class LibraryManager : ILibraryManager
 65    {
 66        private const string ShortcutFileExtension = ".mblink";
 67
 68        private readonly ILogger<LibraryManager> _logger;
 69        private readonly ITaskManager _taskManager;
 70        private readonly IUserManager _userManager;
 71        private readonly IUserDataManager _userDataManager;
 72        private readonly IServerConfigurationManager _configurationManager;
 73        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 74        private readonly Lazy<IProviderManager> _providerManagerFactory;
 75        private readonly Lazy<IUserViewManager> _userViewManagerFactory;
 76        private readonly IServerApplicationHost _appHost;
 77        private readonly IFileSystem _fileSystem;
 78        private readonly IItemRepository _itemRepository;
 79        private readonly IItemPersistenceService _persistenceService;
 80        private readonly INextUpService _nextUpService;
 81        private readonly IItemCountService _countService;
 82        private readonly ILinkedChildrenService _linkedChildrenService;
 83        private readonly IImageProcessor _imageProcessor;
 84        private readonly NamingOptions _namingOptions;
 85        private readonly IPeopleRepository _peopleRepository;
 86        private readonly ExtraResolver _extraResolver;
 87        private readonly IPathManager _pathManager;
 88        private readonly ILocalizationManager _localization;
 89        private readonly IDirectoryService _directoryService;
 90        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 91        private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
 92        private readonly IMediaStreamRepository _mediaStreamRepository;
 93        private readonly Lazy<IExternalDataManager> _externalDataManagerFactory;
 94
 95        /// <summary>
 96        /// The _root folder sync lock.
 97        /// </summary>
 5898        private readonly Lock _rootFolderSyncLock = new();
 5899        private readonly Lock _userRootFolderSyncLock = new();
 100
 58101        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 102
 103        /// <summary>
 104        /// The _root folder.
 105        /// </summary>
 106        private volatile AggregateFolder? _rootFolder;
 107        private volatile UserRootFolder? _userRootFolder;
 108
 109        private bool _wizardCompleted;
 110
 111        /// <summary>
 112        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 113        /// </summary>
 114        /// <param name="appHost">The application host.</param>
 115        /// <param name="loggerFactory">The logger factory.</param>
 116        /// <param name="taskManager">The task manager.</param>
 117        /// <param name="userManager">The user manager.</param>
 118        /// <param name="configurationManager">The configuration manager.</param>
 119        /// <param name="userDataManager">The user data manager.</param>
 120        /// <param name="libraryMonitorFactory">The library monitor.</param>
 121        /// <param name="fileSystem">The file system.</param>
 122        /// <param name="providerManagerFactory">The provider manager.</param>
 123        /// <param name="userViewManagerFactory">The user view manager.</param>
 124        /// <param name="itemRepository">The item repository.</param>
 125        /// <param name="persistenceService">The item persistence service.</param>
 126        /// <param name="nextUpService">The next up service.</param>
 127        /// <param name="countService">The item count service.</param>
 128        /// <param name="linkedChildrenService">The linked children service.</param>
 129        /// <param name="imageProcessor">The image processor.</param>
 130        /// <param name="namingOptions">The naming options.</param>
 131        /// <param name="directoryService">The directory service.</param>
 132        /// <param name="peopleRepository">The people repository.</param>
 133        /// <param name="pathManager">The path manager.</param>
 134        /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
 135        /// <param name="localization">The localization manager.</param>
 136        /// <param name="mediaStreamRepository">The media stream repository.</param>
 137        /// <param name="externalDataManagerFactory">The external data manager (lazy, to break the DI cycle through Chap
 138        public LibraryManager(
 139            IServerApplicationHost appHost,
 140            ILoggerFactory loggerFactory,
 141            ITaskManager taskManager,
 142            IUserManager userManager,
 143            IServerConfigurationManager configurationManager,
 144            IUserDataManager userDataManager,
 145            Lazy<ILibraryMonitor> libraryMonitorFactory,
 146            IFileSystem fileSystem,
 147            Lazy<IProviderManager> providerManagerFactory,
 148            Lazy<IUserViewManager> userViewManagerFactory,
 149            IItemRepository itemRepository,
 150            IItemPersistenceService persistenceService,
 151            INextUpService nextUpService,
 152            IItemCountService countService,
 153            ILinkedChildrenService linkedChildrenService,
 154            IImageProcessor imageProcessor,
 155            NamingOptions namingOptions,
 156            IDirectoryService directoryService,
 157            IPeopleRepository peopleRepository,
 158            IPathManager pathManager,
 159            DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
 160            ILocalizationManager localization,
 161            IMediaStreamRepository mediaStreamRepository,
 162            Lazy<IExternalDataManager> externalDataManagerFactory)
 163        {
 58164            _appHost = appHost;
 58165            _logger = loggerFactory.CreateLogger<LibraryManager>();
 58166            _taskManager = taskManager;
 58167            _userManager = userManager;
 58168            _configurationManager = configurationManager;
 58169            _userDataManager = userDataManager;
 58170            _libraryMonitorFactory = libraryMonitorFactory;
 58171            _fileSystem = fileSystem;
 58172            _providerManagerFactory = providerManagerFactory;
 58173            _userViewManagerFactory = userViewManagerFactory;
 58174            _itemRepository = itemRepository;
 58175            _persistenceService = persistenceService;
 58176            _nextUpService = nextUpService;
 58177            _countService = countService;
 58178            _linkedChildrenService = linkedChildrenService;
 58179            _imageProcessor = imageProcessor;
 180
 58181            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 182
 58183            _namingOptions = namingOptions;
 58184            _peopleRepository = peopleRepository;
 58185            _pathManager = pathManager;
 58186            _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
 58187            _localization = localization;
 58188            _directoryService = directoryService;
 58189            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 190
 58191            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 192
 58193            _mediaStreamRepository = mediaStreamRepository;
 58194            _externalDataManagerFactory = externalDataManagerFactory;
 195
 58196            RecordConfigurationValues(_configurationManager.Configuration);
 58197        }
 198
 199        /// <summary>
 200        /// Occurs when [item added].
 201        /// </summary>
 202        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 203
 204        /// <summary>
 205        /// Occurs when [item updated].
 206        /// </summary>
 207        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 208
 209        /// <summary>
 210        /// Occurs when [item removed].
 211        /// </summary>
 212        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 213
 214        /// <summary>
 215        /// Gets the root folder.
 216        /// </summary>
 217        /// <value>The root folder.</value>
 218        public AggregateFolder RootFolder
 219        {
 220            get
 221            {
 270222                if (_rootFolder is null)
 27223                {
 224                    lock (_rootFolderSyncLock)
 225                    {
 27226                        _rootFolder ??= CreateRootFolder();
 27227                    }
 228                }
 229
 270230                return _rootFolder;
 231            }
 232        }
 233
 56234        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 235
 153236        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 237
 1238        private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
 239
 240        /// <summary>
 241        /// Gets or sets the postscan tasks.
 242        /// </summary>
 243        /// <value>The postscan tasks.</value>
 244        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 245
 246        /// <summary>
 247        /// Gets or sets the intro providers.
 248        /// </summary>
 249        /// <value>The intro providers.</value>
 250        private IIntroProvider[] IntroProviders { get; set; } = [];
 251
 252        /// <summary>
 253        /// Gets or sets the list of entity resolution ignore rules.
 254        /// </summary>
 255        /// <value>The entity resolution ignore rules.</value>
 256        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 257
 258        /// <summary>
 259        /// Gets or sets the list of currently registered entity resolvers.
 260        /// </summary>
 261        /// <value>The entity resolvers enumerable.</value>
 262        private IItemResolver[] EntityResolvers { get; set; } = [];
 263
 264        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 265
 266        /// <summary>
 267        /// Gets or sets the comparers.
 268        /// </summary>
 269        /// <value>The comparers.</value>
 270        private IBaseItemComparer[] Comparers { get; set; } = [];
 271
 272        public bool IsScanRunning { get; private set; }
 273
 274        /// <summary>
 275        /// Adds the parts.
 276        /// </summary>
 277        /// <param name="rules">The rules.</param>
 278        /// <param name="resolvers">The resolvers.</param>
 279        /// <param name="introProviders">The intro providers.</param>
 280        /// <param name="itemComparers">The item comparers.</param>
 281        /// <param name="postScanTasks">The post scan tasks.</param>
 282        public void AddParts(
 283            IEnumerable<IResolverIgnoreRule> rules,
 284            IEnumerable<IItemResolver> resolvers,
 285            IEnumerable<IIntroProvider> introProviders,
 286            IEnumerable<IBaseItemComparer> itemComparers,
 287            IEnumerable<ILibraryPostScanTask> postScanTasks)
 288        {
 50289            EntityResolutionIgnoreRules = rules.ToArray();
 50290            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 50291            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 50292            IntroProviders = introProviders.ToArray();
 50293            Comparers = itemComparers.ToArray();
 50294            PostScanTasks = postScanTasks.ToArray();
 50295        }
 296
 297        /// <summary>
 298        /// Records the configuration values.
 299        /// </summary>
 300        /// <param name="configuration">The configuration.</param>
 301        private void RecordConfigurationValues(ServerConfiguration configuration)
 302        {
 183303            _wizardCompleted = configuration.IsStartupWizardCompleted;
 183304        }
 305
 306        /// <summary>
 307        /// Configurations the updated.
 308        /// </summary>
 309        /// <param name="sender">The sender.</param>
 310        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 311        private void ConfigurationUpdated(object? sender, EventArgs e)
 312        {
 125313            var config = _configurationManager.Configuration;
 314
 125315            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 316
 125317            RecordConfigurationValues(config);
 318
 125319            if (wizardChanged)
 320            {
 17321                QueueLibraryScan();
 322            }
 125323        }
 324
 325        public void RegisterItem(BaseItem item)
 326        {
 305327            ArgumentNullException.ThrowIfNull(item);
 328
 305329            if (item is IItemByName)
 330            {
 0331                if (item is not MusicArtist)
 332                {
 0333                    return;
 334                }
 335            }
 305336            else if (!item.IsFolder)
 337            {
 9338                if (item is not Video && item is not LiveTvChannel)
 339                {
 0340                    return;
 341                }
 342            }
 343
 305344            _cache.AddOrUpdate(item.Id, item);
 305345        }
 346
 347        public void DeleteItem(BaseItem item, DeleteOptions options)
 348        {
 0349            DeleteItem(item, options, false);
 0350        }
 351
 352        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 353        {
 0354            ArgumentNullException.ThrowIfNull(item);
 355
 0356            var parent = item.GetOwner() ?? item.GetParent();
 357
 0358            DeleteItem(item, options, parent, notifyParentItem);
 0359        }
 360
 361        public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
 362        {
 62363            if (items.Count == 0)
 364            {
 60365                return;
 366            }
 367
 2368            var pathMaps = items.Select(e =>
 2369                (Item: e,
 2370                InternalPath: GetInternalMetadataPaths(e),
 2371                DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
 372
 8373            foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
 374            {
 16375                foreach (var metadataPath in internalPaths)
 376                {
 6377                    if (!Directory.Exists(metadataPath))
 378                    {
 379                        continue;
 380                    }
 381
 0382                    _logger.LogDebug(
 0383                        "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0384                        item.GetType().Name,
 0385                        item.Name ?? "Unknown name",
 0386                        metadataPath,
 0387                        item.Id);
 388
 389                    try
 390                    {
 0391                        Directory.Delete(metadataPath, true);
 0392                    }
 0393                    catch (Exception ex)
 394                    {
 0395                        _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0396                    }
 397                }
 398
 4399                foreach (var fileSystemInfo in pathsToDelete)
 400                {
 0401                    DeleteItemPath(item, false, fileSystemInfo);
 402                }
 403            }
 404
 2405            var externalDataManager = _externalDataManagerFactory.Value;
 8406            foreach (var (item, _, _) in pathMaps)
 407            {
 2408                externalDataManager.DeleteExternalItemFiles(item);
 409            }
 410
 2411            _persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
 412
 413            // Evict the deleted items from the cache and announce each removal.
 8414            foreach (var (item, _, _) in pathMaps)
 415            {
 2416                _cache.TryRemove(item.Id, out _);
 2417                ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
 418            }
 2419        }
 420
 421        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 422        {
 0423            ArgumentNullException.ThrowIfNull(item);
 424
 0425            if (item.SourceType == SourceType.Channel)
 426            {
 0427                if (options.DeleteFromExternalProvider)
 428                {
 429                    try
 430                    {
 0431                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0432                    }
 0433                    catch (ArgumentException)
 434                    {
 435                        // channel no longer installed
 0436                    }
 437                }
 438
 0439                options.DeleteFileLocation = false;
 440            }
 441
 0442            if (item is LiveTvProgram)
 443            {
 0444                _logger.LogDebug(
 0445                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0446                    item.GetType().Name,
 0447                    item.Name ?? "Unknown name",
 0448                    item.Path ?? string.Empty,
 0449                    item.Id);
 450            }
 451            else
 452            {
 0453                _logger.LogInformation(
 0454                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0455                    item.GetType().Name,
 0456                    item.Name ?? "Unknown name",
 0457                    item.Path ?? string.Empty,
 0458                    item.Id);
 459            }
 460
 461            // If deleting a primary version video, clear PrimaryVersionId from alternate versions
 462            // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
 0463            if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
 464            {
 0465                var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
 0466                var allAlternateVersions = localAlternateIds
 0467                    .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
 0468                    .Distinct()
 0469                    .Select(id => GetItemById(id))
 0470                    .OfType<Video>()
 0471                    .ToList();
 472
 473                // Partition alternates by whether their files still exist on disk
 0474                var alternateVersions = new List<Video>();
 0475                var missingAlternates = new List<Video>();
 0476                foreach (var alt in allAlternateVersions)
 477                {
 0478                    if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
 479                    {
 0480                        missingAlternates.Add(alt);
 481                    }
 482                    else
 483                    {
 0484                        alternateVersions.Add(alt);
 485                    }
 486                }
 487
 488                // Delete alternates whose files no longer exist to avoid ghost items.
 489                // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
 0490                foreach (var missing in missingAlternates)
 491                {
 0492                    _logger.LogInformation(
 0493                        "Deleting missing alternate version {Name} ({Path})",
 0494                        missing.Name ?? "Unknown name",
 0495                        missing.Path ?? string.Empty);
 0496                    missing.SetPrimaryVersionId(null);
 0497                    missing.OwnerId = Guid.Empty;
 0498                    missing.LocalAlternateVersions = [];
 0499                    missing.LinkedAlternateVersions = [];
 0500                    DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
 501                }
 502
 0503                if (alternateVersions.Count > 0)
 504                {
 0505                    _logger.LogInformation(
 0506                        "Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
 0507                        alternateVersions.Count,
 0508                        item.Name ?? "Unknown name");
 509
 510                    // Promote the first alternate version to be the new primary
 0511                    var newPrimary = alternateVersions[0];
 0512                    newPrimary.SetPrimaryVersionId(null);
 0513                    newPrimary.OwnerId = Guid.Empty;
 514
 515                    // Transfer alternate version arrays from old primary to new primary
 516                    // so UpdateToRepositoryAsync creates correct LinkedChildren entries
 0517                    newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
 0518                        .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
 0519                        .ToArray();
 0520                    newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
 0521                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
 0522                        .ToArray();
 523
 0524                    newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter()
 525
 526                    // Re-route playlist/collection references from deleted primary to new primary
 0527                    RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
 528
 529                    // Update remaining alternates to point to new primary
 0530                    foreach (var alternate in alternateVersions.Skip(1))
 531                    {
 0532                        alternate.SetPrimaryVersionId(newPrimary.Id);
 533                        // Only set OwnerId for local alternates; linked alternates are independent items
 0534                        alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
 0535                        alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaite
 536                    }
 537                }
 538            }
 0539            else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
 540            {
 541                // If deleting an alternate version, re-route references to its primary
 0542                RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter()
 543
 544                // Remove deleted alternate from primary's LinkedAlternateVersions
 0545                if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
 546                {
 0547                    primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
 0548                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
 0549                        .ToArray();
 0550                    primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter
 551                }
 552            }
 553
 0554            var children = item.IsFolder
 0555                ? ((Folder)item).GetRecursiveChildren(false)
 0556                : [];
 557
 0558            foreach (var metadataPath in GetMetadataPaths(item, children))
 559            {
 0560                if (!Directory.Exists(metadataPath))
 561                {
 562                    continue;
 563                }
 564
 0565                _logger.LogDebug(
 0566                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0567                    item.GetType().Name,
 0568                    item.Name ?? "Unknown name",
 0569                    metadataPath,
 0570                    item.Id);
 571
 572                try
 573                {
 0574                    Directory.Delete(metadataPath, true);
 0575                }
 0576                catch (Exception ex)
 577                {
 0578                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0579                }
 580            }
 581
 0582            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 583            {
 584                // Assume only the first is required
 585                // Add this flag to GetDeletePaths if required in the future
 0586                var isRequiredForDelete = true;
 587
 0588                foreach (var fileSystemInfo in item.GetDeletePaths())
 589                {
 0590                    DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
 591
 0592                    isRequiredForDelete = false;
 593                }
 594            }
 595
 0596            item.SetParent(null);
 597
 0598            var externalDataManager = _externalDataManagerFactory.Value;
 0599            externalDataManager.DeleteExternalItemFiles(item);
 0600            foreach (var child in children)
 601            {
 0602                externalDataManager.DeleteExternalItemFiles(child);
 603            }
 604
 0605            _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
 0606            _cache.TryRemove(item.Id, out _);
 0607            foreach (var child in children)
 608            {
 0609                _cache.TryRemove(child.Id, out _);
 610            }
 611
 0612            if (parent is Folder folder)
 613            {
 0614                folder.Children = null;
 0615                folder.UserData = null;
 616            }
 617
 618            // Announce the descendants before the item itself.
 0619            foreach (var child in children)
 620            {
 0621                ReportItemRemoved(child, item);
 622            }
 623
 0624            ReportItemRemoved(item, parent);
 0625        }
 626
 627        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 628        {
 0629            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 630            {
 631                try
 632                {
 0633                    _logger.LogInformation(
 0634                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0635                        item.GetType().Name,
 0636                        item.Name ?? "Unknown name",
 0637                        fileSystemInfo.FullName,
 0638                        item.Id);
 639
 0640                    if (fileSystemInfo.IsDirectory)
 641                    {
 0642                        Directory.Delete(fileSystemInfo.FullName, true);
 643                    }
 644                    else
 645                    {
 0646                        File.Delete(fileSystemInfo.FullName);
 647                    }
 0648                }
 0649                catch (DirectoryNotFoundException)
 650                {
 0651                    _logger.LogInformation(
 0652                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0653                        item.GetType().Name,
 0654                        item.Name ?? "Unknown name",
 0655                        fileSystemInfo.FullName,
 0656                        item.Id);
 0657                }
 0658                catch (FileNotFoundException)
 659                {
 0660                    _logger.LogInformation(
 0661                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0662                        item.GetType().Name,
 0663                        item.Name ?? "Unknown name",
 0664                        fileSystemInfo.FullName,
 0665                        item.Id);
 0666                }
 0667                catch (IOException)
 668                {
 0669                    if (isRequiredForDelete)
 670                    {
 0671                        throw;
 672                    }
 0673                }
 0674                catch (UnauthorizedAccessException)
 675                {
 0676                    if (isRequiredForDelete)
 677                    {
 0678                        throw;
 679                    }
 0680                }
 681            }
 0682        }
 683
 684        private bool IsInternalItem(BaseItem item)
 685        {
 0686            if (!item.IsFileProtocol)
 687            {
 0688                return false;
 689            }
 690
 0691            var pathToCheck = item switch
 0692            {
 0693                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0694                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0695                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0696                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0697                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0698                Year => _configurationManager.ApplicationPaths.YearPath,
 0699                _ => null
 0700            };
 701
 0702            var itemPath = item.Path;
 0703            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 704            {
 0705                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0706                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 707
 0708                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 709            }
 710
 0711            return false;
 712        }
 713
 714        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 715        {
 0716            var list = GetInternalMetadataPaths(item);
 0717            foreach (var child in children)
 718            {
 0719                list.AddRange(GetInternalMetadataPaths(child));
 720            }
 721
 0722            return list;
 723        }
 724
 725        private List<string> GetInternalMetadataPaths(BaseItem item)
 726        {
 2727            var list = new List<string>
 2728            {
 2729                item.GetInternalMetadataPath()
 2730            };
 731
 2732            if (item is Video video)
 733            {
 734                // Trickplay
 2735                list.Add(_pathManager.GetTrickplayDirectory(video));
 736
 737                // Chapter Images
 2738                list.Add(_pathManager.GetChapterImageFolderPath(video));
 739
 740                // Subtitles and attachments
 12741                foreach (var mediaSource in item.GetMediaSources(false))
 742                {
 4743                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 4744                    if (subtitleFolder is not null)
 745                    {
 0746                        list.Add(subtitleFolder);
 747                    }
 748
 4749                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 4750                    if (attachmentFolder is not null)
 751                    {
 0752                        list.Add(attachmentFolder);
 753                    }
 754                }
 755            }
 756
 2757            return list;
 758        }
 759
 760        /// <summary>
 761        /// Resolves the item.
 762        /// </summary>
 763        /// <param name="args">The args.</param>
 764        /// <param name="resolvers">The resolvers.</param>
 765        /// <returns>BaseItem.</returns>
 766        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 767        {
 146768            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 146769                .FirstOrDefault(i => i is not null);
 770
 146771            if (item is not null)
 772            {
 114773                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 774            }
 775
 146776            return item;
 777        }
 778
 779        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 780        {
 781            try
 782            {
 680783                return resolver.ResolvePath(args);
 784            }
 0785            catch (Exception ex)
 786            {
 0787                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0788                return null;
 789            }
 680790        }
 791
 792        public Guid GetNewItemId(string key, Type type)
 793        {
 231794            return GetNewItemIdInternal(key, type, false);
 795        }
 796
 797        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 798        {
 232799            ArgumentException.ThrowIfNullOrEmpty(key);
 232800            ArgumentNullException.ThrowIfNull(type);
 801
 232802            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 232803            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 804            {
 805                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 161806                key = key.Substring(programDataPath.Length)
 161807                    .TrimStart('/', '\\')
 161808                    .Replace('/', '\\');
 809            }
 810
 232811            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 812            {
 1813                key = key.ToLowerInvariant();
 814            }
 815
 232816            key = type.FullName + key;
 817
 232818            return key.GetMD5();
 819        }
 820
 821        public BaseItem? ResolvePath(
 822            FileSystemMetadata fileInfo,
 823            Folder? parent = null,
 824            IDirectoryService? directoryService = null,
 825            CollectionType? collectionType = null)
 56826            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType
 827
 828        private void SetAdditionalPartsFromStack(Video altVideo, string path)
 829        {
 0830            if (altVideo.AdditionalParts is { Length: > 0 })
 831            {
 0832                return;
 833            }
 834
 0835            var directory = Path.GetDirectoryName(path);
 0836            if (string.IsNullOrEmpty(directory))
 837            {
 0838                return;
 839            }
 840
 841            IEnumerable<FileSystemMetadata> siblings;
 842            try
 843            {
 0844                siblings = _fileSystem.GetFiles(directory);
 0845            }
 0846            catch (Exception ex)
 847            {
 0848                _logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
 0849                return;
 850            }
 851
 0852            var stacks = StackResolver.Resolve(siblings, _namingOptions);
 0853            foreach (var stack in stacks)
 854            {
 0855                if (stack.Files.Count > 1
 0856                    && string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
 857                {
 0858                    altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
 0859                    return;
 860                }
 861            }
 0862        }
 863
 864        /// <inheritdoc />
 865        public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? colle
 866        {
 867            // Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
 868            // This happens when items were previously resolved without proper type context
 869            // in mixed-content libraries where collectionType is null.
 2870            var expectedId = GetNewItemId(path, expectedVideoType);
 2871            if (expectedVideoType != typeof(Video))
 872            {
 2873                var wrongTypeId = GetNewItemId(path, typeof(Video));
 2874                if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
 875                {
 2876                    _logger.LogInformation(
 2877                        "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
 2878                        wrongTypeItem.GetType().Name,
 2879                        expectedVideoType.Name,
 2880                        path);
 881
 882                    // A full DeleteItem would save the primary version, which resolves its
 883                    // alternates again and re-enters here before this row is gone.
 2884                    DeleteItemsUnsafeFast([wrongTypeItem]);
 885
 886                    // The fast path skips the parent bookkeeping, and the stale item is listed
 887                    // under its ParentId, so that folder's cached listing has to be dropped.
 2888                    if (wrongTypeItem.GetParent() is Folder staleParent)
 889                    {
 2890                        staleParent.Children = null;
 2891                        staleParent.UserData = null;
 892                    }
 893                }
 894            }
 895
 2896            var resolved = ResolvePath(
 2897                _fileSystem.GetFileSystemInfo(path),
 2898                parent,
 2899                collectionType: collectionType) as Video;
 900
 2901            if (resolved is null)
 902            {
 2903                return null;
 904            }
 905
 906            // Ensure the alternate version has the same concrete type as the primary video.
 907            // ResolvePath may return a generic Video for files in mixed-content libraries
 908            // where collectionType is null, even though the primary is a Movie/Episode/etc.
 0909            if (resolved.GetType() != expectedVideoType)
 910            {
 0911                if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
 912                {
 0913                    correctVideo.Path = resolved.Path;
 0914                    correctVideo.Name = resolved.Name;
 0915                    correctVideo.VideoType = resolved.VideoType;
 0916                    correctVideo.ProductionYear = resolved.ProductionYear;
 0917                    correctVideo.ExtraType = resolved.ExtraType;
 0918                    resolved = correctVideo;
 919                }
 920            }
 921
 0922            resolved.Id = expectedId;
 0923            return resolved;
 924        }
 925
 926        private BaseItem? ResolvePath(
 927            FileSystemMetadata fileInfo,
 928            IDirectoryService directoryService,
 929            IItemResolver[]? resolvers,
 930            Folder? parent = null,
 931            CollectionType? collectionType = null,
 932            LibraryOptions? libraryOptions = null)
 933        {
 146934            ArgumentNullException.ThrowIfNull(fileInfo);
 935
 146936            var fullPath = fileInfo.FullName;
 937
 146938            if (collectionType is null && parent is not null)
 939            {
 49940                collectionType = GetContentTypeOverride(fullPath, true);
 941            }
 942
 146943            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 146944            {
 146945                Parent = parent,
 146946                FileInfo = fileInfo,
 146947                CollectionType = collectionType,
 146948                LibraryOptions = libraryOptions
 146949            };
 950
 951            // Return null if ignore rules deem that we should do so
 146952            if (IgnoreFile(args.FileInfo, args.Parent))
 953            {
 0954                return null;
 955            }
 956
 957            // Gather child folder and files
 146958            if (args.IsDirectory)
 959            {
 73960                var isPhysicalRoot = args.IsPhysicalRoot;
 961
 962                // When resolving the root, we need it's grandchildren (children of user views)
 73963                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 964
 965                FileSystemMetadata[] files;
 73966                var isVf = args.IsVf;
 967
 968                try
 969                {
 73970                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 73971                }
 0972                catch (Exception ex)
 973                {
 0974                    if (parent is not null && parent.IsPhysicalRoot)
 975                    {
 0976                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 977
 0978                        files = [];
 979                    }
 980                    else
 981                    {
 0982                        throw;
 983                    }
 0984                }
 985
 986                // Need to remove sub-paths that may have been resolved from shortcuts
 987                // Example: if \\server\movies exists, then strip out \\server\movies\action
 73988                if (isPhysicalRoot)
 989                {
 27990                    files = NormalizeRootPathList(files).ToArray();
 991                }
 992
 73993                args.FileSystemChildren = files;
 994            }
 995
 996            // Filter content based on ignore rules
 146997            if (args.IsDirectory)
 998            {
 73999                var filtered = args.GetActualFileSystemChildren().ToArray();
 731000                args.FileSystemChildren = filtered ?? [];
 1001            }
 1002
 1461003            return ResolveItem(args, resolvers);
 1004        }
 1005
 1006        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 2181007            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 1008
 1009        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 1010        {
 1041011            var originalList = paths.ToList();
 1012
 1041013            var list = originalList.Where(i => i.IsDirectory)
 1041014                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 1041015                .Distinct()
 1041016                .ToList();
 1017
 1041018            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 1041019                .ToList();
 1020
 2081021            foreach (var dupe in dupes)
 1022            {
 01023                _logger.LogInformation("Found duplicate path: {0}", dupe);
 1024            }
 1025
 1041026            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 1041027            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 1041028            return newList;
 1029        }
 1030
 1031        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 1032        {
 781033            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 1034        }
 1035
 1036        public IEnumerable<BaseItem> ResolvePaths(
 1037            IEnumerable<FileSystemMetadata> files,
 1038            IDirectoryService directoryService,
 1039            Folder parent,
 1040            LibraryOptions libraryOptions,
 1041            CollectionType? collectionType,
 1042            IItemResolver[] resolvers)
 1043        {
 781044            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 1045
 781046            if (parent is not null)
 1047            {
 781048                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 1049
 4681050                foreach (var resolver in multiItemResolvers)
 1051                {
 1561052                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 1053
 1561054                    if (result?.Items.Count > 0)
 1055                    {
 01056                        var items = result.Items;
 01057                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 01058                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 01059                        return items;
 1060                    }
 1061                }
 1062            }
 1063
 781064            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 01065        }
 1066
 1067        private IEnumerable<BaseItem> ResolveFileList(
 1068            IReadOnlyList<FileSystemMetadata> fileList,
 1069            IDirectoryService directoryService,
 1070            Folder? parent,
 1071            CollectionType? collectionType,
 1072            IItemResolver[]? resolvers,
 1073            LibraryOptions libraryOptions)
 1074        {
 1075            // Given that fileList is a list we can save enumerator allocations by indexing
 2541076            for (var i = 0; i < fileList.Count; i++)
 1077            {
 491078                var file = fileList[i];
 491079                BaseItem? result = null;
 1080                try
 1081                {
 491082                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 491083                }
 01084                catch (Exception ex)
 1085                {
 01086                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 01087                }
 1088
 491089                if (result is not null)
 1090                {
 191091                    yield return result;
 1092                }
 1093            }
 781094        }
 1095
 1096        /// <summary>
 1097        /// Creates the root media folder.
 1098        /// </summary>
 1099        /// <returns>AggregateFolder.</returns>
 1100        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 1101        public AggregateFolder CreateRootFolder()
 1102        {
 271103            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 1104
 271105            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 271106                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 271107                             .DeepCopy<Folder, AggregateFolder>();
 1108
 1109            // In case program data folder was moved
 271110            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 1111            {
 01112                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 01113                rootFolder.Path = rootFolderPath;
 1114            }
 1115
 1116            // Add in the plug-in folders
 271117            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 1118
 271119            var info = Directory.CreateDirectory(path);
 271120            Folder folder = new PlaylistsFolder
 271121            {
 271122                Path = path,
 271123                DateCreated = info.CreationTimeUtc,
 271124                DateModified = info.LastWriteTimeUtc,
 271125            };
 1126
 271127            if (folder.Id.IsEmpty())
 1128            {
 271129                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 1130            }
 1131
 271132            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 1133
 271134            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 1135            {
 01136                folder = dbItem;
 1137            }
 1138
 271139            if (!folder.ParentId.Equals(rootFolder.Id))
 1140            {
 271141                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 271142                folder.ParentId = rootFolder.Id;
 271143                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 1144            }
 1145
 271146            rootFolder.AddVirtualChild(folder);
 1147
 271148            RegisterItem(folder);
 1149
 271150            return rootFolder;
 1151        }
 1152
 1153        public Folder GetUserRootFolder()
 1154        {
 12531155            if (_userRootFolder is null)
 271156            {
 1157                lock (_userRootFolderSyncLock)
 1158                {
 271159                    if (_userRootFolder is null)
 1160                    {
 271161                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 1162
 271163                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 271164                        Directory.CreateDirectory(userRootPath);
 1165
 271166                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 271167                        UserRootFolder? tmpItem = null;
 1168                        try
 1169                        {
 271170                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 271171                        }
 01172                        catch (Exception ex)
 1173                        {
 01174                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 01175                        }
 1176
 271177                        if (tmpItem is null)
 1178                        {
 271179                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 271180                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 271181                                        .DeepCopy<Folder, UserRootFolder>();
 1182                        }
 1183
 1184                        // In case program data folder was moved
 271185                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 1186                        {
 01187                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 01188                            tmpItem.Path = userRootPath;
 1189                        }
 1190
 271191                        _userRootFolder = tmpItem;
 271192                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 1193                    }
 271194                }
 1195            }
 1196
 12531197            return _userRootFolder;
 1198        }
 1199
 1200        /// <inheritdoc />
 1201        public BaseItem? FindByPath(string path, bool? isFolder)
 1202        {
 1203            // If this returns multiple items it could be tricky figuring out which one is correct.
 1204            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 01205            ArgumentException.ThrowIfNullOrEmpty(path);
 1206
 01207            var query = new InternalItemsQuery
 01208            {
 01209                Path = path,
 01210                IsFolder = isFolder,
 01211                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 01212                Limit = 1,
 01213                DtoOptions = new DtoOptions(true)
 01214            };
 1215
 01216            return GetItemList(query)
 01217                .FirstOrDefault();
 1218        }
 1219
 1220        /// <inheritdoc />
 1221        public Guid GetPersonId(string name)
 1222        {
 01223            return GetItemByNameId<Person>(Person.GetPath(name));
 1224        }
 1225
 1226        /// <inheritdoc />
 1227        public Person? GetPerson(string name)
 1228        {
 11229            var path = Person.GetPath(name);
 11230            var id = GetItemByNameId<Person>(path);
 11231            if (GetItemById(id) is Person item)
 1232            {
 01233                return item;
 1234            }
 1235
 11236            return null;
 1237        }
 1238
 1239        /// <inheritdoc />
 1240        public Person GetOrCreatePerson(string name)
 1241        {
 01242            var existing = GetPerson(name);
 01243            if (existing is not null)
 1244            {
 01245                return existing;
 1246            }
 1247
 01248            var path = Person.GetPath(name);
 01249            var info = Directory.CreateDirectory(path);
 01250            var item = new Person
 01251            {
 01252                Name = name,
 01253                Id = GetItemByNameId<Person>(path),
 01254                DateCreated = info.CreationTimeUtc,
 01255                DateModified = info.LastWriteTimeUtc,
 01256                Path = path
 01257            };
 1258
 01259            item.PresentationUniqueKey = item.CreatePresentationUniqueKey();
 1260
 01261            CreateItem(item, null);
 1262
 01263            return item;
 1264        }
 1265
 1266        /// <summary>
 1267        /// Gets the studio.
 1268        /// </summary>
 1269        /// <param name="name">The name.</param>
 1270        /// <returns>Task{Studio}.</returns>
 1271        public Studio GetStudio(string name)
 1272        {
 01273            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 1274        }
 1275
 1276        public Guid GetStudioId(string name)
 1277        {
 01278            return GetItemByNameId<Studio>(Studio.GetPath(name));
 1279        }
 1280
 1281        public Guid GetGenreId(string name)
 1282        {
 01283            return GetItemByNameId<Genre>(Genre.GetPath(name));
 1284        }
 1285
 1286        public Guid GetMusicGenreId(string name)
 1287        {
 01288            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 1289        }
 1290
 1291        /// <summary>
 1292        /// Gets the genre.
 1293        /// </summary>
 1294        /// <param name="name">The name.</param>
 1295        /// <returns>Task{Genre}.</returns>
 1296        public Genre GetGenre(string name)
 1297        {
 01298            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 1299        }
 1300
 1301        /// <summary>
 1302        /// Gets the music genre.
 1303        /// </summary>
 1304        /// <param name="name">The name.</param>
 1305        /// <returns>Task{MusicGenre}.</returns>
 1306        public MusicGenre GetMusicGenre(string name)
 1307        {
 01308            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1309        }
 1310
 1311        /// <summary>
 1312        /// Gets the year.
 1313        /// </summary>
 1314        /// <param name="value">The value.</param>
 1315        /// <returns>Task{Year}.</returns>
 1316        public Year GetYear(int value)
 1317        {
 01318            if (value <= 0)
 1319            {
 01320                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1321            }
 1322
 01323            var name = value.ToString(CultureInfo.InvariantCulture);
 1324
 01325            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1326        }
 1327
 1328        /// <summary>
 1329        /// Gets a Genre.
 1330        /// </summary>
 1331        /// <param name="name">The name.</param>
 1332        /// <returns>Task{Genre}.</returns>
 1333        public MusicArtist GetArtist(string name)
 1334        {
 51335            return GetArtist(name, new DtoOptions(true));
 1336        }
 1337
 1338        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1339        {
 241340            return _linkedChildrenService.FindArtists(names);
 1341        }
 1342
 1343        public MusicArtist GetArtist(string name, DtoOptions options)
 1344        {
 51345            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1346        }
 1347
 1348        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1349            where T : BaseItem, new()
 1350        {
 51351            if (typeof(T) == typeof(MusicArtist))
 1352            {
 51353                var existing = GetItemList(new InternalItemsQuery
 51354                {
 51355                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 51356                    Name = name,
 51357                    DtoOptions = options
 51358                }).Cast<MusicArtist>()
 51359                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 51360                .Cast<T>()
 51361                .FirstOrDefault();
 1362
 51363                if (existing is not null)
 1364                {
 51365                    return existing;
 1366                }
 1367            }
 1368
 01369            var path = getPathFn(name);
 01370            var id = GetItemByNameId<T>(path);
 01371            var item = GetItemById(id) as T;
 01372            if (item is null)
 1373            {
 01374                var info = Directory.CreateDirectory(path);
 01375                item = new T
 01376                {
 01377                    Name = name,
 01378                    Id = id,
 01379                    DateCreated = info.CreationTimeUtc,
 01380                    DateModified = info.LastWriteTimeUtc,
 01381                    Path = path
 01382                };
 1383
 01384                CreateItem(item, null);
 1385            }
 1386
 01387            return item;
 1388        }
 1389
 1390        private Guid GetItemByNameId<T>(string path)
 1391              where T : BaseItem, new()
 1392        {
 11393            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11394            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1395        }
 1396
 1397        /// <summary>
 1398        /// Reloads the root media folder.
 1399        /// </summary>
 1400        /// <param name="progress">The progress.</param>
 1401        /// <param name="cancellationToken">The cancellation token.</param>
 1402        /// <returns>Task.</returns>
 1403        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1404        {
 1405            // Just run the scheduled task so that the user can see it
 11406            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1407
 11408            return Task.CompletedTask;
 1409        }
 1410
 1411        /// <summary>
 1412        /// Validates the media library internal.
 1413        /// </summary>
 1414        /// <param name="progress">The progress.</param>
 1415        /// <param name="cancellationToken">The cancellation token.</param>
 1416        /// <returns>Task.</returns>
 1417        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1418        {
 211419            IsScanRunning = true;
 211420            ClearIgnoreRuleCache();
 211421            LibraryMonitor.Stop();
 1422
 1423            try
 1424            {
 211425                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 191426            }
 1427            finally
 1428            {
 211429                ClearIgnoreRuleCache();
 211430                LibraryMonitor.Start();
 211431                IsScanRunning = false;
 1432            }
 191433        }
 1434
 1435        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1436        {
 291437            ClearIgnoreRuleCache();
 291438            RootFolder.Children = null;
 291439            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1440
 1441            // Start by just validating the children of the root, but go no further
 291442            await RootFolder.ValidateChildren(
 291443                new Progress<double>(),
 291444                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 291445                recursive: false,
 291446                allowRemoveRoot: removeRoot,
 291447                cancellationToken: cancellationToken).ConfigureAwait(false);
 1448
 291449            var rootFolder = GetUserRootFolder();
 291450            rootFolder.Children = null;
 1451
 291452            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1453
 291454            await rootFolder.ValidateChildren(
 291455                new Progress<double>(),
 291456                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 291457                recursive: false,
 291458                allowRemoveRoot: removeRoot,
 291459                cancellationToken: cancellationToken).ConfigureAwait(false);
 1460
 1461            // Quickly scan CollectionFolders for changes
 291462            var toDelete = new List<Guid>();
 1041463            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1464            {
 1465                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 231466                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1467                {
 41468                    toDelete.Add(collectionFolder.Id);
 1469                }
 1470                else
 1471                {
 191472                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1473                }
 1474            }
 1475
 291476            if (toDelete.Count > 0)
 1477            {
 41478                _persistenceService.DeleteItem(toDelete.ToArray());
 1479            }
 1480
 291481            ClearIgnoreRuleCache();
 291482        }
 1483
 1484        /// <inheritdoc />
 1485        public void ClearIgnoreRuleCache()
 1486        {
 1081487            _dotIgnoreIgnoreRule.ClearDirectoryCache();
 1081488        }
 1489
 1490        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1491        {
 211492            _logger.LogInformation("Validating media library");
 1493
 211494            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1495
 211496            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1497
 1498            // Validate the entire media library
 211499            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1500
 201501            progress.Report(96);
 1502
 201503            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1504
 201505            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1506
 191507            progress.Report(100);
 191508        }
 1509
 1510        /// <summary>
 1511        /// Runs the post scan tasks.
 1512        /// </summary>
 1513        /// <param name="progress">The progress.</param>
 1514        /// <param name="cancellationToken">The cancellation token.</param>
 1515        /// <returns>Task.</returns>
 1516        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1517        {
 201518            var tasks = PostScanTasks.ToList();
 1519
 201520            var numComplete = 0;
 201521            var numTasks = tasks.Count;
 1522
 201523            _logger.LogInformation("Running {TaskCount} post-scan task(s)", numTasks);
 1524
 201525            var phaseStart = Stopwatch.GetTimestamp();
 1526
 3971527            foreach (var task in tasks)
 1528            {
 1529                // Prevent access to modified closure
 1791530                var currentNumComplete = numComplete;
 1531
 1791532                var innerProgress = new Progress<double>(pct =>
 1791533                {
 1791534                    double innerPercent = pct;
 1791535                    innerPercent /= 100;
 1791536                    innerPercent += currentNumComplete;
 1791537
 1791538                    innerPercent /= numTasks;
 1791539                    innerPercent *= 100;
 1791540
 1791541                    progress.Report(innerPercent);
 1791542                });
 1543
 1791544                var taskName = task.GetType().Name;
 1791545                var taskStart = Stopwatch.GetTimestamp();
 1546
 1791547                _logger.LogInformation(
 1791548                    "Running post-scan task {TaskNumber}/{TaskCount}: {TaskName}",
 1791549                    currentNumComplete + 1,
 1791550                    numTasks,
 1791551                    taskName);
 1552
 1553                try
 1554                {
 1791555                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1556
 1781557                    var elapsed = Stopwatch.GetElapsedTime(taskStart);
 1781558                    _logger.LogInformation(
 1781559                        "Post-scan task {TaskName} completed after {Minutes} minute(s) and {Seconds} seconds",
 1781560                        taskName,
 1781561                        Math.Truncate(elapsed.TotalMinutes),
 1781562                        elapsed.Seconds);
 1781563                }
 11564                catch (OperationCanceledException)
 1565                {
 11566                    var elapsed = Stopwatch.GetElapsedTime(taskStart);
 11567                    _logger.LogInformation(
 11568                        "Post-scan task {TaskName} cancelled after {Minutes} minute(s) and {Seconds} seconds",
 11569                        taskName,
 11570                        Math.Truncate(elapsed.TotalMinutes),
 11571                        elapsed.Seconds);
 11572                    throw;
 1573                }
 01574                catch (Exception ex)
 1575                {
 01576                    var elapsed = Stopwatch.GetElapsedTime(taskStart);
 01577                    _logger.LogError(
 01578                        ex,
 01579                        "Post-scan task {TaskName} failed after {Minutes} minute(s) and {Seconds} seconds",
 01580                        taskName,
 01581                        Math.Truncate(elapsed.TotalMinutes),
 01582                        elapsed.Seconds);
 01583                }
 1584
 1781585                numComplete++;
 1781586                double percent = numComplete;
 1781587                percent /= numTasks;
 1781588                progress.Report(percent * 100);
 1781589            }
 1590
 191591            var phaseElapsed = Stopwatch.GetElapsedTime(phaseStart);
 191592            _logger.LogInformation(
 191593                "All post-scan tasks completed after {Minutes} minute(s) and {Seconds} seconds",
 191594                Math.Truncate(phaseElapsed.TotalMinutes),
 191595                phaseElapsed.Seconds);
 1596
 191597            _persistenceService.UpdateInheritedValues();
 1598
 191599            progress.Report(100);
 191600        }
 1601
 1602        /// <summary>
 1603        /// Gets the default view.
 1604        /// </summary>
 1605        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1606        public List<VirtualFolderInfo> GetVirtualFolders()
 1607        {
 291608            return GetVirtualFolders(false);
 1609        }
 1610
 1611        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1612        {
 301613            _logger.LogDebug("Getting topLibraryFolders");
 301614            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1615
 301616            _logger.LogDebug("Getting refreshQueue");
 301617            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1618
 301619            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 301620                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 301621                .ToList();
 1622        }
 1623
 1624        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1625        {
 11626            var info = new VirtualFolderInfo
 11627            {
 11628                Name = Path.GetFileName(dir),
 11629
 11630                Locations = _fileSystem.GetFilePaths(dir, false)
 11631                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11632                    .Select(i =>
 11633                    {
 11634                        try
 11635                        {
 11636                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11637                        }
 11638                        catch (Exception ex)
 11639                        {
 11640                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11641                            return null;
 11642                        }
 11643                    })
 11644                    .Where(i => i is not null)
 11645                    .Order()
 11646                    .ToArray(),
 11647
 11648                CollectionType = GetCollectionType(dir)
 11649            };
 1650
 11651            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11652            if (libraryFolder is not null)
 1653            {
 11654                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11655                info.ItemId = libraryFolderId;
 11656                if (libraryFolder.HasImage(ImageType.Primary))
 1657                {
 01658                    info.PrimaryImageItemId = libraryFolderId;
 1659                }
 1660
 11661                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1662
 11663                if (refreshQueue is not null)
 1664                {
 11665                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1666
 11667                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1668                }
 1669            }
 1670
 11671            return info;
 1672        }
 1673
 1674        private CollectionTypeOptions? GetCollectionType(string path)
 1675        {
 11676            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21677            foreach (ReadOnlySpan<char> file in files)
 1678            {
 01679                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1680                {
 01681                    return res;
 1682                }
 1683            }
 1684
 11685            return null;
 01686        }
 1687
 1688        /// <inheritdoc />
 1689        public BaseItem? GetItemById(Guid id)
 1690        {
 11731691            if (id.IsEmpty())
 1692            {
 01693                throw new ArgumentException("Guid can't be empty", nameof(id));
 1694            }
 1695
 11731696            if (_cache.TryGet(id, out var item))
 1697            {
 10161698                return item;
 1699            }
 1700
 1571701            item = RetrieveItem(id);
 1702
 1571703            if (item is not null)
 1704            {
 81705                RegisterItem(item);
 1706            }
 1707
 1571708            return item;
 1709        }
 1710
 1711        /// <inheritdoc />
 1712        public T? GetItemById<T>(Guid id)
 1713            where T : BaseItem
 1714        {
 241715            var item = GetItemById(id);
 241716            if (item is T typedItem)
 1717            {
 11718                return typedItem;
 1719            }
 1720
 231721            return null;
 1722        }
 1723
 1724        /// <inheritdoc />
 1725        public T? GetItemById<T>(Guid id, Guid userId)
 1726            where T : BaseItem
 1727        {
 11728            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11729            return GetItemById<T>(id, user);
 1730        }
 1731
 1732        /// <inheritdoc />
 1733        public T? GetItemById<T>(Guid id, User? user)
 1734            where T : BaseItem
 1735        {
 221736            var item = GetItemById<T>(id);
 221737            return ItemIsVisible(item, user) ? item : null;
 1738        }
 1739
 1740        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1741        {
 2191742            if (query.Recursive && !query.ParentId.IsEmpty())
 1743            {
 581744                var parent = GetItemById(query.ParentId);
 581745                if (parent is not null)
 1746                {
 581747                    SetTopParentIdsOrAncestors(query, [parent]);
 1748                }
 1749            }
 1750
 2191751            if (query.User is not null)
 1752            {
 11753                AddUserToQuery(query, query.User, allowExternalContent);
 1754            }
 1755
 2191756            return _itemRepository.GetItemList(query);
 1757        }
 1758
 1759        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1760        {
 2191761            return GetItemList(query, true);
 1762        }
 1763
 1764        public int GetCount(InternalItemsQuery query)
 1765        {
 01766            if (query.Recursive && !query.ParentId.IsEmpty())
 1767            {
 01768                var parent = GetItemById(query.ParentId);
 01769                if (parent is not null)
 1770                {
 01771                    SetTopParentIdsOrAncestors(query, [parent]);
 1772                }
 1773            }
 1774
 01775            if (query.User is not null)
 1776            {
 01777                AddUserToQuery(query, query.User);
 1778            }
 1779
 01780            return _countService.GetCount(query);
 1781        }
 1782
 1783        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1784        {
 01785            if (query.Recursive && !query.ParentId.IsEmpty())
 1786            {
 01787                var parent = GetItemById(query.ParentId);
 01788                if (parent is not null)
 1789                {
 01790                    SetTopParentIdsOrAncestors(query, [parent]);
 1791                }
 1792            }
 1793
 01794            if (query.User is not null)
 1795            {
 01796                AddUserToQuery(query, query.User);
 1797            }
 1798
 01799            return _countService.GetItemCounts(query);
 1800        }
 1801
 1802        /// <inheritdoc/>
 1803        public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? us
 1804        {
 01805            var query = new InternalItemsQuery(user);
 01806            if (user is not null)
 1807            {
 01808                AddUserToQuery(query, user);
 1809            }
 1810
 01811            return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
 1812        }
 1813
 1814        /// <inheritdoc/>
 1815        public Dictionary<Guid, ItemCounts> GetItemCountsForNameItems(BaseItemKind kind, IReadOnlyList<Guid> ids, BaseIt
 1816        {
 01817            var query = new InternalItemsQuery(user);
 01818            if (user is not null)
 1819            {
 01820                AddUserToQuery(query, user);
 1821            }
 1822
 01823            return _countService.GetItemCountsForNameItems(kind, ids, relatedItemKinds, query);
 1824        }
 1825
 1826        public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user)
 1827        {
 61828            return _countService.GetChildCountBatch(parentIds, user);
 1829        }
 1830
 1831        /// <inheritdoc/>
 1832        public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User
 1833        {
 01834            return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
 1835        }
 1836
 1837        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1838        {
 01839            SetTopParentIdsOrAncestors(query, parents);
 1840
 01841            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1842            {
 01843                if (query.User is not null)
 1844                {
 01845                    AddUserToQuery(query, query.User);
 1846                }
 1847            }
 1848
 01849            return _itemRepository.GetItemList(query);
 1850        }
 1851
 1852        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1853        {
 01854            SetTopParentIdsOrAncestors(query, parents);
 1855
 01856            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1857            {
 01858                if (query.User is not null)
 1859                {
 01860                    AddUserToQuery(query, query.User);
 1861                }
 1862            }
 1863
 01864            return _itemRepository.GetLatestItemList(query, collectionType);
 1865        }
 1866
 1867        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1868        {
 01869            SetTopParentIdsOrAncestors(query, parents);
 1870
 01871            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1872            {
 01873                if (query.User is not null)
 1874                {
 01875                    AddUserToQuery(query, query.User);
 1876                }
 1877            }
 1878
 01879            return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
 1880        }
 1881
 1882        /// <inheritdoc />
 1883        public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisod
 1884            InternalItemsQuery query,
 1885            IReadOnlyList<string> seriesKeys,
 1886            bool includeSpecials,
 1887            bool includeWatchedForRewatching)
 1888        {
 01889            return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching
 1890        }
 1891
 1892        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1893        {
 01894            if (query.User is not null)
 1895            {
 01896                AddUserToQuery(query, query.User);
 1897            }
 1898
 01899            if (query.EnableTotalRecordCount)
 1900            {
 01901                return _itemRepository.GetItems(query);
 1902            }
 1903
 01904            return new QueryResult<BaseItem>(
 01905                query.StartIndex,
 01906                null,
 01907                _itemRepository.GetItemList(query));
 1908        }
 1909
 1910        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1911        {
 1001912            if (query.User is not null)
 1913            {
 01914                AddUserToQuery(query, query.User);
 1915            }
 1916
 1001917            return _itemRepository.GetItemIdsList(query);
 1918        }
 1919
 1920        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1921        {
 01922            if (query.User is not null)
 1923            {
 01924                AddUserToQuery(query, query.User);
 1925            }
 1926
 01927            SetTopParentOrAncestorIds(query);
 01928            return _itemRepository.GetStudios(query);
 1929        }
 1930
 1931        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1932        {
 01933            if (query.User is not null)
 1934            {
 01935                AddUserToQuery(query, query.User);
 1936            }
 1937
 01938            SetTopParentOrAncestorIds(query);
 01939            return _itemRepository.GetGenres(query);
 1940        }
 1941
 1942        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1943        {
 01944            if (query.User is not null)
 1945            {
 01946                AddUserToQuery(query, query.User);
 1947            }
 1948
 01949            SetTopParentOrAncestorIds(query);
 01950            return _itemRepository.GetMusicGenres(query);
 1951        }
 1952
 1953        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1954        {
 01955            if (query.User is not null)
 1956            {
 01957                AddUserToQuery(query, query.User);
 1958            }
 1959
 01960            SetTopParentOrAncestorIds(query);
 01961            return _itemRepository.GetAllArtists(query);
 1962        }
 1963
 1964        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1965        {
 01966            if (query.User is not null)
 1967            {
 01968                AddUserToQuery(query, query.User);
 1969            }
 1970
 01971            SetTopParentOrAncestorIds(query);
 01972            return _itemRepository.GetArtists(query);
 1973        }
 1974
 1975        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1976        {
 01977            var ancestorIds = query.AncestorIds;
 01978            int len = ancestorIds.Length;
 01979            if (len == 0)
 1980            {
 01981                return;
 1982            }
 1983
 01984            var parents = new BaseItem[len];
 01985            for (int i = 0; i < len; i++)
 1986            {
 01987                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01988                if (parents[i] is not (ICollectionFolder or UserView))
 1989                {
 01990                    return;
 1991                }
 1992            }
 1993
 1994            // Optimize by querying against top level views
 01995            var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01996            if (topParentIds.Length == 0)
 1997            {
 01998                return;
 1999            }
 2000
 02001            query.TopParentIds = topParentIds;
 02002            query.AncestorIds = [];
 02003        }
 2004
 2005        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 2006        {
 02007            if (query.User is not null)
 2008            {
 02009                AddUserToQuery(query, query.User);
 2010            }
 2011
 02012            SetTopParentOrAncestorIds(query);
 02013            return _itemRepository.GetAlbumArtists(query);
 2014        }
 2015
 2016        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 2017        {
 472018            if (query.Recursive && !query.ParentId.IsEmpty())
 2019            {
 462020                var parent = GetItemById(query.ParentId);
 462021                if (parent is not null)
 2022                {
 462023                    SetTopParentIdsOrAncestors(query, [parent]);
 2024                }
 2025            }
 2026
 472027            if (query.User is not null)
 2028            {
 32029                AddUserToQuery(query, query.User);
 2030            }
 2031
 472032            if (query.EnableTotalRecordCount)
 2033            {
 12034                return _itemRepository.GetItems(query);
 2035            }
 2036
 462037            return new QueryResult<BaseItem>(
 462038                query.StartIndex,
 462039                null,
 462040                _itemRepository.GetItemList(query));
 2041        }
 2042
 2043        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 2044        {
 1042045            if (parents.All(i => i is ICollectionFolder || i is UserView))
 2046            {
 2047                // Optimize by querying against top level views
 462048                var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 2049
 462050                if (topParentIds.Length > 0)
 2051                {
 02052                    query.TopParentIds = topParentIds;
 2053                }
 2054                else
 2055                {
 462056                    SetAncestorIds(query, parents);
 2057                }
 2058            }
 582059            else if (parents.Count == 1 && parents.First() is Folder folder
 582060                && (folder is Playlist || folder is BoxSet)
 582061                && folder.LinkedChildren.Length > 0)
 2062            {
 2063                // Playlists and BoxSets store their contents in LinkedChildren and never
 2064                // populate AncestorIds for those items, so a recursive AncestorIds query
 2065                // would return zero rows. Filter by the descendant set instead, which follows
 2066                // the links and keeps descending, so a linked folder contributes what is below
 2067                // it as well - the episodes of a Series added to a collection, for example.
 02068                query.DescendantOfId = folder.Id;
 2069            }
 2070            else
 2071            {
 582072                SetAncestorIds(query, parents);
 2073            }
 2074
 1042075            query.Parent = null;
 1042076        }
 2077
 2078        private static void SetAncestorIds(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 2079        {
 2080            // We need to be able to query from any arbitrary ancestor up the tree
 1042081            query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 2082
 2083            // Prevent searching in all libraries due to empty filter
 1042084            if (query.AncestorIds.Length == 0)
 2085            {
 02086                query.AncestorIds = [Guid.NewGuid()];
 2087            }
 1042088        }
 2089
 2090        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 2091        {
 42092            if (query.User is null)
 2093            {
 02094                query.SetUser(user);
 2095            }
 2096
 42097            if (query.AncestorIds.Length == 0 &&
 42098                query.ParentId.IsEmpty() &&
 42099                query.ChannelIds.Count == 0 &&
 42100                query.TopParentIds.Length == 0 &&
 42101                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 42102                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 42103                query.ItemIds.Length == 0 &&
 42104                query.OwnerIds.Length == 0)
 2105            {
 12106                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 12107                {
 12108                    User = user,
 12109                    IncludeHidden = true,
 12110                    IncludeExternalContent = allowExternalContent
 12111                });
 2112
 12113                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 2114
 2115                // Prevent searching in all libraries due to empty filter
 12116                if (query.TopParentIds.Length == 0)
 2117                {
 12118                    query.TopParentIds = [Guid.NewGuid()];
 2119                }
 2120            }
 42121        }
 2122
 2123        /// <inheritdoc/>
 2124        public void ConfigureUserAccess(InternalItemsQuery query, User user)
 2125        {
 02126            ArgumentNullException.ThrowIfNull(query);
 02127            ArgumentNullException.ThrowIfNull(user);
 2128
 02129            AddUserToQuery(query, user);
 02130        }
 2131
 2132        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 2133        {
 462134            if (item is UserView view)
 2135            {
 02136                if (view.ViewType == CollectionType.livetv)
 2137                {
 02138                    return [view.Id];
 2139                }
 2140
 2141                // Translate view into folders
 02142                if (!view.DisplayParentId.IsEmpty())
 2143                {
 02144                    var displayParent = GetItemById(view.DisplayParentId);
 02145                    if (displayParent is not null)
 2146                    {
 02147                        return GetTopParentIdsForQuery(displayParent, user);
 2148                    }
 2149
 02150                    return [];
 2151                }
 2152
 02153                if (!view.ParentId.IsEmpty())
 2154                {
 02155                    var displayParent = GetItemById(view.ParentId);
 02156                    if (displayParent is not null)
 2157                    {
 02158                        return GetTopParentIdsForQuery(displayParent, user);
 2159                    }
 2160
 02161                    return [];
 2162                }
 2163
 2164                // Handle grouping
 02165                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 02166                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 2167                {
 02168                    return GetUserRootFolder()
 02169                        .GetChildren(user, true)
 02170                        .OfType<CollectionFolder>()
 02171                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 02172                        .Where(i => user.IsFolderGrouped(i.Id))
 02173                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 2174                }
 2175
 02176                return [];
 2177            }
 2178
 462179            if (item is CollectionFolder collectionFolder)
 2180            {
 462181                return collectionFolder.PhysicalFolderIds;
 2182            }
 2183
 02184            var topParent = item.GetTopParent();
 02185            if (topParent is not null)
 2186            {
 02187                return [topParent.Id];
 2188            }
 2189
 02190            return [];
 2191        }
 2192
 2193        /// <summary>
 2194        /// Gets the intros.
 2195        /// </summary>
 2196        /// <param name="item">The item.</param>
 2197        /// <param name="user">The user.</param>
 2198        /// <returns>IEnumerable{System.String}.</returns>
 2199        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 2200        {
 02201            if (IntroProviders.Length == 0)
 2202            {
 02203                return [];
 2204            }
 2205
 02206            var tasks = IntroProviders
 02207                .Select(i => GetIntros(i, item, user));
 2208
 02209            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 2210
 02211            return items
 02212                .SelectMany(i => i)
 02213                .Select(ResolveIntro)
 02214                .Where(i => i is not null)!; // null values got filtered out
 02215        }
 2216
 2217        /// <summary>
 2218        /// Gets the intros.
 2219        /// </summary>
 2220        /// <param name="provider">The provider.</param>
 2221        /// <param name="item">The item.</param>
 2222        /// <param name="user">The user.</param>
 2223        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 2224        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 2225        {
 2226            try
 2227            {
 02228                return await provider.GetIntros(item, user).ConfigureAwait(false);
 2229            }
 02230            catch (Exception ex)
 2231            {
 02232                _logger.LogError(ex, "Error getting intros");
 2233
 02234                return [];
 2235            }
 02236        }
 2237
 2238        /// <summary>
 2239        /// Resolves the intro.
 2240        /// </summary>
 2241        /// <param name="info">The info.</param>
 2242        /// <returns>Video.</returns>
 2243        private Video? ResolveIntro(IntroInfo info)
 2244        {
 02245            Video? video = null;
 2246
 02247            if (info.ItemId.HasValue)
 2248            {
 2249                // Get an existing item by Id
 02250                video = GetItemById(info.ItemId.Value) as Video;
 2251
 02252                if (video is null)
 2253                {
 02254                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 2255                }
 2256            }
 02257            else if (!string.IsNullOrEmpty(info.Path))
 2258            {
 2259                try
 2260                {
 2261                    // Try to resolve the path into a video
 02262                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 2263
 02264                    if (video is null)
 2265                    {
 02266                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 2267                    }
 2268                    else
 2269                    {
 2270                        // Pull the saved db item that will include metadata
 02271                        var dbItem = GetItemById(video.Id) as Video;
 2272
 02273                        if (dbItem is not null)
 2274                        {
 02275                            video = dbItem;
 2276                        }
 2277                        else
 2278                        {
 02279                            return null;
 2280                        }
 2281                    }
 02282                }
 02283                catch (Exception ex)
 2284                {
 02285                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 02286                }
 2287            }
 2288            else
 2289            {
 02290                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 2291            }
 2292
 02293            return video;
 02294        }
 2295
 2296        /// <inheritdoc />
 2297        public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
 2298        {
 82299            ArgumentNullException.ThrowIfNull(video);
 2300
 82301            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 82302            if (linkedIds.Count > 0)
 2303            {
 02304                return linkedIds;
 2305            }
 2306
 82307            return [];
 2308        }
 2309
 2310        /// <inheritdoc />
 2311        public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
 2312        {
 42313            ArgumentNullException.ThrowIfNull(video);
 2314
 42315            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 42316            if (linkedIds.Count > 0)
 2317            {
 02318                return linkedIds
 02319                    .Select(id => GetItemById(id))
 02320                    .Where(i => i is not null)
 02321                    .OfType<Video>()
 02322                    .OrderBy(i => i.SortName);
 2323            }
 2324
 42325            return [];
 2326        }
 2327
 2328        /// <inheritdoc />
 2329        public IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds)
 2330        {
 02331            return _linkedChildrenService.GetItemIdsWithAlternateVersions(itemIds);
 2332        }
 2333
 2334        /// <inheritdoc />
 2335        public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType chil
 2336        {
 02337            _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
 02338        }
 2339
 2340        /// <inheritdoc />
 2341        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 2342        {
 12343            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2344
 42345            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 2346            {
 12347                if (orderBy is RandomComparer)
 2348                {
 02349                    var randomItems = items.ToArray();
 02350                    Random.Shared.Shuffle(randomItems);
 02351                    items = randomItems;
 2352                    // Items are no longer ordered at this point, so set orderedItems back to null
 02353                    orderedItems = null;
 2354                }
 12355                else if (orderedItems is null)
 2356                {
 12357                    orderedItems = sortOrder == SortOrder.Descending
 12358                        ? items.OrderByDescending(i => i, orderBy)
 12359                        : items.OrderBy(i => i, orderBy);
 2360                }
 2361                else
 2362                {
 02363                    orderedItems = sortOrder == SortOrder.Descending
 02364                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 02365                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 2366                }
 2367            }
 2368
 12369            return orderedItems ?? items;
 2370        }
 2371
 2372        /// <inheritdoc />
 2373        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 2374        {
 22375            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2376
 72377            foreach (var (name, sortOrder) in orderBy)
 2378            {
 22379                var comparer = GetComparer(name, user);
 12380                if (comparer is null)
 2381                {
 2382                    continue;
 2383                }
 2384
 12385                if (comparer is RandomComparer)
 2386                {
 02387                    var randomItems = items.ToArray();
 02388                    Random.Shared.Shuffle(randomItems);
 02389                    items = randomItems;
 2390                    // Items are no longer ordered at this point, so set orderedItems back to null
 02391                    orderedItems = null;
 2392                }
 12393                else if (orderedItems is null)
 2394                {
 12395                    orderedItems = sortOrder == SortOrder.Descending
 12396                        ? items.OrderByDescending(i => i, comparer)
 12397                        : items.OrderBy(i => i, comparer);
 2398                }
 2399                else
 2400                {
 02401                    orderedItems = sortOrder == SortOrder.Descending
 02402                        ? orderedItems!.ThenByDescending(i => i, comparer)
 02403                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 2404                }
 2405            }
 2406
 12407            return orderedItems ?? items;
 2408        }
 2409
 2410        /// <summary>
 2411        /// Gets the comparer.
 2412        /// </summary>
 2413        /// <param name="name">The name.</param>
 2414        /// <param name="user">The user.</param>
 2415        /// <returns>IBaseItemComparer.</returns>
 2416        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 2417        {
 32418            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 2419
 32420            if (comparer is IUserBaseItemComparer)
 2421            {
 12422                if (user is null)
 2423                {
 12424                    throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
 2425                }
 2426
 02427                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 2428
 02429                userComparer.User = user;
 02430                userComparer.UserManager = _userManager;
 02431                userComparer.UserDataManager = _userDataManager;
 2432
 02433                return userComparer;
 2434            }
 2435
 22436            return comparer;
 2437        }
 2438
 2439        /// <inheritdoc />
 2440        public void CreateItem(BaseItem item, BaseItem? parent)
 2441        {
 62442            CreateItems([item], parent, CancellationToken.None);
 62443        }
 2444
 2445        /// <inheritdoc />
 2446        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 2447        {
 2448            // Resolve and add any local alternate version items that don't exist yet
 2449            // This ensures they exist in the database when LinkedChildren are processed
 122450            var allItems = new List<BaseItem>(items);
 122451            var parentFolder = parent as Folder;
 122452            var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
 482453            foreach (var item in items)
 2454            {
 2455                // Parental controls filter on the stored score, so it has to match the ratings being saved.
 122456                item.OnMetadataChanged();
 2457
 122458                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2459                {
 02460                    var videoType = video.GetType();
 02461                    foreach (var path in video.LocalAlternateVersions)
 2462                    {
 02463                        if (string.IsNullOrEmpty(path))
 2464                        {
 2465                            continue;
 2466                        }
 2467
 2468                        // Use the primary video's type for ID calculation to ensure consistency
 02469                        var altId = GetNewItemId(path, videoType);
 02470                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2471                        {
 2472                            // Alternate version doesn't exist, resolve and create it
 2473                            // ensuring it has the same type as the primary video
 02474                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02475                            if (altVideo is not null)
 2476                            {
 02477                                altVideo.OwnerId = video.Id;
 02478                                altVideo.SetPrimaryVersionId(video.Id);
 02479                                altVideo.IsInMixedFolder = video.IsInMixedFolder;
 2480                                // ResolveAlternateVersion only sees the alternate's primary file.
 2481                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2482                                // detect its parts from sibling files so its AdditionalParts persist.
 02483                                SetAdditionalPartsFromStack(altVideo, path);
 02484                                allItems.Add(altVideo);
 2485                            }
 2486                        }
 2487                    }
 2488                }
 2489            }
 2490
 122491            _persistenceService.SaveItems(allItems, cancellationToken);
 2492
 482493            foreach (var item in allItems)
 2494            {
 122495                RegisterItem(item);
 2496            }
 2497
 122498            if (parent is Folder folder)
 2499            {
 62500                folder.Children = null;
 62501                folder.UserData = null;
 2502            }
 2503
 122504            if (ItemAdded is not null)
 2505            {
 442506                foreach (var item in items)
 2507                {
 2508                    // With the live tv guide this just creates too much noise
 112509                    if (item.SourceType != SourceType.Library)
 2510                    {
 2511                        continue;
 2512                    }
 2513
 2514                    try
 2515                    {
 112516                        ItemAdded(
 112517                            this,
 112518                            new ItemChangeEventArgs
 112519                            {
 112520                                Item = item,
 112521                                Parent = parent ?? item.GetParent()
 112522                            });
 112523                    }
 02524                    catch (Exception ex)
 2525                    {
 02526                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02527                    }
 2528                }
 2529            }
 122530        }
 2531
 2532        private bool ImageNeedsRefresh(ItemImageInfo image)
 2533        {
 02534            if (image.Path is not null && image.IsLocalFile)
 2535            {
 02536                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2537                {
 02538                    return true;
 2539                }
 2540
 2541                try
 2542                {
 02543                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2544                }
 02545                catch (Exception ex)
 2546                {
 02547                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02548                    return false;
 2549                }
 2550            }
 2551
 02552            return image.Path is not null && !image.IsLocalFile;
 02553        }
 2554
 2555        /// <inheritdoc />
 2556        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2557        {
 2352558            ArgumentNullException.ThrowIfNull(item);
 2559
 2352560            var outdated = forceUpdate
 2352561                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 2352562                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2563
 2352564            var parentItem = item.GetParent();
 2352565            var isLiveTvShow = item.SourceType != SourceType.Library &&
 2352566                               parentItem is not null &&
 2352567                               parentItem.SourceType != SourceType.Library; // not a channel
 2568
 2569            // Skip image processing if current or live tv show
 2352570            if (outdated.Length == 0 || isLiveTvShow)
 2571            {
 2352572                RegisterItem(item);
 2352573                return;
 2574            }
 2575
 02576            foreach (var img in outdated)
 2577            {
 02578                var image = img;
 02579                if (!img.IsLocalFile)
 2580                {
 2581                    try
 2582                    {
 02583                        var index = item.GetImageIndex(img);
 02584                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 02585                    }
 02586                    catch (ArgumentException)
 2587                    {
 02588                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 02589                        continue;
 2590                    }
 02591                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2592                    {
 02593                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 02594                        continue;
 2595                    }
 02596                    catch (HttpRequestException ex)
 2597                    {
 02598                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 02599                        continue;
 2600                    }
 2601                }
 2602
 02603                if (string.IsNullOrEmpty(image.Path) || !File.Exists(image.Path))
 2604                {
 02605                    _logger.LogWarning(
 02606                        "{ImageType} image for {ItemName} ({ItemId}) not found at \"{ImagePath}\", source was {SourcePat
 02607                        img.Type,
 02608                        item.Name,
 02609                        item.Id,
 02610                        image.Path,
 02611                        img.Path);
 02612                    continue;
 2613                }
 2614
 2615                ImageDimensions size;
 2616                try
 2617                {
 02618                    size = _imageProcessor.GetImageDimensions(item, image);
 02619                    image.Width = size.Width;
 02620                    image.Height = size.Height;
 02621                }
 02622                catch (Exception ex)
 2623                {
 02624                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 02625                    size = default;
 02626                    image.Width = 0;
 02627                    image.Height = 0;
 02628                }
 2629
 2630                try
 2631                {
 02632                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 02633                    image.BlurHash = blurhash;
 02634                }
 02635                catch (Exception ex)
 2636                {
 02637                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 02638                    image.BlurHash = string.Empty;
 02639                }
 2640
 2641                try
 2642                {
 02643                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 02644                    image.DateModified = modifiedDate;
 02645                }
 02646                catch (Exception ex)
 2647                {
 02648                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 02649                }
 02650            }
 2651
 02652            item.ValidateImages();
 2653
 02654            await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
 2655
 02656            RegisterItem(item);
 2352657        }
 2658
 2659        /// <inheritdoc />
 2660        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2661        {
 5962662            foreach (var item in items)
 2663            {
 2664                // Parental controls filter on the stored score, so it has to match the ratings being saved.
 1492665                item.OnMetadataChanged();
 2666
 1492667                item.DateLastSaved = DateTime.UtcNow;
 1492668                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2669
 2670                // Modify again, so saved value is after write time of externally saved metadata
 1492671                item.DateLastSaved = DateTime.UtcNow;
 1492672            }
 2673
 1492674            ForgetDroppedLocalAlternateVersions(items);
 2675
 2676            // Resolve and add any local alternate version items that don't exist yet
 2677            // This ensures they exist in the database when LinkedChildren are processed
 1492678            var allItems = new List<BaseItem>(items);
 1492679            var parentFolder = parent as Folder;
 1492680            var parentCollectionType = GetTopFolderContentType(parent);
 5962681            foreach (var item in items)
 2682            {
 1492683                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2684                {
 02685                    var videoType = video.GetType();
 02686                    foreach (var path in video.LocalAlternateVersions)
 2687                    {
 02688                        if (string.IsNullOrEmpty(path))
 2689                        {
 2690                            continue;
 2691                        }
 2692
 2693                        // Use the primary video's type for ID calculation to ensure consistency
 02694                        var altId = GetNewItemId(path, videoType);
 02695                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2696                        {
 2697                            // Alternate version doesn't exist, resolve and create it
 2698                            // ensuring it has the same type as the primary video
 02699                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02700                            if (altVideo is not null)
 2701                            {
 02702                                altVideo.OwnerId = video.Id;
 02703                                altVideo.SetPrimaryVersionId(video.Id);
 02704                                altVideo.IsInMixedFolder = video.IsInMixedFolder;
 2705                                // ResolveAlternateVersion only sees the alternate's primary file.
 2706                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2707                                // detect its parts from sibling files so its AdditionalParts persist.
 02708                                SetAdditionalPartsFromStack(altVideo, path);
 02709                                allItems.Add(altVideo);
 2710                            }
 2711                        }
 2712                    }
 2713                }
 2714            }
 2715
 1492716            _persistenceService.SaveItems(allItems, cancellationToken);
 2717
 5962718            foreach (var item in allItems)
 2719            {
 1492720                if (!items.Contains(item))
 2721                {
 02722                    RegisterItem(item);
 2723                }
 2724            }
 2725
 1492726            if (parent is Folder folder)
 2727            {
 412728                folder.Children = null;
 412729                folder.UserData = null;
 2730            }
 2731
 1492732            if (ItemUpdated is not null)
 2733            {
 5882734                foreach (var item in items)
 2735                {
 2736                    // With the live tv guide this just creates too much noise
 1472737                    if (item.SourceType != SourceType.Library)
 2738                    {
 2739                        continue;
 2740                    }
 2741
 2742                    try
 2743                    {
 1472744                        ItemUpdated(
 1472745                            this,
 1472746                            new ItemChangeEventArgs
 1472747                            {
 1472748                                Item = item,
 1472749                                Parent = parent,
 1472750                                UpdateReason = updateReason
 1472751                            });
 1472752                    }
 02753                    catch (Exception ex)
 2754                    {
 02755                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 02756                    }
 2757                }
 2758            }
 1492759        }
 2760
 2761        /// <inheritdoc />
 2762        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 1492763            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2764
 2765        /// <summary>
 2766        /// Forgets the cached local alternate versions of the supplied items that they no longer list.
 2767        /// </summary>
 2768        /// <param name="items">The items about to be saved.</param>
 2769        private void ForgetDroppedLocalAlternateVersions(IReadOnlyList<BaseItem> items)
 2770        {
 3022771            foreach (var video in items.OfType<Video>())
 2772            {
 22773                var videoType = video.GetType();
 22774                var keptIds = video.LocalAlternateVersions
 22775                    .Where(path => !string.IsNullOrEmpty(path))
 22776                    .Select(path => GetNewItemId(path, videoType))
 22777                    .ToHashSet();
 2778
 42779                foreach (var versionId in GetLocalAlternateVersionIds(video))
 2780                {
 02781                    if (!keptIds.Contains(versionId))
 2782                    {
 02783                        _cache.TryRemove(versionId, out _);
 2784                    }
 2785                }
 2786            }
 1492787        }
 2788
 2789        /// <inheritdoc />
 2790        public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
 2791        {
 462792            await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
 462793        }
 2794
 2795        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2796        {
 1492797            if (item.IsFileProtocol)
 2798            {
 1472799                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2800            }
 2801
 1492802            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 1492803        }
 2804
 2805        /// <summary>
 2806        /// Reports the item removed.
 2807        /// </summary>
 2808        /// <param name="item">The item.</param>
 2809        /// <param name="parent">The parent item.</param>
 2810        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2811        {
 22812            if (ItemRemoved is not null)
 2813            {
 2814                try
 2815                {
 02816                    ItemRemoved(
 02817                        this,
 02818                        new ItemChangeEventArgs
 02819                        {
 02820                            Item = item,
 02821                            Parent = parent
 02822                        });
 02823                }
 02824                catch (Exception ex)
 2825                {
 02826                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02827                }
 2828            }
 22829        }
 2830
 2831        /// <summary>
 2832        /// Retrieves the item.
 2833        /// </summary>
 2834        /// <param name="id">The id.</param>
 2835        /// <returns>BaseItem.</returns>
 2836        public BaseItem RetrieveItem(Guid id)
 2837        {
 1572838            return _itemRepository.RetrieveItem(id);
 2839        }
 2840
 2841        public List<Folder> GetCollectionFolders(BaseItem item)
 2842        {
 10272843            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2844        }
 2845
 2846        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2847        {
 10822848            while (item is not null)
 2849            {
 10822850                var parent = item.GetParent();
 2851
 10822852                if (parent is AggregateFolder)
 2853                {
 2854                    break;
 2855                }
 2856
 9812857                if (parent is null)
 2858                {
 9262859                    var owner = item.GetOwner();
 2860
 9262861                    if (owner is null)
 2862                    {
 2863                        break;
 2864                    }
 2865
 02866                    item = owner;
 2867                }
 2868                else
 2869                {
 552870                    item = parent;
 2871                }
 2872            }
 2873
 10272874            if (item is null)
 2875            {
 02876                return [];
 2877            }
 2878
 10272879            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2880        }
 2881
 2882        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2883        {
 10272884            return allUserRootChildren
 10272885                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 10272886                .ToList();
 2887        }
 2888
 2889        public LibraryOptions GetLibraryOptions(BaseItem item)
 2890        {
 6702891            if (item is CollectionFolder collectionFolder)
 2892            {
 1172893                return collectionFolder.GetLibraryOptions();
 2894            }
 2895
 2896            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 5532897            return GetCollectionFolders(item)
 5532898                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 5532899                ? collectionFolder2.GetLibraryOptions()
 5532900                : new LibraryOptions();
 2901        }
 2902
 2903        public CollectionType? GetContentType(BaseItem item)
 2904        {
 782905            var configuredContentType = GetConfiguredContentType(item, false);
 782906            if (configuredContentType is not null)
 2907            {
 02908                return configuredContentType;
 2909            }
 2910
 782911            configuredContentType = GetConfiguredContentType(item, true);
 782912            if (configuredContentType is not null)
 2913            {
 02914                return configuredContentType;
 2915            }
 2916
 782917            return GetInheritedContentType(item);
 2918        }
 2919
 2920        public CollectionType? GetInheritedContentType(BaseItem item)
 2921        {
 782922            var type = GetTopFolderContentType(item);
 2923
 782924            if (type is not null)
 2925            {
 02926                return type;
 2927            }
 2928
 782929            return item.GetParents()
 782930                .Select(GetConfiguredContentType)
 782931                .LastOrDefault(i => i is not null);
 2932        }
 2933
 2934        public CollectionType? GetConfiguredContentType(BaseItem item)
 2935        {
 02936            return GetConfiguredContentType(item, false);
 2937        }
 2938
 2939        public CollectionType? GetConfiguredContentType(string path)
 2940        {
 02941            return GetContentTypeOverride(path, false);
 2942        }
 2943
 2944        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2945        {
 1562946            if (item is ICollectionFolder collectionFolder)
 2947            {
 02948                return collectionFolder.CollectionType;
 2949            }
 2950
 1562951            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2952        }
 2953
 2954        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2955        {
 2052956            var nameValuePair = _configurationManager.Configuration.ContentTypes
 2052957                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 2052958                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 2052959                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 2052960            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2961            {
 02962                return collectionType;
 2963            }
 2964
 2052965            return null;
 2966        }
 2967
 2968        private CollectionType? GetTopFolderContentType(BaseItem item)
 2969        {
 2332970            if (item is null)
 2971            {
 1082972                return null;
 2973            }
 2974
 1252975            while (!item.ParentId.IsEmpty())
 2976            {
 02977                var parent = item.GetParent();
 02978                if (parent is null || parent is AggregateFolder)
 2979                {
 2980                    break;
 2981                }
 2982
 02983                item = parent;
 2984            }
 2985
 1252986            return GetUserRootFolder().Children
 1252987                .OfType<ICollectionFolder>()
 1252988                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 1252989                .Select(i => i.CollectionType)
 1252990                .FirstOrDefault(i => i is not null);
 2991        }
 2992
 2993        public UserView GetNamedView(
 2994            User user,
 2995            string name,
 2996            CollectionType? viewType,
 2997            string sortName)
 2998        {
 02999            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 3000        }
 3001
 3002        public UserView GetNamedView(
 3003            string name,
 3004            CollectionType viewType,
 3005            string sortName)
 3006        {
 03007            var path = Path.Combine(
 03008                _configurationManager.ApplicationPaths.InternalMetadataPath,
 03009                "views",
 03010                _fileSystem.GetValidFilename(viewType.ToString()));
 3011
 3012            // The display name is localized, so it must not take part in the id.
 03013            var id = GetNewItemId(path + "_namedview_" + viewType.ToString(), typeof(UserView));
 3014
 03015            var item = GetItemById(id) as UserView;
 3016
 03017            var refresh = false;
 3018
 03019            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 3020            {
 03021                var info = Directory.CreateDirectory(path);
 03022                item = new UserView
 03023                {
 03024                    Path = path,
 03025                    Id = id,
 03026                    DateCreated = info.CreationTimeUtc,
 03027                    DateModified = info.LastWriteTimeUtc,
 03028                    Name = name,
 03029                    ViewType = viewType,
 03030                    ForcedSortName = sortName
 03031                };
 3032
 03033                CreateItem(item, null);
 3034
 03035                refresh = true;
 3036            }
 03037            else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
 3038            {
 03039                item.Name = name;
 03040                item.ForcedSortName = sortName;
 3041
 03042                refresh = true;
 3043            }
 3044
 03045            if (refresh)
 3046            {
 03047                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 03048                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 3049            }
 3050
 03051            return item;
 3052        }
 3053
 3054        public UserView GetNamedView(
 3055            User user,
 3056            string name,
 3057            Guid parentId,
 3058            CollectionType? viewType,
 3059            string sortName)
 3060        {
 23061            var id = GetNamedViewId(user, parentId, viewType);
 3062
 23063            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 3064
 23065            var item = GetItemById(id) as UserView;
 3066
 23067            var isNew = false;
 3068
 23069            if (item is null)
 3070            {
 23071                var info = Directory.CreateDirectory(path);
 23072                item = new UserView
 23073                {
 23074                    Path = path,
 23075                    Id = id,
 23076                    DateCreated = info.CreationTimeUtc,
 23077                    DateModified = info.LastWriteTimeUtc,
 23078                    Name = name,
 23079                    ViewType = viewType,
 23080                    ForcedSortName = sortName,
 23081                    UserId = user.Id,
 23082                    DisplayParentId = parentId
 23083                };
 3084
 23085                CreateItem(item, null);
 3086
 23087                isNew = true;
 3088            }
 03089            else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
 3090            {
 03091                item.Name = name;
 03092                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 3093            }
 3094
 23095            var lastRefreshedUtc = item.DateLastRefreshed;
 23096            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3097
 23098            if (!refresh && !item.DisplayParentId.IsEmpty())
 3099            {
 03100                var displayParent = GetItemById(item.DisplayParentId);
 03101                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3102            }
 3103
 23104            if (refresh)
 3105            {
 23106                ProviderManager.QueueRefresh(
 23107                    item.Id,
 23108                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 23109                    {
 23110                        // Need to force save to increment DateLastSaved
 23111                        ForceSave = true
 23112                    },
 23113                    RefreshPriority.Normal);
 3114            }
 3115
 23116            return item;
 3117        }
 3118
 3119        public Guid GetNamedViewId(User user, Guid parentId, CollectionType? viewType)
 3120        {
 123121            var parentIdString = parentId.IsEmpty()
 123122                ? null
 123123                : parentId.ToString("N", CultureInfo.InvariantCulture);
 3124
 3125            // The name is either localized (grouped views) or the library folder's own name.
 123126            var idValues = "38_namedview_" + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? st
 3127
 123128            return GetNewItemId(idValues, typeof(UserView));
 3129        }
 3130
 3131        public UserView GetShadowView(
 3132            BaseItem parent,
 3133            CollectionType? viewType,
 3134            string sortName)
 3135        {
 33136            ArgumentNullException.ThrowIfNull(parent);
 3137
 33138            var name = parent.Name;
 33139            var parentId = parent.Id;
 33140            var id = GetShadowViewId(name, parentId, viewType);
 3141
 33142            var path = parent.Path;
 3143
 33144            var item = GetItemById(id) as UserView;
 3145
 33146            var isNew = false;
 3147
 33148            if (item is null)
 3149            {
 33150                var info = Directory.CreateDirectory(path);
 33151                item = new UserView
 33152                {
 33153                    Path = path,
 33154                    Id = id,
 33155                    DateCreated = info.CreationTimeUtc,
 33156                    DateModified = info.LastWriteTimeUtc,
 33157                    Name = name,
 33158                    ViewType = viewType,
 33159                    ForcedSortName = sortName,
 33160                    DisplayParentId = parentId
 33161                };
 3162
 33163                CreateItem(item, null);
 3164
 33165                isNew = true;
 3166            }
 3167
 33168            var lastRefreshedUtc = item.DateLastRefreshed;
 33169            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3170
 33171            if (!refresh && !item.DisplayParentId.IsEmpty())
 3172            {
 03173                var displayParent = GetItemById(item.DisplayParentId);
 03174                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3175            }
 3176
 33177            if (refresh)
 3178            {
 33179                ProviderManager.QueueRefresh(
 33180                    item.Id,
 33181                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 33182                    {
 33183                        // Need to force save to increment DateLastSaved
 33184                        ForceSave = true
 33185                    },
 33186                    RefreshPriority.Normal);
 3187            }
 3188
 33189            return item;
 3190        }
 3191
 3192        public Guid GetShadowViewId(string name, Guid parentId, CollectionType? viewType)
 3193        {
 83194            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 3195
 83196            return GetNewItemId(idValues, typeof(UserView));
 3197        }
 3198
 3199        public UserView GetNamedView(
 3200            string name,
 3201            Guid parentId,
 3202            CollectionType? viewType,
 3203            string sortName,
 3204            string uniqueId)
 3205        {
 03206            ArgumentException.ThrowIfNullOrEmpty(name);
 3207
 03208            var parentIdString = parentId.IsEmpty()
 03209                ? null
 03210                : parentId.ToString("N", CultureInfo.InvariantCulture);
 03211            var idValues = "37_namedview_" + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
 03212            if (!string.IsNullOrEmpty(uniqueId))
 3213            {
 03214                idValues += uniqueId;
 3215            }
 3216
 03217            var id = GetNewItemId(idValues, typeof(UserView));
 3218
 03219            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 3220
 03221            var item = GetItemById(id) as UserView;
 3222
 03223            var isNew = false;
 3224
 03225            if (item is null)
 3226            {
 03227                var info = Directory.CreateDirectory(path);
 03228                item = new UserView
 03229                {
 03230                    Path = path,
 03231                    Id = id,
 03232                    DateCreated = info.CreationTimeUtc,
 03233                    DateModified = info.LastWriteTimeUtc,
 03234                    Name = name,
 03235                    ViewType = viewType,
 03236                    ForcedSortName = sortName,
 03237                    DisplayParentId = parentId
 03238                };
 3239
 03240                CreateItem(item, null);
 3241
 03242                isNew = true;
 3243            }
 3244
 03245            if (viewType != item.ViewType || !string.Equals(item.Name, name, StringComparison.Ordinal))
 3246            {
 03247                item.ViewType = viewType;
 03248                item.Name = name;
 03249                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 3250            }
 3251
 03252            var lastRefreshedUtc = item.DateLastRefreshed;
 03253            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3254
 03255            if (!refresh && !item.DisplayParentId.IsEmpty())
 3256            {
 03257                var displayParent = GetItemById(item.DisplayParentId);
 03258                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3259            }
 3260
 03261            if (refresh)
 3262            {
 03263                ProviderManager.QueueRefresh(
 03264                    item.Id,
 03265                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03266                    {
 03267                        // Need to force save to increment DateLastSaved
 03268                        ForceSave = true
 03269                    },
 03270                    RefreshPriority.Normal);
 3271            }
 3272
 03273            return item;
 3274        }
 3275
 3276        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 3277        {
 33278            if (parentId.HasValue)
 3279            {
 03280                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 3281            }
 3282
 33283            if (!userId.IsNullOrEmpty())
 3284            {
 33285                return GetUserRootFolder();
 3286            }
 3287
 03288            return RootFolder;
 3289        }
 3290
 3291        /// <inheritdoc />
 3292        public void QueueLibraryScan()
 3293        {
 223294            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 223295        }
 3296
 3297        /// <inheritdoc />
 3298        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 3299        {
 03300            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 03301            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 3302        }
 3303
 3304        /// <inheritdoc />
 3305        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 3306        {
 03307            var series = episode.Series;
 03308            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 03309            if (!isAbsoluteNaming.Value)
 3310            {
 3311                // In other words, no filter applied
 03312                isAbsoluteNaming = null;
 3313            }
 3314
 03315            var resolver = new EpisodeResolver(_namingOptions);
 3316
 03317            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 3318
 03319            EpisodeInfo? episodeInfo = null;
 03320            if (episode.IsFileProtocol)
 3321            {
 03322                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 3323                // Resolve from parent folder if it's not the Season folder
 03324                var parent = episode.GetParent();
 03325                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 3326                {
 03327                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 03328                    if (episodeInfo is not null)
 3329                    {
 3330                        // add the container
 03331                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 3332                    }
 3333                }
 3334            }
 3335
 03336            var changed = false;
 03337            if (episodeInfo is null)
 3338            {
 03339                return changed;
 3340            }
 3341
 03342            if (episodeInfo.IsByDate)
 3343            {
 03344                if (episode.IndexNumber.HasValue)
 3345                {
 03346                    episode.IndexNumber = null;
 03347                    changed = true;
 3348                }
 3349
 03350                if (episode.IndexNumberEnd.HasValue)
 3351                {
 03352                    episode.IndexNumberEnd = null;
 03353                    changed = true;
 3354                }
 3355
 03356                if (!episode.PremiereDate.HasValue)
 3357                {
 03358                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 3359                    {
 03360                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 3361                    }
 3362
 03363                    if (episode.PremiereDate.HasValue)
 3364                    {
 03365                        changed = true;
 3366                    }
 3367                }
 3368
 03369                if (episode.ProductionYear is null)
 3370                {
 03371                    episode.ProductionYear = episodeInfo.Year;
 3372
 03373                    if (episode.ProductionYear is not null)
 3374                    {
 03375                        changed = true;
 3376                    }
 3377                }
 3378            }
 3379            else
 3380            {
 03381                if (!episode.IndexNumber.HasValue || forceRefresh)
 3382                {
 03383                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 3384                    {
 03385                        changed = true;
 3386                    }
 3387
 03388                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 3389                }
 3390
 03391                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 3392                {
 03393                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 3394                    {
 03395                        changed = true;
 3396                    }
 3397
 03398                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 3399                }
 3400
 03401                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 3402                {
 03403                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 3404                    {
 03405                        changed = true;
 3406                    }
 3407
 03408                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 3409                }
 3410            }
 3411
 03412            if (!episode.ParentIndexNumber.HasValue)
 3413            {
 03414                var season = episode.Season;
 3415
 03416                if (season is not null)
 3417                {
 03418                    episode.ParentIndexNumber = season.IndexNumber;
 3419                }
 3420
 03421                if (episode.ParentIndexNumber.HasValue)
 3422                {
 03423                    changed = true;
 3424                }
 3425            }
 3426
 03427            return changed;
 3428        }
 3429
 3430        public ItemLookupInfo ParseName(string name)
 3431        {
 03432            var namingOptions = _namingOptions;
 03433            var result = VideoResolver.CleanDateTime(name, namingOptions);
 3434
 03435            return new ItemLookupInfo
 03436            {
 03437                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 03438                Year = result.Year
 03439            };
 3440        }
 3441
 3442        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 3443        {
 3444            // Apply .ignore rules
 183445            var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
 183446            var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.Vide
 183447            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.Containi
 183448            if (ownerVideoInfo is null)
 3449            {
 03450                return [];
 3451            }
 3452
 183453            var candidates = new List<ExtraCandidate>();
 3454
 183455            var count = filtered.Count;
 1683456            for (var i = 0; i < count; i++)
 3457            {
 663458                var current = filtered[i];
 663459                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 3460                {
 83461                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 83462                    var filesInSubFolderList = filesInSubFolder.ToList();
 3463
 83464                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 3465
 383466                    foreach (var file in filesInSubFolderList)
 3467                    {
 113468                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType, ou
 3469                        {
 3470                            continue;
 3471                        }
 3472
 103473                        AddCandidate(file, extraType.Value, extraRule, subFolderIsMixedFolder);
 3474                    }
 3475                }
 583476                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 3477                {
 313478                    AddCandidate(current, extraType.Value, extraRule, false);
 3479                }
 3480            }
 3481
 183482            var extras = new List<BaseItem>();
 183483            var typeCounters = new Dictionary<ExtraType, int>();
 183484            var generatedNames = new Dictionary<ExtraType, HashSet<string>>();
 3485
 3486            // Order by path so that the numbering handed out below does not depend on the
 3487            // order the file system happened to list the folder in
 1183488            foreach (var candidate in candidates.OrderBy(c => c.Extra.Path, StringComparer.Ordinal))
 3489            {
 413490                var extra = PrepareExtra(candidate);
 413491                if (extra is not null)
 3492                {
 413493                    extras.Add(extra);
 3494                }
 3495            }
 3496
 183497            return extras;
 3498
 3499            void AddCandidate(FileSystemMetadata file, ExtraType extraType, ExtraRule extraRule, bool isInMixedFolder)
 3500            {
 3501                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 3502                if (extra is Video or Audio)
 3503                {
 3504                    candidates.Add(new ExtraCandidate(extra, extraType, extraRule, isInMixedFolder));
 3505                }
 3506            }
 3507
 3508            BaseItem? PrepareExtra(ExtraCandidate candidate)
 3509            {
 3510                var resolved = candidate.Extra;
 3511                var extra = resolved;
 3512                var name = GetExtraName(candidate, ownerVideoInfo, typeCounters);
 3513
 3514                // Try to retrieve it from the db. If we don't find it, use the resolved version
 3515                var itemById = GetItemById(extra.Id);
 3516                if (itemById is not null)
 3517                {
 3518                    extra = itemById;
 3519                }
 3520
 3521                // The name derived from the file is only a default. A name that came from anywhere else,
 3522                // such as a local metadata file, is the user's and has to survive the scan, so only a
 3523                // name this method handed out itself is renewed; RefreshExtras persists the change.
 3524                if (!string.IsNullOrEmpty(name)
 3525                    && extra.LockedFields?.Contains(MetadataField.Name) != true
 3526                    && (itemById is null || IsGeneratedExtraName(extra.Name, candidate)))
 3527                {
 3528                    extra.Name = name;
 3529                }
 3530
 3531                // Only update extra type if it is more specific then the currently known extra type
 3532                if (extra.ExtraType is null or ExtraType.Unknown || candidate.ExtraType != ExtraType.Unknown)
 3533                {
 3534                    extra.ExtraType = candidate.ExtraType;
 3535                }
 3536
 3537                // Only return items that are actual extras (have ExtraType set)
 3538                // Note: OwnerId and ParentId are set by RefreshExtras, not here,
 3539                // so that RefreshExtras can detect when they need updating and set ForceSave.
 3540                if (extra.ExtraType is not null)
 3541                {
 3542                    extra.IsInMixedFolder = candidate.IsInMixedFolder;
 3543                    return extra;
 3544                }
 3545
 3546                return null;
 3547            }
 3548
 3549            bool IsGeneratedExtraName(string currentName, ExtraCandidate candidate)
 3550            {
 3551                // The file name is what an extra was called before it was given a name of its type
 3552                if (string.Equals(currentName, candidate.Extra.Name, StringComparison.Ordinal))
 3553                {
 3554                    return true;
 3555                }
 3556
 3557                if (!generatedNames.TryGetValue(candidate.ExtraType, out var names))
 3558                {
 3559                    // Any of the numbers of this type may have been handed out, as the order the extras
 3560                    // of a type are numbered in shifts as files appear beside them or are taken away
 3561                    names = new HashSet<string>(StringComparer.Ordinal);
 3562                    var count = candidates.Count(c => c.ExtraType == candidate.ExtraType);
 3563                    for (var seen = 0; seen < count; seen++)
 3564                    {
 3565                        names.Add(GetNumberedExtraName(candidate.ExtraType, seen));
 3566                    }
 3567
 3568                    generatedNames[candidate.ExtraType] = names;
 3569                }
 3570
 3571                return names.Contains(currentName);
 3572            }
 3573        }
 3574
 3575        /// <summary>
 3576        /// Gets the name to give an extra.
 3577        /// </summary>
 3578        /// <param name="candidate">The resolved extra.</param>
 3579        /// <param name="ownerVideoInfo">The naming info of the owner.</param>
 3580        /// <param name="typeCounters">Number of extras named after their type so far, per type.</param>
 3581        /// <returns>The name.</returns>
 3582        private string GetExtraName(ExtraCandidate candidate, VideoFileInfo ownerVideoInfo, Dictionary<ExtraType, int> t
 3583        {
 413584            var isNamedAfterOwner = candidate.ExtraRule.RuleType switch
 413585            {
 43586                ExtraRuleType.Filename => true,
 253587                ExtraRuleType.Suffix => string.Equals(candidate.Extra.Name, ownerVideoInfo.Name, StringComparison.Ordina
 123588                _ => false
 413589            };
 3590
 413591            if (!isNamedAfterOwner)
 3592            {
 193593                return candidate.Extra.Name;
 3594            }
 3595
 223596            typeCounters.TryGetValue(candidate.ExtraType, out var seen);
 223597            typeCounters[candidate.ExtraType] = seen + 1;
 3598
 223599            return GetNumberedExtraName(candidate.ExtraType, seen);
 3600        }
 3601
 3602        /// <summary>
 3603        /// Gets the name given to the n-th extra of a type that is named after its type.
 3604        /// </summary>
 3605        /// <param name="extraType">The extra type.</param>
 3606        /// <param name="seen">Number of extras of the type named before this one.</param>
 3607        /// <returns>The name.</returns>
 3608        private string GetNumberedExtraName(ExtraType extraType, int seen)
 3609        {
 253610            var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(extraType));
 3611
 253612            return seen == 0
 253613                ? typeName
 253614                : string.Format(
 253615                    CultureInfo.InvariantCulture,
 253616                    _localization.GetServerLocalizedString("NameExtraNumbered"),
 253617                    typeName,
 253618                    seen + 1);
 3619        }
 3620
 253621        private static string GetExtraTypeNameKey(ExtraType extraType) => extraType switch
 253622        {
 03623            ExtraType.Clip => "NameExtraClip",
 203624            ExtraType.Trailer => "NameExtraTrailer",
 33625            ExtraType.BehindTheScenes => "NameExtraBehindTheScenes",
 03626            ExtraType.DeletedScene => "NameExtraDeletedScene",
 03627            ExtraType.Interview => "NameExtraInterview",
 03628            ExtraType.Scene => "NameExtraScene",
 03629            ExtraType.Sample => "NameExtraSample",
 13630            ExtraType.ThemeSong => "NameExtraThemeSong",
 03631            ExtraType.ThemeVideo => "NameExtraThemeVideo",
 03632            ExtraType.Featurette => "NameExtraFeaturette",
 03633            ExtraType.Short => "NameExtraShort",
 13634            _ => "NameExtraUnknown"
 253635        };
 3636
 3637        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 3638        {
 123639            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 3640            {
 03641                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 3642                {
 03643                    return newPath;
 3644                }
 3645            }
 3646
 63647            return path;
 3648        }
 3649
 3650        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 3651        {
 03652            return _peopleRepository.GetPeople(query).Items;
 3653        }
 3654
 3655        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 3656        {
 63657            if (item.SupportsPeople)
 3658            {
 03659                var people = GetPeople(new InternalPeopleQuery
 03660                {
 03661                    ItemId = item.Id
 03662                });
 3663
 03664                if (people.Count > 0)
 3665                {
 03666                    return people;
 3667                }
 3668            }
 3669
 63670            return [];
 3671        }
 3672
 3673        public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
 3674        {
 03675            ArgumentNullException.ThrowIfNull(query);
 3676
 3677            // This hands back by-name items, so the people without one are not ours to report.
 03678            query.MustHaveItem = true;
 3679
 03680            var queryResult = _peopleRepository.GetPeople(query);
 03681            var baseItems = queryResult.Items.Select(i =>
 03682                {
 03683                    try
 03684                    {
 03685                        return GetPerson(i.Name);
 03686                    }
 03687                    catch (Exception ex)
 03688                    {
 03689                        _logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
 03690                        return null;
 03691                    }
 03692                })
 03693                .Where(i => i is not null)
 03694                .Where(i => query.User is null || i!.IsVisible(query.User))
 03695                .OfType<BaseItem>()
 03696                .ToList()
 03697                .AsReadOnly();
 3698
 03699            return new QueryResult<BaseItem>
 03700            {
 03701                StartIndex = queryResult.StartIndex,
 03702                TotalRecordCount = queryResult.TotalRecordCount,
 03703                Items = baseItems,
 03704            };
 3705        }
 3706
 3707        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 3708        {
 03709            return _peopleRepository.GetPeopleNames(query);
 3710        }
 3711
 3712        /// <inheritdoc/>
 3713        public int DeleteOrphanedCredits()
 3714        {
 03715            return _peopleRepository.DeleteOrphanedCredits();
 3716        }
 3717
 3718        /// <inheritdoc/>
 3719        public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IRead
 3720        {
 03721            return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
 3722        }
 3723
 3724        /// <inheritdoc/>
 3725        public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
 3726        {
 03727            return _peopleRepository.GetPeopleByItems(itemIds);
 3728        }
 3729
 3730        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 3731        {
 03732            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 03733        }
 3734
 3735        /// <inheritdoc />
 3736        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 3737        {
 03738            if (!item.SupportsPeople)
 3739            {
 03740                return;
 3741            }
 3742
 03743            if (people is not null)
 3744            {
 03745                people = people.Where(e => e is not null).ToArray();
 03746                _peopleRepository.UpdatePeople(item.Id, people);
 03747                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 3748            }
 03749        }
 3750
 3751        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 3752        {
 03753            foreach (var url in image.Path.Split('|'))
 3754            {
 3755                try
 3756                {
 03757                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 3758
 03759                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 3760
 03761                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 3762
 03763                    var localImage = item.GetImageInfo(image.Type, imageIndex);
 03764                    if (localImage is null)
 3765                    {
 03766                        throw new InvalidOperationException(string.Format(
 03767                            CultureInfo.InvariantCulture,
 03768                            "Downloaded {0} image {1} from {2} is not attached to {3} ({4})",
 03769                            image.Type,
 03770                            imageIndex,
 03771                            url,
 03772                            item.Name,
 03773                            item.Id));
 3774                    }
 3775
 03776                    return localImage;
 3777                }
 03778                catch (HttpRequestException ex)
 3779                {
 03780                    if (ex.StatusCode.HasValue
 03781                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 3782                    {
 03783                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 03784                        continue;
 3785                    }
 3786
 03787                    throw;
 3788                }
 3789            }
 3790
 03791            if (removeOnFailure)
 3792            {
 3793                // Remove this image to prevent it from retrying over and over
 03794                item.RemoveImage(image);
 03795                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 3796            }
 3797
 03798            throw new InvalidOperationException(string.Format(
 03799                CultureInfo.InvariantCulture,
 03800                "Unable to convert any {0} image url in \"{1}\" to a local file for {2} ({3})",
 03801                image.Type,
 03802                image.Path,
 03803                item.Name,
 03804                item.Id));
 03805        }
 3806
 3807        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3808        {
 53809            if (string.IsNullOrWhiteSpace(name))
 3810            {
 03811                throw new ArgumentNullException(nameof(name));
 3812            }
 3813
 53814            name = _fileSystem.GetValidFilename(name.Trim());
 3815
 53816            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3817
 53818            var existingNameCount = 1; // first numbered name will be 2
 53819            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 53820            var originalName = name;
 63821            while (Directory.Exists(virtualFolderPath))
 3822            {
 13823                existingNameCount++;
 13824                name = originalName + existingNameCount;
 13825                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3826            }
 3827
 53828            var mediaPathInfos = options.PathInfos;
 53829            if (mediaPathInfos is not null)
 3830            {
 53831                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 53832                if (invalidpath is not null)
 3833                {
 03834                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3835                }
 3836            }
 3837
 53838            LibraryMonitor.Stop();
 3839
 3840            try
 3841            {
 53842                Directory.CreateDirectory(virtualFolderPath);
 3843
 53844                if (collectionType is not null)
 3845                {
 13846                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3847
 13848                    FileHelper.CreateEmpty(path);
 3849                }
 3850
 53851                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3852
 53853                if (mediaPathInfos is not null)
 3854                {
 103855                    foreach (var path in mediaPathInfos)
 3856                    {
 03857                        AddMediaPathInternal(name, path, false);
 3858                    }
 3859                }
 3860
 3861                // The libraries root was listed before this folder existed, so drop that listing:
 3862                // anything still reading it resolves the library set without the new folder.
 53863                _directoryService.Invalidate(virtualFolderPath);
 3864            }
 3865            finally
 3866            {
 53867                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3868
 53869                if (refreshLibrary)
 3870                {
 33871                    _ = StartScanInBackground();
 3872                }
 3873                else
 3874                {
 3875                    // Need to add a delay here or directory watchers may still pick up the changes
 23876                    await Task.Delay(1000).ConfigureAwait(false);
 23877                    LibraryMonitor.Start();
 3878                }
 3879            }
 53880        }
 3881
 3882        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3883        {
 03884            foreach (var person in people)
 3885            {
 03886                cancellationToken.ThrowIfCancellationRequested();
 3887
 03888                var itemUpdateType = ItemUpdateType.MetadataDownload;
 03889                var saveEntity = false;
 03890                var personEntity = GetPerson(person.Name);
 3891
 03892                if (personEntity is null)
 3893                {
 3894                    try
 3895                    {
 03896                        personEntity = GetOrCreatePerson(person.Name);
 03897                        saveEntity = true;
 03898                    }
 03899                    catch (Exception ex)
 3900                    {
 03901                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 03902                        continue;
 3903                    }
 3904                }
 3905
 03906                foreach (var id in person.ProviderIds)
 3907                {
 03908                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3909                    {
 03910                        personEntity.SetProviderId(id.Key, id.Value);
 03911                        saveEntity = true;
 3912                    }
 3913                }
 3914
 03915                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3916                {
 03917                    personEntity.SetImage(
 03918                        new ItemImageInfo
 03919                        {
 03920                            Path = person.ImageUrl,
 03921                            Type = ImageType.Primary
 03922                        },
 03923                        0);
 3924
 03925                    saveEntity = true;
 03926                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3927                }
 3928
 03929                if (saveEntity)
 3930                {
 03931                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 03932                    personEntity.DateLastSaved = DateTime.UtcNow;
 3933
 03934                    CreateItems([personEntity], null, CancellationToken.None);
 3935                }
 03936            }
 03937        }
 3938
 3939        internal Task StartScanInBackground()
 3940        {
 3941            // An active scan already handles library structure changes, so this request can be dropped.
 63942            if (IsScanRunning)
 3943            {
 13944                return Task.CompletedTask;
 3945            }
 3946
 3947            // Queue instead of restarting so a scan that starts after the check is allowed to finish.
 53948            return Task.Run(QueueLibraryScan);
 3949        }
 3950
 3951        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3952        {
 13953            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03954        }
 3955
 3956        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3957        {
 13958            ArgumentNullException.ThrowIfNull(pathInfo);
 3959
 13960            var path = pathInfo.Path;
 3961
 13962            if (string.IsNullOrWhiteSpace(path))
 3963            {
 03964                throw new ArgumentException(nameof(path));
 3965            }
 3966
 13967            if (!Directory.Exists(path))
 3968            {
 13969                throw new FileNotFoundException("The path does not exist.");
 3970            }
 3971
 03972            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03973            var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
 03974                ?? throw new FileNotFoundException(
 03975                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3976
 03977            CreateShortcut(virtualFolderPath, pathInfo);
 3978
 03979            if (saveLibraryOptions)
 3980            {
 03981                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3982
 03983                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3984
 03985                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3986
 03987                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3988            }
 03989        }
 3990
 3991        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3992        {
 03993            ArgumentNullException.ThrowIfNull(mediaPath);
 3994
 03995            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03996            var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
 03997                ?? throw new FileNotFoundException(
 03998                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3999
 04000            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 4001
 04002            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 4003
 04004            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 04005        }
 4006
 4007        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 4008        {
 04009            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 04010            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 4011
 04012            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 4013            {
 04014                var list = options.PathInfos.ToList();
 4015
 04016                foreach (var location in info.Locations)
 4017                {
 04018                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 4019                    {
 04020                        list.Add(new MediaPathInfo(location));
 4021                    }
 4022                }
 4023
 04024                options.PathInfos = list.ToArray();
 4025            }
 04026        }
 4027
 4028        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 4029        {
 104030            if (string.IsNullOrWhiteSpace(name))
 4031            {
 04032                throw new ArgumentNullException(nameof(name));
 4033            }
 4034
 104035            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 4036
 104037            var path = FileSystemHelper.GetChildPath(rootFolderPath, name);
 4038
 104039            if (path is null || !Directory.Exists(path))
 4040            {
 64041                throw new FileNotFoundException("The media folder does not exist");
 4042            }
 4043
 44044            LibraryMonitor.Stop();
 4045
 4046            try
 4047            {
 44048                Directory.Delete(path, true);
 44049                _directoryService.Invalidate(path);
 4050            }
 4051            finally
 4052            {
 44053                CollectionFolder.OnCollectionFolderChange();
 4054
 44055                if (refreshLibrary)
 4056                {
 14057                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 4058
 14059                    _ = StartScanInBackground();
 4060                }
 4061                else
 4062                {
 4063                    // Need to add a delay here or directory watchers may still pick up the changes
 34064                    await Task.Delay(1000).ConfigureAwait(false);
 34065                    LibraryMonitor.Start();
 4066                }
 4067            }
 44068        }
 4069
 4070        private void RemoveContentTypeOverrides(string path)
 4071        {
 04072            if (string.IsNullOrWhiteSpace(path))
 4073            {
 04074                throw new ArgumentNullException(nameof(path));
 4075            }
 4076
 04077            List<NameValuePair>? removeList = null;
 4078
 04079            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 4080            {
 04081                if (string.IsNullOrWhiteSpace(contentType.Name)
 04082                    || _fileSystem.AreEqual(path, contentType.Name)
 04083                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 4084                {
 04085                    (removeList ??= new()).Add(contentType);
 4086                }
 4087            }
 4088
 04089            if (removeList is not null)
 4090            {
 04091                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 04092                    .Except(removeList)
 04093                    .ToArray();
 4094
 04095                _configurationManager.SaveConfiguration();
 4096            }
 04097        }
 4098
 4099        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 4100        {
 14101            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 4102
 14103            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 14104            var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName);
 4105
 14106            if (virtualFolderPath is null || !Directory.Exists(virtualFolderPath))
 4107            {
 14108                throw new FileNotFoundException(
 14109                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 4110            }
 4111
 04112            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 04113                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 04114                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 4115
 04116            if (!string.IsNullOrEmpty(shortcut))
 4117            {
 04118                _fileSystem.DeleteFile(shortcut);
 04119                _directoryService.Invalidate(shortcut);
 4120            }
 4121
 04122            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 4123
 04124            libraryOptions.PathInfos = libraryOptions
 04125                .PathInfos
 04126                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 04127                .ToArray();
 4128
 04129            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 04130        }
 4131
 4132        private static bool ItemIsVisible(BaseItem? item, User? user)
 4133        {
 224134            if (item is null)
 4135            {
 224136                return false;
 4137            }
 4138
 04139            if (user is null)
 4140            {
 04141                return true;
 4142            }
 4143
 04144            return item is UserRootFolder || item.IsVisibleStandalone(user);
 4145        }
 4146
 4147        public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
 4148        {
 04149            var path = pathInfo.Path;
 04150            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 4151
 04152            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 4153
 04154            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 4155
 04156            while (File.Exists(lnk))
 4157            {
 04158                shortcutFilename += "1";
 04159                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 4160            }
 4161
 04162            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 04163            _directoryService.Invalidate(lnk);
 04164            RemoveContentTypeOverrides(path);
 04165        }
 4166
 4167        /// <inheritdoc />
 4168        public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
 4169        {
 04170            var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
 4171
 4172            // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
 04173            foreach (var parentId in affectedParentIds)
 4174            {
 04175                if (GetItemById(parentId) is Folder parent)
 4176                {
 04177                    foreach (var lc in parent.LinkedChildren)
 4178                    {
 04179                        if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
 4180                        {
 04181                            lc.ItemId = toChildId;
 4182                        }
 4183                    }
 4184
 04185                    await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
 4186                }
 4187            }
 04188        }
 4189
 4190        /// <inheritdoc />
 4191        public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
 4192        {
 04193            if (query.User is not null)
 4194            {
 04195                AddUserToQuery(query, query.User);
 4196            }
 4197
 04198            SetTopParentOrAncestorIds(query);
 04199            return _itemRepository.GetQueryFiltersLegacy(query);
 4200        }
 4201
 4202        /// <inheritdoc />
 4203        public IReadOnlyList<string> GetTagNames(InternalItemsQuery query)
 4204        {
 04205            if (query.User is not null)
 4206            {
 04207                AddUserToQuery(query, query.User);
 4208            }
 4209
 04210            SetTopParentOrAncestorIds(query);
 04211            return _itemRepository.GetTagNames(query);
 4212        }
 4213
 4214        /// <inheritdoc />
 4215        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
 4216        {
 04217            return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
 4218        }
 4219
 4220        /// <inheritdoc />
 4221        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType, InternalItemsQuery query)
 4222        {
 04223            if (query.User is not null)
 4224            {
 04225                AddUserToQuery(query, query.User);
 4226            }
 4227
 04228            SetTopParentOrAncestorIds(query);
 04229            return _itemRepository.GetMediaStreamLanguages(query, mediaStreamType);
 4230        }
 4231
 4232        private sealed record ExtraCandidate(BaseItem Extra, ExtraType ExtraType, ExtraRule ExtraRule, bool IsInMixedFol
 4233    }
 4234}

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.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>)
GetPersonId(System.String)
GetPerson(System.String)
GetOrCreatePerson(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)
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)
GetItemCountsForNameItems(Jellyfin.Data.Enums.BaseItemKind,System.Collections.Generic.IReadOnlyList`1<System.Guid>,Jellyfin.Data.Enums.BaseItemKind[],Jellyfin.Database.Implementations.Entities.User)
GetChildCountBatch(System.Collections.Generic.IReadOnlyList`1<System.Guid>,Jellyfin.Database.Implementations.Entities.User)
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>)
SetAncestorIds(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)
GetNamedViewId(Jellyfin.Database.Implementations.Entities.User,System.Guid,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
GetShadowView(MediaBrowser.Controller.Entities.BaseItem,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetShadowViewId(System.String,System.Guid,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
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>)
GetNumberedExtraName(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)
DeleteOrphanedCredits()
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)
GetTagNames(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetMediaStreamLanguages(MediaBrowser.Model.Entities.MediaStreamType)
GetMediaStreamLanguages(MediaBrowser.Model.Entities.MediaStreamType,MediaBrowser.Controller.Entities.InternalItemsQuery)