< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.BaseItem
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs
Line coverage
60%
Covered lines: 603
Uncovered lines: 390
Coverable lines: 993
Total lines: 3145
Line coverage: 60.7%
Branch coverage
48%
Covered branches: 314
Total branches: 654
Branch coverage: 48%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/18/2026 - 12:15:49 AM Line coverage: 45.5% (404/887) Branch coverage: 36.5% (202/552) Total lines: 27735/20/2026 - 12:15:44 AM Line coverage: 45.5% (404/887) Branch coverage: 32.4% (179/552) Total lines: 27735/27/2026 - 12:15:38 AM Line coverage: 45.6% (406/890) Branch coverage: 32.6% (182/558) Total lines: 27886/2/2026 - 12:15:49 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27796/6/2026 - 12:15:50 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27817/6/2026 - 12:16:28 AM Line coverage: 54.4% (505/927) Branch coverage: 41% (244/594) Total lines: 29137/26/2026 - 12:17:51 AM Line coverage: 55% (519/943) Branch coverage: 42% (258/614) Total lines: 29708/3/2026 - 12:16:46 AM Line coverage: 54.5% (519/952) Branch coverage: 41.7% (258/618) Total lines: 30028/6/2026 - 12:17:15 AM Line coverage: 56.9% (547/961) Branch coverage: 43.2% (270/624) Total lines: 302010/29/2026 - 12:17:11 AM Line coverage: 60.8% (603/991) Branch coverage: 48.4% (317/654) Total lines: 312410/30/2026 - 12:16:57 AM Line coverage: 60.8% (603/991) Branch coverage: 48.6% (318/654) Total lines: 312410/31/2026 - 12:17:46 AM Line coverage: 60.6% (601/991) Branch coverage: 48% (314/654) Total lines: 312411/4/2026 - 12:18:26 AM Line coverage: 60.6% (601/991) Branch coverage: 48.1% (315/654) Total lines: 312411/5/2026 - 12:17:36 AM Line coverage: 60.8% (603/991) Branch coverage: 48.4% (317/654) Total lines: 312411/6/2026 - 12:17:54 AM Line coverage: 60.6% (601/991) Branch coverage: 48% (314/654) Total lines: 312411/7/2026 - 12:19:31 AM Line coverage: 60.7% (603/993) Branch coverage: 48% (314/654) Total lines: 3145 5/18/2026 - 12:15:49 AM Line coverage: 45.5% (404/887) Branch coverage: 36.5% (202/552) Total lines: 27735/20/2026 - 12:15:44 AM Line coverage: 45.5% (404/887) Branch coverage: 32.4% (179/552) Total lines: 27735/27/2026 - 12:15:38 AM Line coverage: 45.6% (406/890) Branch coverage: 32.6% (182/558) Total lines: 27886/2/2026 - 12:15:49 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27796/6/2026 - 12:15:50 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27817/6/2026 - 12:16:28 AM Line coverage: 54.4% (505/927) Branch coverage: 41% (244/594) Total lines: 29137/26/2026 - 12:17:51 AM Line coverage: 55% (519/943) Branch coverage: 42% (258/614) Total lines: 29708/3/2026 - 12:16:46 AM Line coverage: 54.5% (519/952) Branch coverage: 41.7% (258/618) Total lines: 30028/6/2026 - 12:17:15 AM Line coverage: 56.9% (547/961) Branch coverage: 43.2% (270/624) Total lines: 302010/29/2026 - 12:17:11 AM Line coverage: 60.8% (603/991) Branch coverage: 48.4% (317/654) Total lines: 312410/30/2026 - 12:16:57 AM Line coverage: 60.8% (603/991) Branch coverage: 48.6% (318/654) Total lines: 312410/31/2026 - 12:17:46 AM Line coverage: 60.6% (601/991) Branch coverage: 48% (314/654) Total lines: 312411/4/2026 - 12:18:26 AM Line coverage: 60.6% (601/991) Branch coverage: 48.1% (315/654) Total lines: 312411/5/2026 - 12:17:36 AM Line coverage: 60.8% (603/991) Branch coverage: 48.4% (317/654) Total lines: 312411/6/2026 - 12:17:54 AM Line coverage: 60.6% (601/991) Branch coverage: 48% (314/654) Total lines: 312411/7/2026 - 12:19:31 AM Line coverage: 60.7% (603/993) Branch coverage: 48% (314/654) Total lines: 3145

Coverage delta

Coverage delta 9 -9

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
get_LUFS()100%11100%
set_LUFS(...)100%11100%
get_NormalizationGain()100%11100%
set_NormalizationGain(...)100%11100%
get_SupportsAddingToPlaylist()100%210%
get_AlwaysScanInternalMetadataPath()100%11100%
get_SupportsPlayedStatus()100%210%
get_SupportsPositionTicksResume()100%210%
get_SupportsRemoteImageDownloading()100%11100%
get_Name()100%11100%
set_Name(...)100%11100%
get_IsUnaired()0%620%
get_IsThemeMedia()0%2040%
get_OriginalLanguage()100%11100%
set_OriginalLanguage(...)50%66100%
get_DisplayPreferencesId()50%22100%
get_SourceType()50%2266.66%
get_ContainingFolderPath()100%22100%
get_IsHidden()100%210%
get_LocationType()33.33%7666.66%
get_PathProtocol()100%22100%
get_IsFileProtocol()100%11100%
get_HasPathProtocol()100%210%
get_SupportsLocalMetadata()50%2266.66%
get_FileNameWithoutExtension()0%620%
get_EnableAlphaNumericSorting()100%11100%
get_IsHD()100%11100%
get_PrimaryImagePath()100%210%
get_MediaType()100%11100%
get_PhysicalLocations()0%620%
get_EnableMediaSourceDisplay()50%2266.66%
get_ForcedSortName()100%11100%
set_ForcedSortName(...)100%11100%
get_SortName()100%44100%
set_SortName(...)100%11100%
get_DisplayParentId()100%11100%
get_DisplayParent()100%22100%
get_CriticRating()100%11100%
set_CriticRating(...)100%11100%
get_CommunityRating()100%11100%
set_CommunityRating(...)100%11100%
get_HasLocalAlternateVersions()100%210%
get_OfficialRatingForComparison()75%4485.71%
get_CustomRatingForComparison()100%11100%
get_LatestItemsIndexContainer()100%210%
get_EnableRememberingTrackSelections()100%11100%
get_IsTopParent()80%121075%
get_SupportsAncestors()100%11100%
get_SupportsOwnedItems()100%22100%
get_SearchesContainingFolderForExtras()50%1414100%
get_SupportsPeople()100%11100%
get_SupportsThemeMedia()100%210%
get_SupportsInheritedParentImages()100%210%
get_IsFolder()100%11100%
get_IsDisplayedAsFolder()100%210%
GetCustomRatingForComparision(...)87.5%8888.88%
GetDefaultPrimaryImageAspectRatio()100%210%
CreatePresentationUniqueKey()100%11100%
CanDelete()0%620%
IsAuthorizedToDelete(...)0%7280%
GetOwner()100%22100%
CanDelete(...)50%22100%
CanDelete(...)100%11100%
CanDownload()100%11100%
IsAuthorizedToDownload(...)100%210%
CanDownload(...)50%22100%
ToString()100%11100%
GetInternalMetadataPath()100%11100%
GetInternalMetadataPath(...)50%2275%
CreateSortName()100%11100%
GetItemByNameFolderName(...)60%111081.81%
GetSortName(...)80%101083.33%
ApplySortCharacterRules(...)100%44100%
NormalizeSortNameFilter(...)100%11100%
ModifySortChunks(...)87.5%8893.75%
GetParent()100%22100%
GetParents()100%22100%
FindParent()50%7440%
GetPlayAccess(...)50%2266.66%
GetMediaStreams()100%210%
IsActiveRecording()100%210%
GetMediaSources(...)37.5%10866.66%
GetAllItemsForMediaSources()100%210%
GetVersionInfo(...)36.95%594681.63%
GetMediaSourceName(...)57.14%834271.42%
FiniteOrNull(...)100%44100%
GetCommonNamePrefix(...)80%101087.5%
GetCommonVersionPrefix(...)96.15%2626100%
IsDecimalPoint(...)78.57%141487.5%
RefreshMetadata(...)100%11100%
RefreshMetadata()62.5%8882.35%
IsVisibleStandaloneInternal(...)0%342180%
SetParent(...)50%22100%
RefreshedOwnedItems()87.5%8887.5%
GetFileSystemChildren(...)100%11100%
RefreshExtras()36.36%1452236.73%
GetPresentationUniqueKey()0%620%
RequiresRefresh()66.66%66100%
GetUserDataKeys()25%5466.66%
UpdateFromResolvedItem(...)50%2260%
AfterMetadataRefresh()100%11100%
GetPreferredMetadataLanguage()50%88100%
GetPreferredMetadataCountryCode()50%88100%
GetInheritedOriginalLanguage()100%210%
IsSaveLocalMetadataEnabled()50%2275%
IsParentalAllowed(...)18.75%641642.85%
GetParentalRatingScore()100%44100%
GetInheritedTags()100%44100%
IsVisibleViaTags(...)36.36%482262.5%
GetBlockUnratedType()50%2266.66%
GetBlockUnratedValue(...)100%44100%
IsVisible(...)100%11100%
IsVisibleStandalone(...)0%2040%
GetClientTypeName()70%141066.66%
GetBaseItemKind()100%22100%
GetLinkedChild(...)0%7280%
FindLinkedChild(...)0%210140%
AddStudio(...)50%4475%
SetStudios(...)100%11100%
AddGenre(...)50%22100%
MarkPlayed(...)0%110100%
MarkUnplayed(...)100%210%
ResetPlayedState(...)100%11100%
ChangedExternally()100%210%
HasImage(...)100%11100%
SetImage(...)75%4490.9%
SetImagePath(...)75%4490.9%
DeleteImageAsync()0%4260%
RemoveImage(...)100%210%
RemoveImages(...)100%11100%
AddImage(...)100%11100%
UpdateToRepositoryAsync()100%11100%
ReattachUserDataAsync()100%11100%
ValidateImages()91.66%1212100%
GetImagePath(...)50%22100%
GetImageInfo(...)16.66%24620%
GetImageIndex(...)0%110100%
GetImages()83.33%6685.71%
AddImages(...)85%202091.3%
GetImageInfo(...)100%11100%
GetDeletePaths()100%210%
GetLocalMetadataFilesToDelete()0%2040%
AllowsMultipleImages(...)100%22100%
SwapImagesAsync(...)0%110100%
IsPlayed(...)0%2040%
IsFavoriteOrLiked(...)0%4260%
IsUnplayed(...)0%2040%
MediaBrowser.Controller.Providers.IHasLookupInfo<MediaBrowser.Controller.Providers.ItemLookupInfo>.GetLookupInfo()100%11100%
GetItemLookupInfo()100%11100%
GetNameForMetadataLookup()100%11100%
BeforeMetadataRefresh(...)25%5466.66%
GetMappedPath(...)0%620%
FillUserDataDtoValues(...)0%4260%
InheritDatesFromOwner(...)91.66%1212100%
RefreshMetadataForOwnedItem()0%420200%
RefreshMetadataForOwnedVideo()0%4260%
GetEtag(...)100%11100%
GetEtagValues(...)100%11100%
GetAncestorIds()100%11100%
GetTopParent()100%22100%
GetIdsForAncestorQuery()100%11100%
GetRefreshProgress()100%210%
OnMetadataChanged()62.5%8886.66%
UpdateRatingToItems(...)0%4260%
GetThemeSongs(...)100%210%
GetThemeSongs(...)100%210%
GetThemeVideos(...)100%210%
GetThemeVideos(...)100%210%
GetExtraOwnerIds()100%11100%
GetOwnedVersionIds()100%210%
GetOwnerIdForExtra(...)100%210%
GetExtras(...)100%11100%
GetExtras(...)100%210%
GetRunTimeTicksForPlayState()100%210%
Equals(...)50%22100%
Equals(...)50%22100%
GetHashCode()100%11100%

File(s)

/srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591, SA1401
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Text;
 12using System.Text.Json.Serialization;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Common.Extensions;
 21using MediaBrowser.Controller.Channels;
 22using MediaBrowser.Controller.Chapters;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Dto;
 25using MediaBrowser.Controller.Entities.TV;
 26using MediaBrowser.Controller.Library;
 27using MediaBrowser.Controller.MediaSegments;
 28using MediaBrowser.Controller.Persistence;
 29using MediaBrowser.Controller.Providers;
 30using MediaBrowser.Model.Configuration;
 31using MediaBrowser.Model.Dto;
 32using MediaBrowser.Model.Entities;
 33using MediaBrowser.Model.Globalization;
 34using MediaBrowser.Model.IO;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.MediaInfo;
 38using Microsoft.Extensions.Logging;
 39
 40namespace MediaBrowser.Controller.Entities
 41{
 42    /// <summary>
 43    /// Class BaseItem.
 44    /// </summary>
 45    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 46    {
 47        private BaseItemKind? _baseItemKind;
 48
 49        public const string ThemeSongFileName = "theme";
 50
 51        // Well below the 255 byte limit of the common Linux filesystems and the 255 character limit
 52        // of Windows, so the files inside the folder still fit within MAX_PATH.
 53        private const int MaxItemByNameFolderNameBytes = 128;
 54
 55        /// <summary>
 56        /// The supported image extensions.
 57        /// </summary>
 558        public static readonly string[] SupportedImageExtensions
 559            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 60
 561        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 562        {
 563            ".nfo",
 564            ".xml",
 565            ".srt",
 566            ".vtt",
 567            ".sub",
 568            ".sup",
 569            ".idx",
 570            ".txt",
 571            ".edl",
 572            ".bif",
 573            ".smi",
 574            ".ttml",
 575            ".lrc",
 576            ".elrc"
 577        };
 78
 79        /// <summary>
 80        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 81        /// </summary>
 582        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 583        {
 584            Model.Entities.ExtraType.Unknown,
 585            Model.Entities.ExtraType.BehindTheScenes,
 586            Model.Entities.ExtraType.Clip,
 587            Model.Entities.ExtraType.DeletedScene,
 588            Model.Entities.ExtraType.Interview,
 589            Model.Entities.ExtraType.Sample,
 590            Model.Entities.ExtraType.Scene,
 591            Model.Entities.ExtraType.Featurette,
 592            Model.Entities.ExtraType.Short
 593        };
 94
 595        private protected static readonly char[] VersionDelimiters = ['-', '_', '.'];
 96
 97        private string _sortName;
 98
 99        private string _forcedSortName;
 100
 101        private string _name;
 102
 103        private string _originalLanguage;
 104
 105        private float? _lufs;
 106
 107        private float? _normalizationGain;
 108
 109        private float? _criticRating;
 110
 111        private float? _communityRating;
 112
 113        public const char SlugChar = '-';
 114
 115        protected BaseItem()
 116        {
 3950117            Tags = Array.Empty<string>();
 3950118            Genres = Array.Empty<string>();
 3950119            Studios = Array.Empty<string>();
 3950120            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 3950121            LockedFields = Array.Empty<MetadataField>();
 3950122            ImageInfos = Array.Empty<ItemImageInfo>();
 3950123            ProductionLocations = Array.Empty<string>();
 3950124            RemoteTrailers = Array.Empty<MediaUrl>();
 3950125            UserData = [];
 3950126        }
 127
 128        /// <summary>
 129        /// Gets or Sets the user data collection as cached from the last Db query.
 130        /// </summary>
 131        [JsonIgnore]
 132        public ICollection<UserData> UserData { get; set; }
 133
 134        [JsonIgnore]
 135        public string PreferredMetadataCountryCode { get; set; }
 136
 137        [JsonIgnore]
 138        public string PreferredMetadataLanguage { get; set; }
 139
 140        public long? Size { get; set; }
 141
 142        public string Container { get; set; }
 143
 144        [JsonIgnore]
 145        public string Tagline { get; set; }
 146
 147        [JsonIgnore]
 148        public virtual ItemImageInfo[] ImageInfos { get; set; }
 149
 150        [JsonIgnore]
 151        public bool IsVirtualItem { get; set; }
 152
 153        /// <summary>
 154        /// Gets or sets the album.
 155        /// </summary>
 156        /// <value>The album.</value>
 157        [JsonIgnore]
 158        public string Album { get; set; }
 159
 160        /// <summary>
 161        /// Gets or sets the LUFS value.
 162        /// </summary>
 163        /// <value>The LUFS Value.</value>
 164        [JsonIgnore]
 165        public float? LUFS
 166        {
 266167            get => _lufs;
 268168            set => _lufs = FiniteOrNull(value);
 169        }
 170
 171        /// <summary>
 172        /// Gets or sets the gain required for audio normalization.
 173        /// </summary>
 174        /// <value>The gain required for audio normalization.</value>
 175        [JsonIgnore]
 176        public float? NormalizationGain
 177        {
 266178            get => _normalizationGain;
 268179            set => _normalizationGain = FiniteOrNull(value);
 180        }
 181
 182        /// <summary>
 183        /// Gets or sets the channel identifier.
 184        /// </summary>
 185        /// <value>The channel identifier.</value>
 186        [JsonIgnore]
 187        public Guid ChannelId { get; set; }
 188
 189        [JsonIgnore]
 0190        public virtual bool SupportsAddingToPlaylist => false;
 191
 192        [JsonIgnore]
 49193        public virtual bool AlwaysScanInternalMetadataPath => false;
 194
 195        /// <summary>
 196        /// Gets or sets a value indicating whether this instance is in mixed folder.
 197        /// </summary>
 198        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 199        [JsonIgnore]
 200        public bool IsInMixedFolder { get; set; }
 201
 202        [JsonIgnore]
 0203        public virtual bool SupportsPlayedStatus => false;
 204
 205        [JsonIgnore]
 0206        public virtual bool SupportsPositionTicksResume => false;
 207
 208        [JsonIgnore]
 18209        public virtual bool SupportsRemoteImageDownloading => true;
 210
 211        /// <summary>
 212        /// Gets or sets the name.
 213        /// </summary>
 214        /// <value>The name.</value>
 215        [JsonIgnore]
 216        public virtual string Name
 217        {
 1430218            get => _name;
 219            set
 220            {
 938221                _name = value;
 222
 223                // lazy load this again
 938224                _sortName = null;
 938225            }
 226        }
 227
 228        [JsonIgnore]
 0229        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 230
 231        [JsonIgnore]
 232        public int? TotalBitrate { get; set; }
 233
 234        [JsonIgnore]
 235        public ExtraType? ExtraType { get; set; }
 236
 237        [JsonIgnore]
 0238        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 239
 240        [JsonIgnore]
 241        public string OriginalTitle { get; set; }
 242
 243        [JsonIgnore]
 244        public string OriginalLanguage
 245        {
 539246            get => _originalLanguage;
 443247            set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
 248        }
 249
 250        /// <summary>
 251        /// Gets or sets the id.
 252        /// </summary>
 253        /// <value>The id.</value>
 254        [JsonIgnore]
 255        public Guid Id { get; set; }
 256
 257        [JsonIgnore]
 258        public Guid OwnerId { get; set; }
 259
 260        /// <summary>
 261        /// Gets or sets the audio.
 262        /// </summary>
 263        /// <value>The audio.</value>
 264        [JsonIgnore]
 265        public ProgramAudio? Audio { get; set; }
 266
 267        /// <summary>
 268        /// Gets the id that should be used to key display prefs for this item.
 269        /// Default is based on the type for everything except actual generic folders.
 270        /// </summary>
 271        /// <value>The display prefs id.</value>
 272        [JsonIgnore]
 273        public virtual Guid DisplayPreferencesId
 274        {
 275            get
 276            {
 6277                var thisType = GetType();
 6278                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 279            }
 280        }
 281
 282        /// <summary>
 283        /// Gets or sets the path.
 284        /// </summary>
 285        /// <value>The path.</value>
 286        [JsonIgnore]
 287        public virtual string Path { get; set; }
 288
 289        [JsonIgnore]
 290        public virtual SourceType SourceType
 291        {
 292            get
 293            {
 5345294                if (!ChannelId.IsEmpty())
 295                {
 0296                    return SourceType.Channel;
 297                }
 298
 5345299                return SourceType.Library;
 300            }
 301        }
 302
 303        /// <summary>
 304        /// Gets the folder containing the item.
 305        /// If the item is a folder, it returns the folder itself.
 306        /// </summary>
 307        [JsonIgnore]
 308        public virtual string ContainingFolderPath
 309        {
 310            get
 311            {
 721312                if (IsFolder)
 313                {
 611314                    return Path;
 315                }
 316
 110317                return System.IO.Path.GetDirectoryName(Path);
 318            }
 319        }
 320
 321        /// <summary>
 322        /// Gets or sets the name of the service.
 323        /// </summary>
 324        /// <value>The name of the service.</value>
 325        [JsonIgnore]
 326        public string ServiceName { get; set; }
 327
 328        /// <summary>
 329        /// Gets or sets the external id.
 330        /// </summary>
 331        /// <remarks>
 332        /// If this content came from an external service, the id of the content on that service.
 333        /// </remarks>
 334        [JsonIgnore]
 335        public string ExternalId { get; set; }
 336
 337        [JsonIgnore]
 338        public string ExternalSeriesId { get; set; }
 339
 340        [JsonIgnore]
 0341        public virtual bool IsHidden => false;
 342
 343        /// <summary>
 344        /// Gets the type of the location.
 345        /// </summary>
 346        /// <value>The type of the location.</value>
 347        [JsonIgnore]
 348        public virtual LocationType LocationType
 349        {
 350            get
 351            {
 33352                var path = Path;
 33353                if (string.IsNullOrEmpty(path))
 354                {
 17355                    if (SourceType == SourceType.Channel)
 356                    {
 0357                        return LocationType.Remote;
 358                    }
 359
 17360                    return LocationType.Virtual;
 361                }
 362
 16363                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 364            }
 365        }
 366
 367        [JsonIgnore]
 368        public MediaProtocol? PathProtocol
 369        {
 370            get
 371            {
 3858372                var path = Path;
 373
 3858374                if (string.IsNullOrEmpty(path))
 375                {
 131376                    return null;
 377                }
 378
 3727379                return MediaSourceManager.GetPathProtocol(path);
 380            }
 381        }
 382
 383        [JsonIgnore]
 3815384        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 385
 386        [JsonIgnore]
 0387        public bool HasPathProtocol => PathProtocol.HasValue;
 388
 389        [JsonIgnore]
 390        public virtual bool SupportsLocalMetadata
 391        {
 392            get
 393            {
 1839394                if (SourceType == SourceType.Channel)
 395                {
 0396                    return false;
 397                }
 398
 1839399                return IsFileProtocol;
 400            }
 401        }
 402
 403        [JsonIgnore]
 404        public virtual string FileNameWithoutExtension
 405        {
 406            get
 407            {
 0408                if (IsFileProtocol)
 409                {
 0410                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 411                }
 412
 0413                return null;
 414            }
 415        }
 416
 417        [JsonIgnore]
 185418        public virtual bool EnableAlphaNumericSorting => true;
 419
 190420        public virtual bool IsHD => Height >= 720;
 421
 422        public bool IsShortcut { get; set; }
 423
 424        public string ShortcutPath { get; set; }
 425
 426        public int Width { get; set; }
 427
 428        public int Height { get; set; }
 429
 430        /// <summary>
 431        /// Gets the primary image path.
 432        /// </summary>
 433        /// <remarks>
 434        /// This is just a helper for convenience.
 435        /// </remarks>
 436        /// <value>The primary image path.</value>
 437        [JsonIgnore]
 0438        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 439
 440        /// <summary>
 441        /// Gets or sets the date created.
 442        /// </summary>
 443        /// <value>The date created.</value>
 444        [JsonIgnore]
 445        public DateTime DateCreated { get; set; }
 446
 447        /// <summary>
 448        /// Gets or sets the date modified.
 449        /// </summary>
 450        /// <value>The date modified.</value>
 451        [JsonIgnore]
 452        public DateTime DateModified { get; set; }
 453
 454        public DateTime DateLastSaved { get; set; }
 455
 456        [JsonIgnore]
 457        public DateTime DateLastRefreshed { get; set; }
 458
 459        [JsonIgnore]
 460        public bool IsLocked { get; set; }
 461
 462        /// <summary>
 463        /// Gets or sets the locked fields.
 464        /// </summary>
 465        /// <value>The locked fields.</value>
 466        [JsonIgnore]
 467        public MetadataField[] LockedFields { get; set; }
 468
 469        /// <summary>
 470        /// Gets the type of the media.
 471        /// </summary>
 472        /// <value>The type of the media.</value>
 473        [JsonIgnore]
 187474        public virtual MediaType MediaType => MediaType.Unknown;
 475
 476        [JsonIgnore]
 477        public virtual string[] PhysicalLocations
 478        {
 479            get
 480            {
 0481                if (!IsFileProtocol)
 482                {
 0483                    return Array.Empty<string>();
 484                }
 485
 0486                return [Path];
 487            }
 488        }
 489
 490        [JsonIgnore]
 491        public bool EnableMediaSourceDisplay
 492        {
 493            get
 494            {
 6495                if (SourceType == SourceType.Channel)
 496                {
 0497                    return ChannelManager.EnableMediaSourceDisplay(this);
 498                }
 499
 6500                return true;
 501            }
 502        }
 503
 504        [JsonIgnore]
 505        public Guid ParentId { get; set; }
 506
 507        /// <summary>
 508        /// Gets or sets the logger.
 509        /// </summary>
 510        public static ILogger<BaseItem> Logger { get; set; }
 511
 512        public static ILibraryManager LibraryManager { get; set; }
 513
 514        public static IServerConfigurationManager ConfigurationManager { get; set; }
 515
 516        public static IProviderManager ProviderManager { get; set; }
 517
 518        public static ILocalizationManager LocalizationManager { get; set; }
 519
 520        public static IItemRepository ItemRepository { get; set; }
 521
 522        public static IItemCountService ItemCountService { get; set; }
 523
 524        public static IChapterManager ChapterManager { get; set; }
 525
 526        public static IFileSystem FileSystem { get; set; }
 527
 528        public static IUserDataManager UserDataManager { get; set; }
 529
 530        public static IChannelManager ChannelManager { get; set; }
 531
 532        public static IMediaSourceManager MediaSourceManager { get; set; }
 533
 534        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 535
 536        /// <summary>
 537        /// Gets or sets the name of the forced sort.
 538        /// </summary>
 539        /// <value>The name of the forced sort.</value>
 540        [JsonIgnore]
 541        public string ForcedSortName
 542        {
 681543            get => _forcedSortName;
 544            set
 545            {
 296546                _forcedSortName = value;
 296547                _sortName = null;
 296548            }
 549        }
 550
 551        /// <summary>
 552        /// Gets or sets the name of the sort.
 553        /// </summary>
 554        /// <value>The name of the sort.</value>
 555        [JsonIgnore]
 556        public string SortName
 557        {
 558            get
 559            {
 284560                if (_sortName is null)
 561                {
 185562                    if (!string.IsNullOrEmpty(ForcedSortName))
 563                    {
 564                        // Run the forced sort name through the same cleaning as auto-generated sort names.
 1565                        _sortName = GetSortName(ForcedSortName, EnableAlphaNumericSorting, ConfigurationManager.Configur
 566                    }
 567                    else
 568                    {
 184569                        _sortName = CreateSortName();
 570                    }
 571                }
 572
 284573                return _sortName;
 574            }
 575
 330576            set => _sortName = value;
 577        }
 578
 579        [JsonIgnore]
 1051580        public virtual Guid DisplayParentId => ParentId;
 581
 582        [JsonIgnore]
 583        public BaseItem DisplayParent
 584        {
 585            get
 586            {
 1064587                var id = DisplayParentId;
 1064588                if (id.IsEmpty())
 589                {
 776590                    return null;
 591                }
 592
 288593                return LibraryManager.GetItemById(id);
 594            }
 595        }
 596
 597        /// <summary>
 598        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 599        /// </summary>
 600        /// <value>The premiere date.</value>
 601        [JsonIgnore]
 602        public DateTime? PremiereDate { get; set; }
 603
 604        /// <summary>
 605        /// Gets or sets the end date.
 606        /// </summary>
 607        /// <value>The end date.</value>
 608        [JsonIgnore]
 609        public DateTime? EndDate { get; set; }
 610
 611        /// <summary>
 612        /// Gets or sets the official rating.
 613        /// </summary>
 614        /// <value>The official rating.</value>
 615        [JsonIgnore]
 616        public string OfficialRating { get; set; }
 617
 618        [JsonIgnore]
 619        public int? InheritedParentalRatingValue { get; set; }
 620
 621        [JsonIgnore]
 622        public int? InheritedParentalRatingSubValue { get; set; }
 623
 624        /// <summary>
 625        /// Gets or sets the critic rating.
 626        /// </summary>
 627        /// <value>The critic rating.</value>
 628        [JsonIgnore]
 629        public float? CriticRating
 630        {
 511631            get => _criticRating;
 437632            set => _criticRating = FiniteOrNull(value);
 633        }
 634
 635        /// <summary>
 636        /// Gets or sets the custom rating.
 637        /// </summary>
 638        /// <value>The custom rating.</value>
 639        [JsonIgnore]
 640        public string CustomRating { get; set; }
 641
 642        /// <summary>
 643        /// Gets or sets the overview.
 644        /// </summary>
 645        /// <value>The overview.</value>
 646        [JsonIgnore]
 647        public string Overview { get; set; }
 648
 649        /// <summary>
 650        /// Gets or sets the studios.
 651        /// </summary>
 652        /// <value>The studios.</value>
 653        [JsonIgnore]
 654        public string[] Studios { get; set; }
 655
 656        /// <summary>
 657        /// Gets or sets the genres.
 658        /// </summary>
 659        /// <value>The genres.</value>
 660        [JsonIgnore]
 661        public string[] Genres { get; set; }
 662
 663        /// <summary>
 664        /// Gets or sets the tags.
 665        /// </summary>
 666        /// <value>The tags.</value>
 667        [JsonIgnore]
 668        public string[] Tags { get; set; }
 669
 670        [JsonIgnore]
 671        public string[] ProductionLocations { get; set; }
 672
 673        /// <summary>
 674        /// Gets or sets the home page URL.
 675        /// </summary>
 676        /// <value>The home page URL.</value>
 677        [JsonIgnore]
 678        public string HomePageUrl { get; set; }
 679
 680        /// <summary>
 681        /// Gets or sets the community rating.
 682        /// </summary>
 683        /// <value>The community rating.</value>
 684        [JsonIgnore]
 685        public float? CommunityRating
 686        {
 513687            get => _communityRating;
 457688            set => _communityRating = FiniteOrNull(value);
 689        }
 690
 691        /// <summary>
 692        /// Gets or sets the run time ticks.
 693        /// </summary>
 694        /// <value>The run time ticks.</value>
 695        [JsonIgnore]
 696        public long? RunTimeTicks { get; set; }
 697
 698        /// <summary>
 699        /// Gets or sets the production year.
 700        /// </summary>
 701        /// <value>The production year.</value>
 702        [JsonIgnore]
 703        public int? ProductionYear { get; set; }
 704
 705        /// <summary>
 706        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 707        /// This could be episode number, album track number, etc.
 708        /// </summary>
 709        /// <value>The index number.</value>
 710        [JsonIgnore]
 711        public int? IndexNumber { get; set; }
 712
 713        /// <summary>
 714        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 715        /// </summary>
 716        /// <value>The parent index number.</value>
 717        [JsonIgnore]
 718        public int? ParentIndexNumber { get; set; }
 719
 720        [JsonIgnore]
 0721        public virtual bool HasLocalAlternateVersions => false;
 722
 723        [JsonIgnore]
 724        public string OfficialRatingForComparison
 725        {
 726            get
 727            {
 532728                var officialRating = OfficialRating;
 532729                if (!string.IsNullOrEmpty(officialRating))
 730                {
 11731                    return officialRating;
 732                }
 733
 521734                var parent = DisplayParent;
 521735                if (parent is not null)
 736                {
 141737                    return parent.OfficialRatingForComparison;
 738                }
 739
 380740                return null;
 741            }
 742        }
 743
 744        [JsonIgnore]
 745        public string CustomRatingForComparison
 746        {
 747            get
 748            {
 397749                return GetCustomRatingForComparision();
 750            }
 751        }
 752
 753        /// <summary>
 754        /// Gets or sets the provider ids.
 755        /// </summary>
 756        /// <value>The provider ids.</value>
 757        [JsonIgnore]
 758        public Dictionary<string, string> ProviderIds { get; set; }
 759
 760        [JsonIgnore]
 0761        public virtual Folder LatestItemsIndexContainer => null;
 762
 763        [JsonIgnore]
 764        public string PresentationUniqueKey { get; set; }
 765
 766        [JsonIgnore]
 39767        public virtual bool EnableRememberingTrackSelections => true;
 768
 769        [JsonIgnore]
 770        public virtual bool IsTopParent
 771        {
 772            get
 773            {
 402774                if (this is BasePluginFolder || this is Channel)
 775                {
 75776                    return true;
 777                }
 778
 327779                if (this is IHasCollectionType view)
 780                {
 55781                    if (view.CollectionType == CollectionType.livetv)
 782                    {
 0783                        return true;
 784                    }
 785                }
 786
 327787                if (GetParent() is AggregateFolder)
 788                {
 0789                    return true;
 790                }
 791
 327792                return false;
 793            }
 794        }
 795
 796        [JsonIgnore]
 684797        public virtual bool SupportsAncestors => true;
 798
 799        [JsonIgnore]
 180800        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 801
 802        /// <summary>
 803        /// Gets a value indicating whether this item searches the folder it lives in for its own extras.
 804        /// </summary>
 805        [JsonIgnore]
 806        protected virtual bool SearchesContainingFolderForExtras =>
 76807            IsFileProtocol
 76808            && SupportsOwnedItems
 76809            && !IsInMixedFolder
 76810            && this is not (ICollectionFolder or UserRootFolder or AggregateFolder)
 76811            && GetType() != typeof(Folder);
 812
 813        [JsonIgnore]
 75814        public virtual bool SupportsPeople => false;
 815
 816        [JsonIgnore]
 0817        public virtual bool SupportsThemeMedia => false;
 818
 819        [JsonIgnore]
 0820        public virtual bool SupportsInheritedParentImages => false;
 821
 822        /// <summary>
 823        /// Gets a value indicating whether this instance is folder.
 824        /// </summary>
 825        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 826        [JsonIgnore]
 165827        public virtual bool IsFolder => false;
 828
 829        [JsonIgnore]
 0830        public virtual bool IsDisplayedAsFolder => false;
 831
 832        /// <summary>
 833        /// Gets or sets the remote trailers.
 834        /// </summary>
 835        /// <value>The remote trailers.</value>
 836        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 837
 838        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 839        {
 538840            callstack ??= new();
 538841            var customRating = CustomRating;
 538842            if (!string.IsNullOrEmpty(customRating))
 843            {
 6844                return customRating;
 845            }
 846
 532847            callstack.Add(Id);
 848
 532849            var parent = DisplayParent;
 532850            if (parent is not null && !callstack.Contains(parent.Id))
 851            {
 141852                return parent.GetCustomRatingForComparision(callstack);
 853            }
 854
 391855            return null;
 856        }
 857
 858        public virtual double GetDefaultPrimaryImageAspectRatio()
 859        {
 0860            return 0;
 861        }
 862
 863        public virtual string CreatePresentationUniqueKey()
 864        {
 113865            return Id.ToString("N", CultureInfo.InvariantCulture);
 866        }
 867
 868        public virtual bool CanDelete()
 869        {
 0870            if (SourceType == SourceType.Channel)
 871            {
 0872                return ChannelManager.CanDelete(this);
 873            }
 874
 0875            return IsFileProtocol;
 876        }
 877
 878        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 879        {
 0880            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 881            {
 0882                return true;
 883            }
 884
 0885            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 886
 0887            if (SourceType == SourceType.Channel)
 888            {
 0889                return allowed.Contains(ChannelId);
 890            }
 891
 0892            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 893
 0894            foreach (var folder in collectionFolders)
 895            {
 0896                if (allowed.Contains(folder.Id))
 897                {
 0898                    return true;
 899                }
 900            }
 901
 0902            return false;
 0903        }
 904
 905        public BaseItem GetOwner()
 906        {
 957907            var ownerId = OwnerId;
 957908            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 909        }
 910
 911        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 912        {
 6913            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 914        }
 915
 916        public virtual bool CanDelete(User user)
 917        {
 6918            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 919
 6920            return CanDelete(user, allCollectionFolders);
 921        }
 922
 923        public virtual bool CanDownload()
 924        {
 6925            return false;
 926        }
 927
 928        public virtual bool IsAuthorizedToDownload(User user)
 929        {
 0930            return user.HasPermission(PermissionKind.EnableContentDownloading);
 931        }
 932
 933        public bool CanDownload(User user)
 934        {
 6935            return CanDownload() && IsAuthorizedToDownload(user);
 936        }
 937
 938        /// <inheritdoc />
 939        public override string ToString()
 940        {
 69941            return Name;
 942        }
 943
 944        public virtual string GetInternalMetadataPath()
 945        {
 93946            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 947
 93948            return GetInternalMetadataPath(basePath);
 949        }
 950
 951        protected virtual string GetInternalMetadataPath(string basePath)
 952        {
 93953            if (SourceType == SourceType.Channel)
 954            {
 0955                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 956            }
 957
 93958            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 959
 93960            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 961        }
 962
 963        /// <summary>
 964        /// Creates the name of the sort.
 965        /// </summary>
 966        /// <returns>System.String.</returns>
 967        protected virtual string CreateSortName()
 968        {
 184969            return GetSortName(Name, EnableAlphaNumericSorting, ConfigurationManager.Configuration);
 970        }
 971
 972        /// <summary>
 973        /// Turns an item-by-name entity's name into a folder name every supported filesystem accepts.
 974        /// </summary>
 975        /// <param name="name">The entity's name.</param>
 976        /// <returns>The folder name.</returns>
 977        public static string GetItemByNameFolderName(string name)
 978        {
 979            // Trim the period at the end because windows will have a hard time with that
 7980            var validName = FileSystem.GetValidFilename(name).Trim().TrimEnd('.');
 981
 982            // Most Linux filesystems cap a path component at 255 bytes, so a name past that cannot be
 983            // turned into a folder at all - and an entity with no folder can never be created, which
 984            // leaves the credit behind it stuck: not refreshable, not deletable, retried on every scan.
 985            // Only broken provider data gets this long, but it still has to resolve to something, so
 986            // keep a readable prefix and let a hash of the whole name tell two of them apart.
 7987            if (Encoding.UTF8.GetByteCount(validName) <= MaxItemByNameFolderNameBytes)
 988            {
 2989                return validName;
 990            }
 991
 5992            var suffix = "-" + validName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 5993            var budget = MaxItemByNameFolderNameBytes - suffix.Length;
 5994            var length = Math.Min(validName.Length, budget);
 5995            while (length > 0 && Encoding.UTF8.GetByteCount(validName.AsSpan(0, length)) > budget)
 996            {
 0997                length--;
 998            }
 999
 1000            // Never cut a surrogate pair in half, the lone half is not a valid file name character.
 51001            if (length > 0 && char.IsHighSurrogate(validName[length - 1]))
 1002            {
 01003                length--;
 1004            }
 1005
 51006            return string.Concat(validName.AsSpan(0, length).TrimEnd().TrimEnd('.'), suffix);
 1007        }
 1008
 1009        /// <summary>
 1010        /// Cleans a raw name into its sortable form by applying the configured sort rules.
 1011        /// </summary>
 1012        /// <param name="name">The raw name to clean.</param>
 1013        /// <param name="enableAlphaNumericSorting">Whether alphanumeric sorting rules should be applied.</param>
 1014        /// <param name="configuration">The server configuration providing the sort rules.</param>
 1015        /// <returns>The cleaned, sortable name, or <c>null</c> if <paramref name="name"/> is <c>null</c>.</returns>
 1016        public static string GetSortName(string name, bool enableAlphaNumericSorting, ServerConfiguration configuration)
 1017        {
 1911018            if (name is null)
 1019            {
 01020                return null; // some items may not have name filled in properly
 1021            }
 1022
 1911023            if (!enableAlphaNumericSorting)
 1024            {
 11025                return name.TrimStart();
 1026            }
 1027
 1901028            var sortable = name.Trim().ToLowerInvariant();
 1029
 15201030            foreach (var search in configuration.SortRemoveWords)
 1031            {
 1032                // Remove from beginning if a space follows
 5701033                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 1034                {
 61035                    sortable = sortable.Remove(0, search.Length + 1);
 1036                }
 1037
 1038                // Remove from middle if surrounded by spaces
 5701039                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 1040
 1041                // Remove from end if preceeded by a space
 5701042                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 1043                {
 01044                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 1045                }
 1046            }
 1047
 1901048            return ModifySortChunks(ApplySortCharacterRules(sortable, configuration));
 1049        }
 1050
 1051        private static string ApplySortCharacterRules(string name, ServerConfiguration configuration)
 1052        {
 30661053            foreach (var removeChar in configuration.SortRemoveCharacters)
 1054            {
 13141055                name = name.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 1056            }
 1057
 17521058            foreach (var replaceChar in configuration.SortReplaceCharacters)
 1059            {
 6571060                name = name.Replace(replaceChar, " ", StringComparison.Ordinal);
 1061            }
 1062
 2191063            return name;
 1064        }
 1065
 1066        /// <summary>
 1067        /// Normalizes a user-provided name filter value to match against <see cref="SortName"/>.
 1068        /// Applies the same character removal/replacement, zero-padding, diacritics removal, and transliteration
 1069        /// as sort name generation, but not <c>SortRemoveWords</c> since stripping articles from a prefix is unsafe.
 1070        /// </summary>
 1071        /// <param name="input">The raw filter value from the API.</param>
 1072        /// <returns>The normalized value suitable for comparison against SortName. Casing is preserved; callers requiri
 1073        public static string NormalizeSortNameFilter(string input)
 291074            => ModifySortChunks(ApplySortCharacterRules(input, ConfigurationManager.Configuration));
 1075
 1076        /// <summary>
 1077        /// Processes a name by zero-padding numeric chunks to 10 digits for natural sort order,
 1078        /// removing diacritics, and transliterating non-ASCII characters.
 1079        /// </summary>
 1080        /// <param name="name">The input name to process.</param>
 1081        /// <returns>The processed string with padded numeric chunks.</returns>
 1082        public static string ModifySortChunks(ReadOnlySpan<char> name)
 1083        {
 1084            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 1085            {
 1086                if (isDigitChunk && chunk.Length < 10)
 1087                {
 1088                    builder.Append('0', 10 - chunk.Length);
 1089                }
 1090
 1091                builder.Append(chunk);
 1092            }
 1093
 2251094            if (name.IsEmpty)
 1095            {
 11096                return string.Empty;
 1097            }
 1098
 2241099            var builder = new StringBuilder(name.Length);
 1100
 2241101            int chunkStart = 0;
 2241102            bool isDigitChunk = char.IsDigit(name[0]);
 36101103            for (int i = 0; i < name.Length; i++)
 1104            {
 15811105                var isDigit = char.IsDigit(name[i]);
 15811106                if (isDigit != isDigitChunk)
 1107                {
 281108                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 281109                    chunkStart = i;
 281110                    isDigitChunk = isDigit;
 1111                }
 1112            }
 1113
 2241114            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 1115
 1116            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 2241117            var result = builder.ToString().RemoveDiacritics();
 2241118            if (!result.All(char.IsAscii))
 1119            {
 01120                result = result.Transliterated();
 1121            }
 1122
 2241123            return result;
 1124        }
 1125
 1126        public BaseItem GetParent()
 1127        {
 30321128            var parentId = ParentId;
 30321129            if (parentId.IsEmpty())
 1130            {
 24481131                return null;
 1132            }
 1133
 5841134            return LibraryManager.GetItemById(parentId);
 1135        }
 1136
 1137        public IEnumerable<BaseItem> GetParents()
 1138        {
 9671139            var parent = GetParent();
 1140
 11181141            while (parent is not null)
 1142            {
 1521143                yield return parent;
 1144
 1511145                parent = parent.GetParent();
 1146            }
 9661147        }
 1148
 1149        /// <summary>
 1150        /// Finds a parent of a given type.
 1151        /// </summary>
 1152        /// <typeparam name="T">Type of parent.</typeparam>
 1153        /// <returns>``0.</returns>
 1154        public T FindParent<T>()
 1155            where T : Folder
 1156        {
 841157            foreach (var parent in GetParents())
 1158            {
 01159                if (parent is T item)
 1160                {
 01161                    return item;
 1162                }
 1163            }
 1164
 421165            return null;
 01166        }
 1167
 1168        /// <summary>
 1169        /// Gets the play access.
 1170        /// </summary>
 1171        /// <param name="user">The user.</param>
 1172        /// <returns>PlayAccess.</returns>
 1173        public PlayAccess GetPlayAccess(User user)
 1174        {
 61175            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1176            {
 01177                return PlayAccess.None;
 1178            }
 1179
 1180            // if (!user.IsParentalScheduleAllowed())
 1181            // {
 1182            //    return PlayAccess.None;
 1183            // }
 1184
 61185            return PlayAccess.Full;
 1186        }
 1187
 1188        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1189        {
 01190            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01191            {
 01192                ItemId = Id
 01193            });
 1194        }
 1195
 1196        protected virtual bool IsActiveRecording()
 1197        {
 01198            return false;
 1199        }
 1200
 1201        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1202        {
 81203            if (SourceType == SourceType.Channel)
 1204            {
 01205                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01206                           .ToList();
 1207
 01208                if (sources.Count > 0)
 1209                {
 01210                    return sources;
 1211                }
 1212            }
 1213
 81214            var list = GetAllItemsForMediaSources().ToList();
 81215            var commonPrefix = GetCommonNamePrefix(list);
 81216            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType, commonPrefix
 1217
 81218            if (IsActiveRecording())
 1219            {
 01220                foreach (var mediaSource in result)
 1221                {
 01222                    mediaSource.Type = MediaSourceType.Placeholder;
 1223                }
 1224            }
 1225
 1226            // The source belonging to the item being queried sorts first so it is the default the client plays.
 81227            var selfId = Id.ToString("N", CultureInfo.InvariantCulture);
 1228
 81229            return result
 81230                .OrderByDescending(i => string.Equals(i.Id, selfId, StringComparison.OrdinalIgnoreCase))
 81231                .ThenBy(i => i.VideoType == VideoType.VideoFile ? 0 : 1)
 81232                .ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 81233                .ThenByDescending(i => i, new MediaSourceWidthComparator())
 81234                .ToArray();
 1235        }
 1236
 1237        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1238        {
 01239            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1240        }
 1241
 1242        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type, string 
 1243        {
 221244            ArgumentNullException.ThrowIfNull(item);
 1245
 221246            var protocol = item.PathProtocol;
 221247            var itemPath = item.Path;
 1248
 221249            var info = new MediaSourceInfo
 221250            {
 221251                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 221252                Protocol = protocol ?? MediaProtocol.File,
 221253                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 221254                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 221255                Name = GetMediaSourceName(item, commonPrefix),
 221256                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 221257                RunTimeTicks = item.RunTimeTicks,
 221258                Container = item.Container,
 221259                Size = item.Size,
 221260                Type = type,
 221261                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 221262                    && (protocol is null or MediaProtocol.File)
 221263                    && MediaSegmentManager.HasSegments(item.Id)
 221264            };
 1265
 221266            if (string.IsNullOrEmpty(info.Path))
 1267            {
 01268                info.Type = MediaSourceType.Placeholder;
 1269            }
 1270
 221271            if (info.Protocol == MediaProtocol.File)
 1272            {
 221273                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1274            }
 1275
 221276            var video = item as Video;
 221277            if (video is not null)
 1278            {
 221279                info.IsoType = video.IsoType;
 221280                info.VideoType = video.VideoType;
 221281                info.Video3DFormat = video.Video3DFormat;
 221282                info.Timestamp = video.Timestamp;
 1283
 221284                if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
 1285                {
 01286                    var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
 1287
 1288                    // Only allow remote shortcut paths — local file paths in .strm files
 1289                    // could be used to read arbitrary files from the server.
 01290                    if (shortcutProtocol != MediaProtocol.File)
 1291                    {
 01292                        info.IsRemote = true;
 01293                        info.Path = video.ShortcutPath;
 01294                        info.Protocol = shortcutProtocol;
 1295                    }
 1296                }
 1297
 221298                if (string.IsNullOrEmpty(info.Container))
 1299                {
 221300                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1301                    {
 221302                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1303                        {
 221304                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1305                        }
 1306                    }
 1307                }
 1308            }
 1309
 221310            if (string.IsNullOrEmpty(info.Container))
 1311            {
 01312                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1313                {
 01314                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1315                }
 1316            }
 1317
 221318            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1319            {
 221320                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1321            }
 1322
 221323            if (video is not null && video.VideoType != VideoType.VideoFile)
 1324            {
 01325                info.SupportsDirectStream = false;
 1326            }
 1327
 221328            info.Bitrate = item.TotalBitrate;
 221329            info.InferTotalBitrate();
 1330
 221331            return info;
 1332        }
 1333
 1334        internal string GetMediaSourceName(BaseItem item, string commonPrefix = null)
 1335        {
 481336            var terms = new List<string>();
 1337
 481338            var path = item.Path;
 481339            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1340            {
 481341                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 1342
 1343                // Prefer the suffix that differs from the other versions: strip the prefix shared by
 1344                // all sibling files. This works regardless of folder layout, so it also labels episode
 1345                // versions that share a season folder (e.g. "Greyscale" instead of the full
 1346                // "Show - S01E02 - Title - Greyscale"). The prefix is already retreated to a delimiter
 1347                // boundary (see GetCommonVersionPrefix).
 481348                if (!string.IsNullOrEmpty(commonPrefix)
 481349                    && displayName.Length > commonPrefix.Length
 481350                    && displayName.StartsWith(commonPrefix, StringComparison.OrdinalIgnoreCase))
 1351                {
 351352                    var name = displayName.AsSpan(commonPrefix.Length).TrimStart([' ', .. VersionDelimiters]);
 351353                    if (!name.IsWhiteSpace())
 1354                    {
 351355                        terms.Add(name.ToString());
 1356                    }
 1357                }
 1358
 1359                // Fall back to the containing folder name (the common layout for movie versions, and
 1360                // the path taken when no common prefix could be derived).
 481361                if (terms.Count == 0 && HasLocalAlternateVersions)
 1362                {
 81363                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 81364                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1365                    {
 21366                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', .. VersionDelimiters]
 21367                        if (!name.IsWhiteSpace())
 1368                        {
 21369                            terms.Add(name.ToString());
 1370                        }
 1371                    }
 1372                }
 1373
 481374                if (terms.Count == 0)
 1375                {
 111376                    terms.Add(displayName);
 1377                }
 1378            }
 1379
 481380            if (terms.Count == 0)
 1381            {
 01382                terms.Add(item.Name);
 1383            }
 1384
 481385            if (item is Video video)
 1386            {
 481387                if (video.Video3DFormat.HasValue)
 1388                {
 01389                    terms.Add("3D");
 1390                }
 1391
 481392                if (video.VideoType == VideoType.BluRay)
 1393                {
 01394                    terms.Add("Bluray");
 1395                }
 481396                else if (video.VideoType == VideoType.Dvd)
 1397                {
 01398                    terms.Add("DVD");
 1399                }
 481400                else if (video.VideoType == VideoType.Iso)
 1401                {
 01402                    if (video.IsoType.HasValue)
 1403                    {
 01404                        if (video.IsoType.Value == IsoType.BluRay)
 1405                        {
 01406                            terms.Add("Bluray");
 1407                        }
 01408                        else if (video.IsoType.Value == IsoType.Dvd)
 1409                        {
 01410                            terms.Add("DVD");
 1411                        }
 1412                    }
 1413                    else
 1414                    {
 01415                        terms.Add("ISO");
 1416                    }
 1417                }
 1418            }
 1419
 481420            return string.Join('/', terms);
 1421        }
 1422
 1423        /// <summary>
 1424        /// Treats a rating or gain that is not a finite number as no value, since NaN and infinity
 1425        /// cannot be stored by every database or written as JSON.
 1426        /// </summary>
 1427        /// <param name="value">The value to check.</param>
 1428        /// <returns><paramref name="value"/> when it is finite or has no value, otherwise <see langword="null"/>.</retu
 1429        private static float? FiniteOrNull(float? value)
 14301430            => value.HasValue && !float.IsFinite(value.Value) ? null : value;
 1431
 1432        /// <summary>
 1433        /// Derives the prefix shared by the supplied media source items' file names, used to strip the
 1434        /// common part and surface a short version label per source. Returns null when there are fewer
 1435        /// than two file-based sources, since there is nothing to differentiate.
 1436        /// </summary>
 1437        /// <param name="items">The media source items.</param>
 1438        /// <returns>The shared prefix, or null when no useful prefix exists.</returns>
 1439        private static string GetCommonNamePrefix(IReadOnlyList<(BaseItem Item, MediaSourceType MediaSourceType)> items)
 1440        {
 81441            var fileNames = new List<string>();
 601442            foreach (var (item, _) in items)
 1443            {
 221444                if (item.IsFileProtocol && !string.IsNullOrEmpty(item.Path))
 1445                {
 221446                    fileNames.Add(System.IO.Path.GetFileNameWithoutExtension(item.Path));
 1447                }
 1448            }
 1449
 81450            if (fileNames.Count < 2)
 1451            {
 01452                return null;
 1453            }
 1454
 81455            var prefix = GetCommonVersionPrefix(fileNames);
 81456            return string.IsNullOrEmpty(prefix) ? null : prefix;
 1457        }
 1458
 1459        /// <summary>
 1460        /// Computes the case-insensitive longest common prefix of the supplied version file names,
 1461        /// retreated to the last delimiter boundary. Retreating keeps the differing suffix intact:
 1462        /// it avoids slicing through a word every version shares (e.g. "Grey" in "Greyscale" and
 1463        /// "Greyish") while still trimming the common part when every version is suffixed (e.g.
 1464        /// "- Greyscale" / "- Colorized"). It prefers a structural delimiter ('-', '_', '.') so a
 1465        /// token shared by the descriptors but separated only by spaces (e.g. a common "2160p ") is
 1466        /// kept in the label, falling back to a space only when no structural delimiter is shared. The
 1467        /// separators mirror the version delimiters recognised by the naming layer (Emby.Naming
 1468        /// VideoFlagDelimiters), except that a dot between digits is a decimal point rather than a
 1469        /// delimiter, so numeric version labels stay whole.
 1470        /// </summary>
 1471        /// <param name="fileNames">The version file names without extension; must contain at least one entry.</param>
 1472        /// <returns>The shared prefix retreated to a separator boundary, or an empty string when none is shared.</retur
 1473        internal static string GetCommonVersionPrefix(IReadOnlyList<string> fileNames)
 1474        {
 201475            var prefix = fileNames[0];
 941476            for (var i = 1; i < fileNames.Count && prefix.Length > 0; i++)
 1477            {
 271478                var name = fileNames[i];
 271479                var length = Math.Min(prefix.Length, name.Length);
 271480                var common = 0;
 4331481                while (common < length && char.ToUpperInvariant(prefix[common]) == char.ToUpperInvariant(name[common]))
 1482                {
 4061483                    common++;
 1484                }
 1485
 271486                prefix = prefix[..common];
 1487            }
 1488
 1489            // If the common prefix is itself a whole file name then one version is unlabelled (the
 1490            // base name); the boundary already sits at the end of that name, so don't retreat into it.
 201491            var prefixIsWholeName = false;
 941492            for (var i = 0; i < fileNames.Count; i++)
 1493            {
 361494                if (fileNames[i].Length == prefix.Length)
 1495                {
 91496                    prefixIsWholeName = true;
 91497                    break;
 1498                }
 1499            }
 1500
 201501            if (!prefixIsWholeName)
 1502            {
 1503                // Retreat to the last structural delimiter ('-', '_', '.'), skipping dots that are
 1504                // decimal points within a number rather than delimiters (see IsDecimalPoint).
 111505                var cut = prefix.Length;
 711506                while (cut > 0
 711507                    && (Array.IndexOf(VersionDelimiters, prefix[cut - 1]) < 0
 711508                        || IsDecimalPoint(prefix, cut - 1, fileNames)))
 1509                {
 601510                    cut--;
 1511                }
 1512
 111513                if (cut == 0)
 1514                {
 21515                    cut = prefix.Length;
 51516                    while (cut > 0 && prefix[cut - 1] != ' ')
 1517                    {
 31518                        cut--;
 1519                    }
 1520                }
 1521
 111522                prefix = prefix[..cut];
 1523            }
 1524
 201525            return prefix;
 1526        }
 1527
 1528        private static bool IsDecimalPoint(string prefix, int index, IReadOnlyList<string> fileNames)
 1529        {
 121530            if (index == 0 || prefix[index] != '.' || !char.IsDigit(prefix[index - 1]))
 1531            {
 81532                return false;
 1533            }
 1534
 41535            if (index + 1 < prefix.Length)
 1536            {
 01537                return char.IsDigit(prefix[index + 1]);
 1538            }
 1539
 1540            // The dot ends the prefix, so the character after it is the first one that differs between
 1541            // the versions: only a decimal point when every version continues the number.
 221542            for (var i = 0; i < fileNames.Count; i++)
 1543            {
 81544                if (fileNames[i].Length <= index + 1 || !char.IsDigit(fileNames[i][index + 1]))
 1545                {
 11546                    return false;
 1547                }
 1548            }
 1549
 31550            return true;
 1551        }
 1552
 1553        public Task RefreshMetadata(CancellationToken cancellationToken)
 1554        {
 821555            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1556        }
 1557
 1558        /// <summary>
 1559        /// The base implementation to refresh metadata.
 1560        /// </summary>
 1561        /// <param name="options">The options.</param>
 1562        /// <param name="cancellationToken">The cancellation token.</param>
 1563        /// <returns>true if a provider reports we changed.</returns>
 1564        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1565        {
 1061566            var requiresSave = false;
 1567
 1061568            if (SupportsOwnedItems)
 1569            {
 1570                try
 1571                {
 721572                    if (IsFileProtocol)
 1573                    {
 721574                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1575                    }
 1576
 721577                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 721578                }
 01579                catch (Exception ex)
 1580                {
 01581                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01582                }
 1583            }
 1584
 1061585            var refreshOptions = requiresSave
 1061586                ? new MetadataRefreshOptions(options)
 1061587                {
 1061588                    ForceSave = true
 1061589                }
 1061590                : options;
 1591
 1061592            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 1051593        }
 1594
 1595        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1596        {
 01597            if (!IsVisible(user))
 1598            {
 01599                return false;
 1600            }
 1601
 01602            var parents = GetParents().ToList();
 01603            if (parents.Any(i => !i.IsVisible(user, true)))
 1604            {
 01605                return false;
 1606            }
 1607
 01608            if (checkFolders)
 1609            {
 01610                var topParent = parents.Count > 0 ? parents[^1] : this;
 1611
 01612                if (string.IsNullOrEmpty(topParent.Path))
 1613                {
 01614                    return true;
 1615                }
 1616
 01617                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1618
 01619                if (itemCollectionFolders.Count > 0)
 1620                {
 01621                    var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 1622                    IEnumerable<Guid> userCollectionFolderIds;
 01623                    if (blockedMediaFolders.Length > 0)
 1624                    {
 1625                        // User has blocked folders - get all library folders and exclude blocked ones
 01626                        userCollectionFolderIds = LibraryManager.GetUserRootFolder().Children
 01627                            .Select(i => i.Id)
 01628                            .Where(id => !blockedMediaFolders.Contains(id));
 1629                    }
 01630                    else if (user.HasPermission(PermissionKind.EnableAllFolders))
 1631                    {
 1632                        // User can access all folders - no need to filter
 01633                        return true;
 1634                    }
 1635                    else
 1636                    {
 1637                        // User has specific enabled folders
 01638                        userCollectionFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders);
 1639                    }
 1640
 01641                    if (!itemCollectionFolders.Any(userCollectionFolderIds.Contains))
 1642                    {
 01643                        return false;
 1644                    }
 1645                }
 1646            }
 1647
 01648            return true;
 1649        }
 1650
 1651        public void SetParent(Folder parent)
 1652        {
 261653            ParentId = parent is null ? Guid.Empty : parent.Id;
 261654        }
 1655
 1656        /// <summary>
 1657        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1658        /// Returns true or false indicating if changes were found.
 1659        /// </summary>
 1660        /// <param name="options">The metadata refresh options.</param>
 1661        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1662        /// <param name="cancellationToken">The cancellation token.</param>
 1663        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1664        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1665        {
 741666            if (!SearchesContainingFolderForExtras)
 1667            {
 721668                return false;
 1669            }
 1670
 21671            if (GetParent() is Folder container
 21672                && container.SearchesContainingFolderForExtras
 21673                && string.Equals(container.Path, ContainingFolderPath, StringComparison.OrdinalIgnoreCase))
 1674            {
 11675                return false;
 1676            }
 1677
 11678            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 741679        }
 1680
 1681        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1682        {
 561683            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1684        }
 1685
 1686        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1687        {
 1688            // An extra is owned by the version it is named after, so all of them are maintained together.
 11689            var currentExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 11690            {
 11691                OwnerIds = item.GetOwnedVersionIds()
 11692            }).Where(e => e.ExtraType.HasValue).ToList();
 1693
 11694            var currentExtraIds = currentExtras.Select(e => e.Id).ToArray();
 1695
 1696            // Snapshot the persisted names before resolving, as FindExtras corrects the name on the
 1697            // items it hands back and may well hand back these very instances.
 11698            var currentExtraNames = new Dictionary<Guid, string>();
 21699            foreach (var extra in currentExtras)
 1700            {
 01701                currentExtraNames[extra.Id] = extra.Name;
 1702            }
 1703
 11704            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 11705            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1706
 11707            var renamedExtraIds = extras
 11708                .Where(e => currentExtraNames.TryGetValue(e.Id, out var oldName) && !string.Equals(oldName, e.Name, Stri
 11709                .Select(e => e.Id)
 11710                .ToHashSet();
 1711
 11712            var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
 1713
 11714            if (!extrasChanged && renamedExtraIds.Count == 0 && !options.ReplaceAllMetadata && options.MetadataRefreshMo
 1715            {
 1716                // The owner's dates may only have become known after its extras were created, so keep
 1717                // them in sync even when there is nothing to refresh.
 21718                foreach (var extra in currentExtras)
 1719                {
 01720                    if (extra.ExtraType is not null && InheritDatesFromOwner(item, extra))
 1721                    {
 01722                        await extra.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwa
 1723                    }
 1724                }
 1725
 11726                return false;
 1727            }
 1728
 01729            var tasks = extras.Select(i =>
 01730            {
 01731                var ownerId = item.GetOwnerIdForExtra(i);
 01732                var subOptions = new MetadataRefreshOptions(options);
 01733                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty() || renamedExtraIds.Contains(i.Id))
 01734                {
 01735                    subOptions.ForceSave = true;
 01736                }
 01737
 01738                i.OwnerId = ownerId;
 01739                i.ParentId = Guid.Empty;
 01740
 01741                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 01742            });
 1743
 01744            var removedExtraIds = currentExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 01745            if (removedExtraIds.Length > 0)
 1746            {
 01747                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 01748                {
 01749                    ItemIds = removedExtraIds
 01750                });
 01751                foreach (var removedExtra in removedExtras)
 1752                {
 1753                    // Only delete items that are actual extras (have ExtraType set)
 1754                    // Items with OwnerId but no ExtraType might be alternate versions, not extras
 01755                    if (removedExtra.ExtraType.HasValue)
 1756                    {
 01757                        LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 01758                        {
 01759                            DeleteFileLocation = false
 01760                        });
 1761                    }
 1762                }
 1763            }
 1764
 01765            await Task.WhenAll(tasks).ConfigureAwait(false);
 1766
 01767            return true;
 11768        }
 1769
 1770        public string GetPresentationUniqueKey()
 1771        {
 01772            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1773        }
 1774
 1775        public virtual bool RequiresRefresh()
 1776        {
 1061777            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1778            {
 1011779                return false;
 1780            }
 1781
 51782            var info = FileSystem.GetFileSystemInfo(Path);
 1783
 51784            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1785        }
 1786
 1787        public virtual List<string> GetUserDataKeys()
 1788        {
 2471789            var list = new List<string>();
 1790
 2471791            if (SourceType == SourceType.Channel)
 1792            {
 01793                if (!string.IsNullOrEmpty(ExternalId))
 1794                {
 01795                    list.Add(ExternalId);
 1796                }
 1797            }
 1798
 2471799            list.Add(Id.ToString());
 2471800            return list;
 1801        }
 1802
 1803        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1804        {
 121805            var updateType = ItemUpdateType.None;
 1806
 121807            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1808            {
 01809                IsInMixedFolder = newItem.IsInMixedFolder;
 01810                updateType |= ItemUpdateType.MetadataImport;
 1811            }
 1812
 121813            return updateType;
 1814        }
 1815
 1816        public void AfterMetadataRefresh()
 1817        {
 1121818            _sortName = null;
 1121819        }
 1820
 1821        /// <summary>
 1822        /// Gets the preferred metadata language.
 1823        /// </summary>
 1824        /// <returns>System.String.</returns>
 1825        public string GetPreferredMetadataLanguage()
 1826        {
 491827            string lang = PreferredMetadataLanguage;
 1828
 491829            if (string.IsNullOrEmpty(lang))
 1830            {
 461831                lang = GetParents()
 461832                    .Select(i => i.PreferredMetadataLanguage)
 461833                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1834            }
 1835
 491836            if (string.IsNullOrEmpty(lang))
 1837            {
 461838                lang = LibraryManager.GetCollectionFolders(this)
 461839                    .Select(i => i.PreferredMetadataLanguage)
 461840                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1841            }
 1842
 491843            if (string.IsNullOrEmpty(lang))
 1844            {
 461845                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1846            }
 1847
 491848            if (string.IsNullOrEmpty(lang))
 1849            {
 461850                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1851            }
 1852
 491853            return lang;
 1854        }
 1855
 1856        /// <summary>
 1857        /// Gets the preferred metadata country code.
 1858        /// </summary>
 1859        /// <returns>System.String.</returns>
 1860        public string GetPreferredMetadataCountryCode()
 1861        {
 661862            string lang = PreferredMetadataCountryCode;
 1863
 661864            if (string.IsNullOrEmpty(lang))
 1865            {
 461866                lang = GetParents()
 461867                    .Select(i => i.PreferredMetadataCountryCode)
 461868                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1869            }
 1870
 661871            if (string.IsNullOrEmpty(lang))
 1872            {
 461873                lang = LibraryManager.GetCollectionFolders(this)
 461874                    .Select(i => i.PreferredMetadataCountryCode)
 461875                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1876            }
 1877
 661878            if (string.IsNullOrEmpty(lang))
 1879            {
 461880                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1881            }
 1882
 661883            if (string.IsNullOrEmpty(lang))
 1884            {
 461885                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1886            }
 1887
 661888            return lang;
 1889        }
 1890
 1891        /// <summary>
 1892        /// Gets the original language of the item, inheriting from parent items if necessary.
 1893        /// </summary>
 1894        /// <returns>System.String.</returns>
 1895        public virtual string GetInheritedOriginalLanguage()
 1896        {
 01897            return OriginalLanguage;
 1898        }
 1899
 1900        public virtual bool IsSaveLocalMetadataEnabled()
 1901        {
 1141902            if (SourceType == SourceType.Channel)
 1903            {
 01904                return false;
 1905            }
 1906
 1141907            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1908
 1141909            return libraryOptions.SaveLocalMetadata;
 1910        }
 1911
 1912        /// <summary>
 1913        /// Determines if a given user has access to this item.
 1914        /// </summary>
 1915        /// <param name="user">The user.</param>
 1916        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1917        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1918        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1919        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1920        {
 111921            ArgumentNullException.ThrowIfNull(user);
 1922
 111923            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1924            {
 01925                return false;
 1926            }
 1927
 111928            var maxAllowedRating = user.MaxParentalRatingScore;
 111929            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 111930            var rating = CustomRatingForComparison;
 1931
 111932            if (string.IsNullOrEmpty(rating))
 1933            {
 111934                rating = OfficialRatingForComparison;
 1935            }
 1936
 111937            if (string.IsNullOrEmpty(rating))
 1938            {
 111939                return !GetBlockUnratedValue(user);
 1940            }
 1941
 01942            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1943
 1944            // Could not determine rating level
 01945            if (ratingScore is null)
 1946            {
 01947                var isAllowed = !GetBlockUnratedValue(user);
 1948
 01949                if (!isAllowed)
 1950                {
 01951                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1952                }
 1953
 01954                return isAllowed;
 1955            }
 1956
 01957            if (!maxAllowedRating.HasValue)
 1958            {
 01959                return true;
 1960            }
 1961
 01962            if (ratingScore.Score != maxAllowedRating.Value)
 1963            {
 01964                return ratingScore.Score < maxAllowedRating.Value;
 1965            }
 1966
 01967            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1968        }
 1969
 1970        public ParentalRatingScore GetParentalRatingScore()
 1971        {
 3861972            var rating = CustomRatingForComparison;
 1973
 3861974            if (string.IsNullOrEmpty(rating))
 1975            {
 3801976                rating = OfficialRatingForComparison;
 1977            }
 1978
 3861979            if (string.IsNullOrEmpty(rating))
 1980            {
 3691981                return null;
 1982            }
 1983
 171984            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1985        }
 1986
 1987        public List<string> GetInheritedTags()
 1988        {
 1721989            var list = new List<string>();
 1721990            list.AddRange(Tags);
 1991
 4421992            foreach (var parent in GetParents())
 1993            {
 491994                list.AddRange(parent.Tags);
 1995            }
 1996
 3621997            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1998            {
 91999                list.AddRange(folder.Tags);
 2000            }
 2001
 1722002            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 2003        }
 2004
 2005        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 2006        {
 112007            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 112008            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 2009
 112010            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 2011            {
 102012                return true;
 2013            }
 2014
 2015            // Normalize tags using the same logic as database queries
 12016            var normalizedBlockedTags = blockedTags
 12017                .Where(t => !string.IsNullOrWhiteSpace(t))
 12018                .Select(t => t.GetCleanValue())
 12019                .ToHashSet(StringComparer.Ordinal);
 2020
 12021            var normalizedItemTags = GetInheritedTags()
 12022                .Select(t => t.GetCleanValue())
 12023                .ToHashSet(StringComparer.Ordinal);
 2024
 2025            // Check blocked tags - item is hidden if it has any blocked tag
 12026            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 2027            {
 02028                return false;
 2029            }
 2030
 12031            var parent = GetParents().FirstOrDefault() ?? this;
 12032            if (parent is UserRootFolder or AggregateFolder or UserView)
 2033            {
 12034                return true;
 2035            }
 2036
 2037            // Check allowed tags - item must have at least one allowed tag
 02038            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 2039            {
 02040                var normalizedAllowedTags = allowedTags
 02041                    .Where(t => !string.IsNullOrWhiteSpace(t))
 02042                    .Select(t => t.GetCleanValue())
 02043                    .ToHashSet(StringComparer.Ordinal);
 2044
 02045                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 2046                {
 02047                    return false;
 2048                }
 2049            }
 2050
 02051            return true;
 2052        }
 2053
 2054        public virtual UnratedItem GetBlockUnratedType()
 2055        {
 1572056            if (SourceType == SourceType.Channel)
 2057            {
 02058                return UnratedItem.ChannelContent;
 2059            }
 2060
 1572061            return UnratedItem.Other;
 2062        }
 2063
 2064        /// <summary>
 2065        /// Gets a bool indicating if access to the unrated item is blocked or not.
 2066        /// </summary>
 2067        /// <param name="user">The configuration.</param>
 2068        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 2069        protected virtual bool GetBlockUnratedValue(User user)
 2070        {
 2071            // Don't block plain folders that are unrated. Let the media underneath get blocked
 2072            // Special folders like series and albums will override this method.
 112073            if (IsFolder || this is IItemByName)
 2074            {
 82075                return false;
 2076            }
 2077
 32078            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 2079        }
 2080
 2081        /// <summary>
 2082        /// Determines if this folder should be visible to a given user.
 2083        /// Default is just parental allowed. Can be overridden for more functionality.
 2084        /// </summary>
 2085        /// <param name="user">The user.</param>
 2086        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 2087        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 2088        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 2089        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 2090        {
 82091            ArgumentNullException.ThrowIfNull(user);
 2092
 82093            return IsParentalAllowed(user, skipAllowedTagsCheck);
 2094        }
 2095
 2096        public virtual bool IsVisibleStandalone(User user)
 2097        {
 02098            if (SourceType == SourceType.Channel)
 2099            {
 02100                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 2101            }
 2102
 02103            return IsVisibleStandaloneInternal(user, true);
 2104        }
 2105
 2106        public virtual string GetClientTypeName()
 2107        {
 1822108            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 2109            {
 02110                return "ChannelFolderItem";
 2111            }
 2112
 1822113            return GetType().Name;
 2114        }
 2115
 2116        public BaseItemKind GetBaseItemKind()
 2117        {
 4222118            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 2119        }
 2120
 2121        /// <summary>
 2122        /// Gets the linked child.
 2123        /// </summary>
 2124        /// <param name="info">The info.</param>
 2125        /// <returns>BaseItem.</returns>
 2126        protected BaseItem GetLinkedChild(LinkedChild info)
 2127        {
 2128            // First get using the cached Id
 02129            if (info.ItemId.HasValue)
 2130            {
 02131                if (info.ItemId.Value.IsEmpty())
 2132                {
 02133                    return null;
 2134                }
 2135
 02136                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 2137
 02138                if (itemById is not null)
 2139                {
 02140                    return itemById;
 2141                }
 2142            }
 2143
 02144            var item = FindLinkedChild(info);
 2145
 2146            // If still null, log
 02147            if (item is null)
 2148            {
 2149                // Don't keep searching over and over
 02150                info.ItemId = Guid.Empty;
 2151            }
 2152            else
 2153            {
 2154                // Cache the id for next time
 02155                info.ItemId = item.Id;
 2156            }
 2157
 02158            return item;
 2159        }
 2160
 2161#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 2162        private BaseItem FindLinkedChild(LinkedChild info)
 2163        {
 2164            // First try to find by ItemId (new preferred method)
 02165            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 2166            {
 02167                var item = LibraryManager.GetItemById(info.ItemId.Value);
 02168                if (item is not null)
 2169                {
 02170                    return item;
 2171                }
 2172
 02173                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 2174            }
 2175
 2176            // Fall back to Path (legacy method)
 02177            var path = info.Path;
 02178            if (!string.IsNullOrEmpty(path))
 2179            {
 02180                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 2181
 02182                var itemByPath = LibraryManager.FindByPath(path, null);
 2183
 02184                if (itemByPath is null)
 2185                {
 02186                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 2187                }
 2188
 02189                return itemByPath;
 2190            }
 2191
 2192            // Fall back to LibraryItemId (legacy method)
 02193            if (!string.IsNullOrEmpty(info.LibraryItemId))
 2194            {
 02195                var item = LibraryManager.GetItemById(info.LibraryItemId);
 2196
 02197                if (item is null)
 2198                {
 02199                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 2200                }
 2201
 02202                return item;
 2203            }
 2204
 02205            return null;
 2206        }
 2207#pragma warning restore CS0618
 2208
 2209        /// <summary>
 2210        /// Adds a studio to the item.
 2211        /// </summary>
 2212        /// <param name="name">The name.</param>
 2213        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 2214        public void AddStudio(string name)
 2215        {
 42216            ArgumentException.ThrowIfNullOrEmpty(name);
 42217            var current = Studios;
 2218
 42219            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 2220            {
 42221                int curLen = current.Length;
 42222                if (curLen == 0)
 2223                {
 42224                    Studios = [name];
 2225                }
 2226                else
 2227                {
 02228                    Studios = [.. current, name];
 2229                }
 2230            }
 02231        }
 2232
 2233        public void SetStudios(IEnumerable<string> names)
 2234        {
 202235            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 202236        }
 2237
 2238        /// <summary>
 2239        /// Adds a genre to the item.
 2240        /// </summary>
 2241        /// <param name="name">The name.</param>
 2242        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 2243        public void AddGenre(string name)
 2244        {
 132245            ArgumentException.ThrowIfNullOrEmpty(name);
 2246
 132247            var genres = Genres;
 132248            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 2249            {
 132250                Genres = [.. genres, name];
 2251            }
 132252        }
 2253
 2254        /// <summary>
 2255        /// Marks the played.
 2256        /// </summary>
 2257        /// <param name="user">The user.</param>
 2258        /// <param name="datePlayed">The date played.</param>
 2259        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 2260        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2261        public virtual void MarkPlayed(
 2262            User user,
 2263            DateTime? datePlayed,
 2264            bool resetPosition)
 2265        {
 02266            ArgumentNullException.ThrowIfNull(user);
 2267
 02268            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 02269            {
 02270                Key = GetUserDataKeys().First(),
 02271            };
 2272
 02273            if (datePlayed.HasValue)
 2274            {
 2275                // Increment
 02276                data.PlayCount++;
 2277            }
 2278
 2279            // Ensure it's at least one
 02280            data.PlayCount = Math.Max(data.PlayCount, 1);
 2281
 02282            if (resetPosition)
 2283            {
 02284                data.PlaybackPositionTicks = 0;
 2285            }
 2286
 02287            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 02288            data.Played = true;
 2289
 02290            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02291        }
 2292
 2293        /// <summary>
 2294        /// Marks the unplayed.
 2295        /// </summary>
 2296        /// <param name="user">The user.</param>
 2297        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2298        public virtual void MarkUnplayed(User user)
 2299        {
 02300            ArgumentNullException.ThrowIfNull(user);
 2301
 02302            var data = UserDataManager.GetUserData(user, this);
 2303
 2304            // I think it is okay to do this here.
 2305            // if this is only called when a user is manually forcing something to un-played
 2306            // then it probably is what we want to do...
 02307            ResetPlayedState(data);
 2308
 02309            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02310        }
 2311
 2312        /// <summary>
 2313        /// Clears the played state on the supplied user data.
 2314        /// </summary>
 2315        /// <param name="data">The user data to reset.</param>
 2316        protected static void ResetPlayedState(UserItemData data)
 2317        {
 22318            ArgumentNullException.ThrowIfNull(data);
 2319
 22320            data.PlayCount = 0;
 22321            data.PlaybackPositionTicks = 0;
 22322            data.LastPlayedDate = null;
 22323            data.Played = false;
 22324        }
 2325
 2326        /// <summary>
 2327        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2328        /// </summary>
 2329        public virtual void ChangedExternally()
 2330        {
 02331            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02332        }
 2333
 2334        /// <summary>
 2335        /// Gets an image.
 2336        /// </summary>
 2337        /// <param name="type">The type.</param>
 2338        /// <param name="imageIndex">Index of the image.</param>
 2339        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2340        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2341        public bool HasImage(ImageType type, int imageIndex)
 2342        {
 1532343            return GetImageInfo(type, imageIndex) is not null;
 2344        }
 2345
 2346        public void SetImage(ItemImageInfo image, int index)
 2347        {
 242348            if (image.Type == ImageType.Chapter)
 2349            {
 02350                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2351            }
 2352
 242353            var existingImage = GetImageInfo(image.Type, index);
 2354
 242355            if (existingImage is null)
 2356            {
 222357                AddImage(image);
 2358            }
 2359            else
 2360            {
 22361                existingImage.Path = image.Path;
 22362                existingImage.DateModified = image.DateModified;
 22363                existingImage.Width = image.Width;
 22364                existingImage.Height = image.Height;
 22365                existingImage.BlurHash = image.BlurHash;
 2366            }
 22367        }
 2368
 2369        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2370        {
 492371            if (type == ImageType.Chapter)
 2372            {
 02373                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2374            }
 2375
 492376            var image = GetImageInfo(type, index);
 2377
 492378            if (image is null)
 2379            {
 482380                AddImage(GetImageInfo(file, type));
 2381            }
 2382            else
 2383            {
 12384                var imageInfo = GetImageInfo(file, type);
 2385
 12386                image.Path = file.FullName;
 12387                image.DateModified = imageInfo.DateModified;
 2388
 2389                // reset these values
 12390                image.Width = 0;
 12391                image.Height = 0;
 2392            }
 12393        }
 2394
 2395        /// <summary>
 2396        /// Deletes the image.
 2397        /// </summary>
 2398        /// <param name="type">The type.</param>
 2399        /// <param name="index">The index.</param>
 2400        /// <returns>A task.</returns>
 2401        public async Task DeleteImageAsync(ImageType type, int index)
 2402        {
 02403            var info = GetImageInfo(type, index);
 2404
 02405            if (info is null)
 2406            {
 2407                // Nothing to do
 02408                return;
 2409            }
 2410
 2411            // Remove from file system
 02412            var path = info.Path;
 02413            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2414            {
 02415                FileSystem.DeleteFile(path);
 2416            }
 2417
 2418            // Remove from item
 02419            RemoveImage(info);
 2420
 02421            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02422        }
 2423
 2424        public void RemoveImage(ItemImageInfo image)
 2425        {
 02426            RemoveImages([image]);
 02427        }
 2428
 2429        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2430        {
 92431            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92432        }
 2433
 2434        public void AddImage(ItemImageInfo image)
 2435        {
 702436            ImageInfos = [.. ImageInfos, image];
 702437        }
 2438
 2439        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1612440         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2441
 2442        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 452443            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2444
 2445        /// <summary>
 2446        /// Validates that images within the item are still on the filesystem.
 2447        /// </summary>
 2448        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2449        public bool ValidateImages()
 2450        {
 1292451            List<ItemImageInfo> deletedImages = null;
 2942452            foreach (var imageInfo in ImageInfos)
 2453            {
 182454                if (!imageInfo.IsLocalFile)
 2455                {
 2456                    continue;
 2457                }
 2458
 182459                if (File.Exists(imageInfo.Path))
 2460                {
 2461                    continue;
 2462                }
 2463
 32464                (deletedImages ??= []).Add(imageInfo);
 2465            }
 2466
 1292467            var anyImagesRemoved = deletedImages?.Count > 0;
 1292468            if (anyImagesRemoved)
 2469            {
 22470                RemoveImages(deletedImages);
 2471            }
 2472
 1292473            return anyImagesRemoved;
 2474        }
 2475
 2476        /// <summary>
 2477        /// Gets the image path.
 2478        /// </summary>
 2479        /// <param name="imageType">Type of the image.</param>
 2480        /// <param name="imageIndex">Index of the image.</param>
 2481        /// <returns>System.String.</returns>
 2482        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2483        public string GetImagePath(ImageType imageType, int imageIndex)
 22484            => GetImageInfo(imageType, imageIndex)?.Path;
 2485
 2486        /// <summary>
 2487        /// Gets the image information.
 2488        /// </summary>
 2489        /// <param name="imageType">Type of the image.</param>
 2490        /// <param name="imageIndex">Index of the image.</param>
 2491        /// <returns>ItemImageInfo.</returns>
 2492        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2493        {
 3842494            if (imageType == ImageType.Chapter)
 2495            {
 02496                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2497
 02498                if (chapter is null)
 2499                {
 02500                    return null;
 2501                }
 2502
 02503                var path = chapter.ImagePath;
 2504
 02505                if (string.IsNullOrEmpty(path))
 2506                {
 02507                    return null;
 2508                }
 2509
 02510                return new ItemImageInfo
 02511                {
 02512                    Path = path,
 02513                    DateModified = chapter.ImageDateModified,
 02514                    Type = imageType
 02515                };
 2516            }
 2517
 3842518            return GetImages(imageType)
 3842519                .ElementAtOrDefault(imageIndex);
 2520        }
 2521
 2522        /// <summary>
 2523        /// Computes image index for given image or raises if no matching image found.
 2524        /// </summary>
 2525        /// <param name="image">Image to compute index for.</param>
 2526        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2527        /// </exception>
 2528        /// <returns>Image index.</returns>
 2529        public int GetImageIndex(ItemImageInfo image)
 2530        {
 02531            ArgumentNullException.ThrowIfNull(image);
 2532
 02533            if (image.Type == ImageType.Chapter)
 2534            {
 02535                var chapters = ChapterManager.GetChapters(Id);
 02536                for (var i = 0; i < chapters.Count; i++)
 2537                {
 02538                    if (chapters[i].ImagePath == image.Path)
 2539                    {
 02540                        return i;
 2541                    }
 2542                }
 2543
 02544                throw new ArgumentException("No chapter index found for image path", image.Path);
 2545            }
 2546
 02547            var images = GetImages(image.Type).ToArray();
 02548            for (var i = 0; i < images.Length; i++)
 2549            {
 02550                if (images[i].Path == image.Path)
 2551                {
 02552                    return i;
 2553                }
 2554            }
 2555
 02556            throw new ArgumentException("No image index found for image path", image.Path);
 2557        }
 2558
 2559        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2560        {
 5042561            if (imageType == ImageType.Chapter)
 2562            {
 02563                throw new ArgumentException("No image info for chapter images");
 2564            }
 2565
 2566            // Yield return is more performant than LINQ Where on an Array
 15822567            for (var i = 0; i < ImageInfos.Length; i++)
 2568            {
 3132569                var imageInfo = ImageInfos[i];
 3132570                if (imageInfo.Type == imageType)
 2571                {
 1672572                    yield return imageInfo;
 2573                }
 2574            }
 4782575        }
 2576
 2577        /// <summary>
 2578        /// Adds the images, updating metadata if they already are part of this item.
 2579        /// </summary>
 2580        /// <param name="imageType">Type of the image.</param>
 2581        /// <param name="images">The images.</param>
 2582        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2583        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2584        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2585        {
 52586            if (imageType == ImageType.Chapter)
 2587            {
 02588                throw new ArgumentException("Cannot call AddImages with chapter images");
 2589            }
 2590
 52591            var existingImages = GetImages(imageType)
 52592                .ToList();
 2593
 52594            var newImageList = new List<FileSystemMetadata>();
 52595            var imageUpdated = false;
 2596
 302597            foreach (var newImage in images)
 2598            {
 102599                if (newImage is null)
 2600                {
 02601                    throw new ArgumentException("null image found in list");
 2602                }
 2603
 102604                var existing = existingImages
 102605                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2606
 102607                if (existing is null)
 2608                {
 42609                    newImageList.Add(newImage);
 2610                }
 2611                else
 2612                {
 62613                    if (existing.IsLocalFile)
 2614                    {
 62615                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2616
 2617                        // If date changed then we need to reset saved image dimensions. Stores such as PostgreSQL keep
 2618                        // only microseconds, so an exact comparison would treat every unchanged image as modified.
 62619                        if (Math.Abs((existing.DateModified - newDateModified).TotalSeconds) > 1 && (existing.Width > 0 
 2620                        {
 22621                            existing.Width = 0;
 22622                            existing.Height = 0;
 22623                            imageUpdated = true;
 2624                        }
 2625
 62626                        existing.DateModified = newDateModified;
 2627                    }
 2628                }
 2629            }
 2630
 52631            if (newImageList.Count > 0)
 2632            {
 22633                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2634            }
 2635
 52636            return imageUpdated || newImageList.Count > 0;
 2637        }
 2638
 2639        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2640        {
 532641            return new ItemImageInfo
 532642            {
 532643                Path = file.FullName,
 532644                Type = type,
 532645                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 532646            };
 2647        }
 2648
 2649        /// <summary>
 2650        /// Gets the file system path to delete when the item is to be deleted.
 2651        /// </summary>
 2652        /// <returns>The metadata for the deleted paths.</returns>
 2653        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2654        {
 02655            return new[]
 02656            {
 02657                FileSystem.GetFileSystemInfo(Path)
 02658            }.Concat(GetLocalMetadataFilesToDelete());
 2659        }
 2660
 2661        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2662        {
 02663            if (IsFolder || !IsInMixedFolder)
 2664            {
 02665                return [];
 2666            }
 2667
 02668            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2669
 02670            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02671                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02672                .ToList();
 2673        }
 2674
 2675        public bool AllowsMultipleImages(ImageType type)
 2676        {
 232677            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2678        }
 2679
 2680        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2681        {
 02682            if (!AllowsMultipleImages(type))
 2683            {
 02684                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2685            }
 2686
 02687            var info1 = GetImageInfo(type, index1);
 02688            var info2 = GetImageInfo(type, index2);
 2689
 02690            if (info1 is null || info2 is null)
 2691            {
 2692                // Nothing to do
 02693                return Task.CompletedTask;
 2694            }
 2695
 02696            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2697            {
 2698                // TODO: Not supported  yet
 02699                return Task.CompletedTask;
 2700            }
 2701
 02702            var path1 = info1.Path;
 02703            var path2 = info2.Path;
 2704
 02705            FileSystem.SwapFiles(path1, path2);
 2706
 2707            // Refresh these values
 02708            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02709            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2710
 02711            info1.Width = 0;
 02712            info1.Height = 0;
 02713            info2.Width = 0;
 02714            info2.Height = 0;
 2715
 02716            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2717        }
 2718
 2719        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2720        {
 02721            userItemData ??= UserDataManager.GetUserData(user, this);
 2722
 02723            return userItemData is not null && userItemData.Played;
 2724        }
 2725
 2726        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2727        {
 02728            userItemData ??= UserDataManager.GetUserData(user, this);
 2729
 02730            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2731        }
 2732
 2733        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2734        {
 02735            ArgumentNullException.ThrowIfNull(user);
 2736
 02737            userItemData ??= UserDataManager.GetUserData(user, this);
 2738
 02739            return userItemData is null || !userItemData.Played;
 2740        }
 2741
 2742        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2743        {
 492744            return GetItemLookupInfo<ItemLookupInfo>();
 2745        }
 2746
 2747        protected T GetItemLookupInfo<T>()
 2748            where T : ItemLookupInfo, new()
 2749        {
 492750            return new T
 492751            {
 492752                Path = Path,
 492753                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 492754                MetadataLanguage = GetPreferredMetadataLanguage(),
 492755                Name = GetNameForMetadataLookup(),
 492756                OriginalTitle = OriginalTitle,
 492757                ProviderIds = ProviderIds,
 492758                IndexNumber = IndexNumber,
 492759                ParentIndexNumber = ParentIndexNumber,
 492760                Year = ProductionYear,
 492761                PremiereDate = PremiereDate
 492762            };
 2763        }
 2764
 2765        protected virtual string GetNameForMetadataLookup()
 2766        {
 492767            return Name;
 2768        }
 2769
 2770        /// <summary>
 2771        /// This is called before any metadata refresh and returns true if changes were made.
 2772        /// </summary>
 2773        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2774        /// <returns>true if the item has change, else false.</returns>
 2775        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2776        {
 462777            _sortName = null;
 2778
 462779            var hasChanges = false;
 2780
 462781            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2782            {
 02783                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02784                hasChanges = true;
 2785            }
 2786
 462787            return hasChanges;
 2788        }
 2789
 2790        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2791        {
 02792            if (protocol == MediaProtocol.File)
 2793            {
 02794                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2795            }
 2796
 02797            return path;
 2798        }
 2799
 2800        public virtual void FillUserDataDtoValues(
 2801            UserItemDataDto dto,
 2802            UserItemData userData,
 2803            BaseItemDto itemDto,
 2804            User user,
 2805            DtoOptions fields,
 2806            (int Played, int Total)? precomputedCounts = null)
 2807        {
 02808            if (RunTimeTicks.HasValue)
 2809            {
 02810                double pct = RunTimeTicks.Value;
 2811
 02812                if (pct > 0)
 2813                {
 02814                    pct = userData.PlaybackPositionTicks / pct;
 2815
 02816                    if (pct > 0)
 2817                    {
 02818                        dto.PlayedPercentage = 100 * pct;
 2819                    }
 2820                }
 2821            }
 02822        }
 2823
 2824        /// <summary>
 2825        /// Applies the owner's premiere date and production year to an owned item, returning whether anything changed.
 2826        /// </summary>
 2827        /// <param name="owner">The owner.</param>
 2828        /// <param name="ownedItem">The owned item.</param>
 2829        /// <returns><c>true</c> if the owned item was changed, else <c>false</c>.</returns>
 2830        internal static bool InheritDatesFromOwner(BaseItem owner, BaseItem ownedItem)
 2831        {
 2832            // Extras have no release date of their own, so the owner's is authoritative.
 42833            var changed = false;
 2834
 42835            if (owner.ProductionYear is not null && ownedItem.ProductionYear != owner.ProductionYear)
 2836            {
 22837                ownedItem.ProductionYear = owner.ProductionYear;
 22838                changed = true;
 2839            }
 2840
 42841            if (owner.PremiereDate is not null && ownedItem.PremiereDate != owner.PremiereDate)
 2842            {
 22843                ownedItem.PremiereDate = owner.PremiereDate;
 22844                changed = true;
 2845            }
 2846
 42847            return changed;
 2848        }
 2849
 2850        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2851        {
 02852            var newOptions = new MetadataRefreshOptions(options)
 02853            {
 02854                SearchResult = null
 02855            };
 2856
 02857            var item = this;
 2858
 02859            if (copyTitleMetadata)
 2860            {
 2861                // Take some data from the main item, for querying purposes
 02862                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2863                {
 02864                    newOptions.ForceSave = true;
 02865                    ownedItem.Genres = item.Genres;
 2866                }
 2867
 02868                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2869                {
 02870                    newOptions.ForceSave = true;
 02871                    ownedItem.Studios = item.Studios;
 2872                }
 2873
 02874                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2875                {
 02876                    newOptions.ForceSave = true;
 02877                    ownedItem.ProductionLocations = item.ProductionLocations;
 2878                }
 2879
 02880                if (item.CommunityRating != ownedItem.CommunityRating)
 2881                {
 02882                    ownedItem.CommunityRating = item.CommunityRating;
 02883                    newOptions.ForceSave = true;
 2884                }
 2885
 02886                if (item.CriticRating != ownedItem.CriticRating)
 2887                {
 02888                    ownedItem.CriticRating = item.CriticRating;
 02889                    newOptions.ForceSave = true;
 2890                }
 2891
 02892                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2893                {
 02894                    ownedItem.Overview = item.Overview;
 02895                    newOptions.ForceSave = true;
 2896                }
 2897
 02898                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2899                {
 02900                    ownedItem.OfficialRating = item.OfficialRating;
 02901                    newOptions.ForceSave = true;
 2902                }
 2903
 02904                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2905                {
 02906                    ownedItem.CustomRating = item.CustomRating;
 02907                    newOptions.ForceSave = true;
 2908                }
 2909
 02910                if (InheritDatesFromOwner(item, ownedItem))
 2911                {
 02912                    newOptions.ForceSave = true;
 2913                }
 2914            }
 2915
 02916            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02917        }
 2918
 2919        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2920        {
 02921            var newOptions = new MetadataRefreshOptions(options)
 02922            {
 02923                SearchResult = null
 02924            };
 2925
 02926            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2927
 2928            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02929            if (LibraryManager.GetItemById(id) is not Video video)
 2930            {
 02931                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2932
 02933                newOptions.ForceSave = true;
 2934            }
 2935
 02936            if (video is null)
 2937            {
 02938                return;
 2939            }
 2940
 02941            if (video.OwnerId.IsEmpty())
 2942            {
 02943                video.OwnerId = Id;
 2944            }
 2945
 02946            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02947        }
 2948
 2949        public string GetEtag(User user)
 2950        {
 62951            var list = GetEtagValues(user);
 2952
 62953            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2954        }
 2955
 2956        protected virtual List<string> GetEtagValues(User user)
 2957        {
 62958            return
 62959            [
 62960                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62961            ];
 2962        }
 2963
 2964        public virtual IEnumerable<Guid> GetAncestorIds()
 2965        {
 1712966            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2967        }
 2968
 2969        public BaseItem GetTopParent()
 2970        {
 1712971            if (IsTopParent)
 2972            {
 272973                return this;
 2974            }
 2975
 1442976            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2977        }
 2978
 2979        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2980        {
 1032981            return [Id];
 2982        }
 2983
 2984        public virtual double? GetRefreshProgress()
 2985        {
 02986            return null;
 2987        }
 2988
 2989        public virtual ItemUpdateType OnMetadataChanged()
 2990        {
 3862991            var updateType = ItemUpdateType.None;
 2992
 3862993            var item = this;
 2994
 3862995            var rating = item.GetParentalRatingScore();
 3862996            if (rating is not null)
 2997            {
 22998                if (rating.Score != item.InheritedParentalRatingValue)
 2999                {
 23000                    item.InheritedParentalRatingValue = rating.Score;
 23001                    updateType |= ItemUpdateType.MetadataImport;
 3002                }
 3003
 23004                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 3005                {
 03006                    item.InheritedParentalRatingSubValue = rating.SubScore;
 03007                    updateType |= ItemUpdateType.MetadataImport;
 3008                }
 3009            }
 3010            else
 3011            {
 3843012                if (item.InheritedParentalRatingValue is not null)
 3013                {
 13014                    item.InheritedParentalRatingValue = null;
 13015                    item.InheritedParentalRatingSubValue = null;
 13016                    updateType |= ItemUpdateType.MetadataImport;
 3017                }
 3018            }
 3019
 3863020            return updateType;
 3021        }
 3022
 3023        /// <summary>
 3024        /// Updates the official rating based on content and returns true or false indicating if it changed.
 3025        /// </summary>
 3026        /// <param name="children">Media children.</param>
 3027        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 3028        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 3029        {
 03030            var currentOfficialRating = OfficialRating;
 3031
 3032            // Gather all possible ratings
 03033            var ratings = children
 03034                .Select(i => i.OfficialRating)
 03035                .Where(i => !string.IsNullOrEmpty(i))
 03036                .Distinct(StringComparer.OrdinalIgnoreCase)
 03037                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 03038                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 03039                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 03040                .Select(i => i.rating);
 3041
 03042            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 3043
 03044            return !string.Equals(
 03045                currentOfficialRating ?? string.Empty,
 03046                OfficialRating ?? string.Empty,
 03047                StringComparison.OrdinalIgnoreCase);
 3048        }
 3049
 3050        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 3051        {
 03052            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 3053        }
 3054
 3055        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 3056        {
 03057            return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), us
 3058        }
 3059
 3060        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 3061        {
 03062            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 3063        }
 3064
 3065        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 3066        {
 03067            return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), u
 3068        }
 3069
 3070        /// <summary>
 3071        /// Gets the ids of the items whose owned extras belong to this item.
 3072        /// </summary>
 3073        /// <returns>An array containing the owner ids.</returns>
 3074        protected virtual Guid[] GetExtraOwnerIds()
 3075        {
 63076            return [Id];
 3077        }
 3078
 3079        /// <summary>
 3080        /// Gets the ids of this item and the versions of it whose extras it maintains.
 3081        /// </summary>
 3082        /// <returns>An array containing the version ids.</returns>
 3083        protected virtual Guid[] GetOwnedVersionIds()
 3084        {
 03085            return [Id];
 3086        }
 3087
 3088        /// <summary>
 3089        /// Gets the id of the version an extra belongs to.
 3090        /// </summary>
 3091        /// <param name="extra">The extra.</param>
 3092        /// <returns>The id of the owning version.</returns>
 3093        protected virtual Guid GetOwnerIdForExtra(BaseItem extra)
 3094        {
 03095            return Id;
 3096        }
 3097
 3098        /// <summary>
 3099        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 3100        /// </summary>
 3101        /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</
 3102        /// <returns>An enumerable containing the items.</returns>
 3103        public IEnumerable<BaseItem> GetExtras(User user = null)
 3104        {
 63105            return LibraryManager.GetItemList(new InternalItemsQuery(user)
 63106            {
 63107                OwnerIds = GetExtraOwnerIds(),
 63108                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 63109            });
 3110        }
 3111
 3112        /// <summary>
 3113        /// Get all extras with specific types that are associated with this item.
 3114        /// </summary>
 3115        /// <param name="extraTypes">The types of extras to retrieve.</param>
 3116        /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</
 3117        /// <returns>An enumerable containing the extras.</returns>
 3118        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes, User user = null)
 3119        {
 03120            return LibraryManager.GetItemList(new InternalItemsQuery(user)
 03121            {
 03122                OwnerIds = GetExtraOwnerIds(),
 03123                ExtraTypes = extraTypes.ToArray(),
 03124                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 03125            });
 3126        }
 3127
 3128        public virtual long GetRunTimeTicksForPlayState()
 3129        {
 03130            return RunTimeTicks ?? 0;
 3131        }
 3132
 3133        /// <inheritdoc />
 3134        public override bool Equals(object obj)
 3135        {
 1383136            return obj is BaseItem baseItem && this.Equals(baseItem);
 3137        }
 3138
 3139        /// <inheritdoc />
 3453140        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 3141
 3142        /// <inheritdoc />
 1413143        public override int GetHashCode() => HashCode.Combine(Id);
 3144    }
 3145}

Methods/Properties

.cctor()
.ctor()
get_LUFS()
set_LUFS(System.Nullable`1<System.Single>)
get_NormalizationGain()
set_NormalizationGain(System.Nullable`1<System.Single>)
get_SupportsAddingToPlaylist()
get_AlwaysScanInternalMetadataPath()
get_SupportsPlayedStatus()
get_SupportsPositionTicksResume()
get_SupportsRemoteImageDownloading()
get_Name()
set_Name(System.String)
get_IsUnaired()
get_IsThemeMedia()
get_OriginalLanguage()
set_OriginalLanguage(System.String)
get_DisplayPreferencesId()
get_SourceType()
get_ContainingFolderPath()
get_IsHidden()
get_LocationType()
get_PathProtocol()
get_IsFileProtocol()
get_HasPathProtocol()
get_SupportsLocalMetadata()
get_FileNameWithoutExtension()
get_EnableAlphaNumericSorting()
get_IsHD()
get_PrimaryImagePath()
get_MediaType()
get_PhysicalLocations()
get_EnableMediaSourceDisplay()
get_ForcedSortName()
set_ForcedSortName(System.String)
get_SortName()
set_SortName(System.String)
get_DisplayParentId()
get_DisplayParent()
get_CriticRating()
set_CriticRating(System.Nullable`1<System.Single>)
get_CommunityRating()
set_CommunityRating(System.Nullable`1<System.Single>)
get_HasLocalAlternateVersions()
get_OfficialRatingForComparison()
get_CustomRatingForComparison()
get_LatestItemsIndexContainer()
get_EnableRememberingTrackSelections()
get_IsTopParent()
get_SupportsAncestors()
get_SupportsOwnedItems()
get_SearchesContainingFolderForExtras()
get_SupportsPeople()
get_SupportsThemeMedia()
get_SupportsInheritedParentImages()
get_IsFolder()
get_IsDisplayedAsFolder()
GetCustomRatingForComparision(System.Collections.Generic.HashSet`1<System.Guid>)
GetDefaultPrimaryImageAspectRatio()
CreatePresentationUniqueKey()
CanDelete()
IsAuthorizedToDelete(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>)
GetOwner()
CanDelete(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>)
CanDelete(Jellyfin.Database.Implementations.Entities.User)
CanDownload()
IsAuthorizedToDownload(Jellyfin.Database.Implementations.Entities.User)
CanDownload(Jellyfin.Database.Implementations.Entities.User)
ToString()
GetInternalMetadataPath()
GetInternalMetadataPath(System.String)
CreateSortName()
GetItemByNameFolderName(System.String)
GetSortName(System.String,System.Boolean,MediaBrowser.Model.Configuration.ServerConfiguration)
ApplySortCharacterRules(System.String,MediaBrowser.Model.Configuration.ServerConfiguration)
NormalizeSortNameFilter(System.String)
ModifySortChunks(System.ReadOnlySpan`1<System.Char>)
GetParent()
GetParents()
FindParent()
GetPlayAccess(Jellyfin.Database.Implementations.Entities.User)
GetMediaStreams()
IsActiveRecording()
GetMediaSources(System.Boolean)
GetAllItemsForMediaSources()
GetVersionInfo(System.Boolean,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceType,System.String)
GetMediaSourceName(MediaBrowser.Controller.Entities.BaseItem,System.String)
FiniteOrNull(System.Nullable`1<System.Single>)
GetCommonNamePrefix(System.Collections.Generic.IReadOnlyList`1<System.ValueTuple`2<MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceType>>)
GetCommonVersionPrefix(System.Collections.Generic.IReadOnlyList`1<System.String>)
IsDecimalPoint(System.String,System.Int32,System.Collections.Generic.IReadOnlyList`1<System.String>)
RefreshMetadata(System.Threading.CancellationToken)
RefreshMetadata()
IsVisibleStandaloneInternal(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
SetParent(MediaBrowser.Controller.Entities.Folder)
RefreshedOwnedItems()
GetFileSystemChildren(MediaBrowser.Controller.Providers.IDirectoryService)
RefreshExtras()
GetPresentationUniqueKey()
RequiresRefresh()
GetUserDataKeys()
UpdateFromResolvedItem(MediaBrowser.Controller.Entities.BaseItem)
AfterMetadataRefresh()
GetPreferredMetadataLanguage()
GetPreferredMetadataCountryCode()
GetInheritedOriginalLanguage()
IsSaveLocalMetadataEnabled()
IsParentalAllowed(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
GetParentalRatingScore()
GetInheritedTags()
IsVisibleViaTags(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
GetBlockUnratedType()
GetBlockUnratedValue(Jellyfin.Database.Implementations.Entities.User)
IsVisible(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
IsVisibleStandalone(Jellyfin.Database.Implementations.Entities.User)
GetClientTypeName()
GetBaseItemKind()
GetLinkedChild(MediaBrowser.Controller.Entities.LinkedChild)
FindLinkedChild(MediaBrowser.Controller.Entities.LinkedChild)
AddStudio(System.String)
SetStudios(System.Collections.Generic.IEnumerable`1<System.String>)
AddGenre(System.String)
MarkPlayed(Jellyfin.Database.Implementations.Entities.User,System.Nullable`1<System.DateTime>,System.Boolean)
MarkUnplayed(Jellyfin.Database.Implementations.Entities.User)
ResetPlayedState(MediaBrowser.Controller.Entities.UserItemData)
ChangedExternally()
HasImage(MediaBrowser.Model.Entities.ImageType,System.Int32)
SetImage(MediaBrowser.Controller.Entities.ItemImageInfo,System.Int32)
SetImagePath(MediaBrowser.Model.Entities.ImageType,System.Int32,MediaBrowser.Model.IO.FileSystemMetadata)
DeleteImageAsync()
RemoveImage(MediaBrowser.Controller.Entities.ItemImageInfo)
RemoveImages(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.ItemImageInfo>)
AddImage(MediaBrowser.Controller.Entities.ItemImageInfo)
UpdateToRepositoryAsync()
ReattachUserDataAsync()
ValidateImages()
GetImagePath(MediaBrowser.Model.Entities.ImageType,System.Int32)
GetImageInfo(MediaBrowser.Model.Entities.ImageType,System.Int32)
GetImageIndex(MediaBrowser.Controller.Entities.ItemImageInfo)
GetImages()
AddImages(MediaBrowser.Model.Entities.ImageType,System.Collections.Generic.List`1<MediaBrowser.Model.IO.FileSystemMetadata>)
GetImageInfo(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Model.Entities.ImageType)
GetDeletePaths()
GetLocalMetadataFilesToDelete()
AllowsMultipleImages(MediaBrowser.Model.Entities.ImageType)
SwapImagesAsync(MediaBrowser.Model.Entities.ImageType,System.Int32,System.Int32)
IsPlayed(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
IsFavoriteOrLiked(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
IsUnplayed(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
MediaBrowser.Controller.Providers.IHasLookupInfo<MediaBrowser.Controller.Providers.ItemLookupInfo>.GetLookupInfo()
GetItemLookupInfo()
GetNameForMetadataLookup()
BeforeMetadataRefresh(System.Boolean)
GetMappedPath(MediaBrowser.Controller.Entities.BaseItem,System.String,System.Nullable`1<MediaBrowser.Model.MediaInfo.MediaProtocol>)
FillUserDataDtoValues(MediaBrowser.Model.Dto.UserItemDataDto,MediaBrowser.Controller.Entities.UserItemData,MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions,System.Nullable`1<System.ValueTuple`2<System.Int32,System.Int32>>)
InheritDatesFromOwner(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
RefreshMetadataForOwnedItem()
RefreshMetadataForOwnedVideo()
GetEtag(Jellyfin.Database.Implementations.Entities.User)
GetEtagValues(Jellyfin.Database.Implementations.Entities.User)
GetAncestorIds()
GetTopParent()
GetIdsForAncestorQuery()
GetRefreshProgress()
OnMetadataChanged()
UpdateRatingToItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
GetThemeSongs(Jellyfin.Database.Implementations.Entities.User)
GetThemeSongs(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Enums.SortOrder>>)
GetThemeVideos(Jellyfin.Database.Implementations.Entities.User)
GetThemeVideos(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Enums.SortOrder>>)
GetExtraOwnerIds()
GetOwnedVersionIds()
GetOwnerIdForExtra(MediaBrowser.Controller.Entities.BaseItem)
GetExtras(Jellyfin.Database.Implementations.Entities.User)
GetExtras(System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Model.Entities.ExtraType>,Jellyfin.Database.Implementations.Entities.User)
GetRunTimeTicksForPlayState()
Equals(System.Object)
Equals(MediaBrowser.Controller.Entities.BaseItem)
GetHashCode()