< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.BaseItem
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs
Line coverage
45%
Covered lines: 406
Uncovered lines: 484
Coverable lines: 890
Total lines: 2788
Line coverage: 45.6%
Branch coverage
32%
Covered branches: 182
Total branches: 558
Branch coverage: 32.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/14/2026 - 12:11:17 AM Line coverage: 51.1% (376/735) Branch coverage: 40.3% (188/466) Total lines: 27003/3/2026 - 12:13:24 AM Line coverage: 51.2% (375/731) Branch coverage: 40.2% (185/460) Total lines: 26884/7/2026 - 12:14:03 AM Line coverage: 51% (374/732) Branch coverage: 39.8% (185/464) Total lines: 26944/19/2026 - 12:14:27 AM Line coverage: 47.2% (404/855) Branch coverage: 38.6% (207/536) Total lines: 26945/4/2026 - 12:15:16 AM Line coverage: 45.4% (403/887) Branch coverage: 36.5% (202/552) Total lines: 27705/8/2026 - 12:15:13 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: 2788 2/14/2026 - 12:11:17 AM Line coverage: 51.1% (376/735) Branch coverage: 40.3% (188/466) Total lines: 27003/3/2026 - 12:13:24 AM Line coverage: 51.2% (375/731) Branch coverage: 40.2% (185/460) Total lines: 26884/7/2026 - 12:14:03 AM Line coverage: 51% (374/732) Branch coverage: 39.8% (185/464) Total lines: 26944/19/2026 - 12:14:27 AM Line coverage: 47.2% (404/855) Branch coverage: 38.6% (207/536) Total lines: 26945/4/2026 - 12:15:16 AM Line coverage: 45.4% (403/887) Branch coverage: 36.5% (202/552) Total lines: 27705/8/2026 - 12:15:13 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: 2788

Coverage delta

Coverage delta 5 -5

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()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%11650%
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()75%4480%
set_SortName(...)100%11100%
get_DisplayParentId()100%11100%
get_DisplayParent()100%22100%
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_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()50%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()71.42%171475%
ModifySortChunks(...)87.5%8893.75%
GetParent()100%22100%
GetParents()100%22100%
FindParent()0%2040%
GetPlayAccess(...)50%2266.66%
GetMediaStreams()100%210%
IsActiveRecording()100%210%
GetMediaSources(...)0%7280%
GetAllItemsForMediaSources()100%210%
GetVersionInfo(...)0%2970540%
GetMediaSourceName(...)46.87%743265.51%
RefreshMetadata(...)100%11100%
RefreshMetadata()62.5%8882.35%
IsVisibleStandaloneInternal(...)0%342180%
SetParent(...)50%22100%
RefreshedOwnedItems()62.5%201675%
GetFileSystemChildren(...)100%11100%
RefreshExtras()0%156120%
GetPresentationUniqueKey()0%620%
RequiresRefresh()33.33%11650%
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()50%4483.33%
GetInheritedTags()75%4485.71%
IsVisibleViaTags(...)9.09%3022216.66%
GetBlockUnratedType()50%2266.66%
GetBlockUnratedValue(...)50%5466.66%
IsVisible(...)100%11100%
IsVisibleStandalone(...)0%2040%
GetClientTypeName()70%141066.66%
GetBaseItemKind()100%22100%
GetLinkedChild(...)0%7280%
FindLinkedChild(...)0%210140%
AddStudio(...)50%4475%
SetStudios(...)100%210%
AddGenre(...)50%22100%
MarkPlayed(...)0%110100%
MarkUnplayed(...)100%210%
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%
RefreshMetadataForOwnedItem()0%342180%
RefreshMetadataForOwnedVideo()0%4260%
GetEtag(...)100%11100%
GetEtagValues(...)100%11100%
GetAncestorIds()100%11100%
GetTopParent()100%22100%
GetIdsForAncestorQuery()100%11100%
GetRefreshProgress()100%210%
OnMetadataChanged()25%22840%
UpdateRatingToItems(...)0%4260%
GetThemeSongs(...)100%210%
GetThemeSongs(...)100%210%
GetThemeVideos(...)100%210%
GetThemeVideos(...)100%210%
GetExtras()100%11100%
GetExtras(...)100%210%
GetRunTimeTicksForPlayState()100%210%
Equals(...)0%620%
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.IO;
 27using MediaBrowser.Controller.Library;
 28using MediaBrowser.Controller.MediaSegments;
 29using MediaBrowser.Controller.Persistence;
 30using MediaBrowser.Controller.Providers;
 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        /// <summary>
 52        /// The supported image extensions.
 53        /// </summary>
 454        public static readonly string[] SupportedImageExtensions
 455            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 56
 457        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 458        {
 459            ".nfo",
 460            ".xml",
 461            ".srt",
 462            ".vtt",
 463            ".sub",
 464            ".sup",
 465            ".idx",
 466            ".txt",
 467            ".edl",
 468            ".bif",
 469            ".smi",
 470            ".ttml",
 471            ".lrc",
 472            ".elrc"
 473        };
 74
 75        /// <summary>
 76        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 77        /// </summary>
 478        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 479        {
 480            Model.Entities.ExtraType.Unknown,
 481            Model.Entities.ExtraType.BehindTheScenes,
 482            Model.Entities.ExtraType.Clip,
 483            Model.Entities.ExtraType.DeletedScene,
 484            Model.Entities.ExtraType.Interview,
 485            Model.Entities.ExtraType.Sample,
 486            Model.Entities.ExtraType.Scene,
 487            Model.Entities.ExtraType.Featurette,
 488            Model.Entities.ExtraType.Short
 489        };
 90
 91        private string _sortName;
 92
 93        private string _forcedSortName;
 94
 95        private string _name;
 96
 97        private string _originalLanguage;
 98
 99        public const char SlugChar = '-';
 100
 101        protected BaseItem()
 102        {
 1124103            Tags = Array.Empty<string>();
 1124104            Genres = Array.Empty<string>();
 1124105            Studios = Array.Empty<string>();
 1124106            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 1124107            LockedFields = Array.Empty<MetadataField>();
 1124108            ImageInfos = Array.Empty<ItemImageInfo>();
 1124109            ProductionLocations = Array.Empty<string>();
 1124110            RemoteTrailers = Array.Empty<MediaUrl>();
 1124111            UserData = [];
 1124112        }
 113
 114        /// <summary>
 115        /// Gets or Sets the user data collection as cached from the last Db query.
 116        /// </summary>
 117        [JsonIgnore]
 118        public ICollection<UserData> UserData { get; set; }
 119
 120        [JsonIgnore]
 121        public string PreferredMetadataCountryCode { get; set; }
 122
 123        [JsonIgnore]
 124        public string PreferredMetadataLanguage { get; set; }
 125
 126        public long? Size { get; set; }
 127
 128        public string Container { get; set; }
 129
 130        [JsonIgnore]
 131        public string Tagline { get; set; }
 132
 133        [JsonIgnore]
 134        public virtual ItemImageInfo[] ImageInfos { get; set; }
 135
 136        [JsonIgnore]
 137        public bool IsVirtualItem { get; set; }
 138
 139        /// <summary>
 140        /// Gets or sets the album.
 141        /// </summary>
 142        /// <value>The album.</value>
 143        [JsonIgnore]
 144        public string Album { get; set; }
 145
 146        /// <summary>
 147        /// Gets or sets the LUFS value.
 148        /// </summary>
 149        /// <value>The LUFS Value.</value>
 150        [JsonIgnore]
 151        public float? LUFS { get; set; }
 152
 153        /// <summary>
 154        /// Gets or sets the gain required for audio normalization.
 155        /// </summary>
 156        /// <value>The gain required for audio normalization.</value>
 157        [JsonIgnore]
 158        public float? NormalizationGain { get; set; }
 159
 160        /// <summary>
 161        /// Gets or sets the channel identifier.
 162        /// </summary>
 163        /// <value>The channel identifier.</value>
 164        [JsonIgnore]
 165        public Guid ChannelId { get; set; }
 166
 167        [JsonIgnore]
 0168        public virtual bool SupportsAddingToPlaylist => false;
 169
 170        [JsonIgnore]
 15171        public virtual bool AlwaysScanInternalMetadataPath => false;
 172
 173        /// <summary>
 174        /// Gets or sets a value indicating whether this instance is in mixed folder.
 175        /// </summary>
 176        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 177        [JsonIgnore]
 178        public bool IsInMixedFolder { get; set; }
 179
 180        [JsonIgnore]
 0181        public virtual bool SupportsPlayedStatus => false;
 182
 183        [JsonIgnore]
 0184        public virtual bool SupportsPositionTicksResume => false;
 185
 186        [JsonIgnore]
 17187        public virtual bool SupportsRemoteImageDownloading => true;
 188
 189        /// <summary>
 190        /// Gets or sets the name.
 191        /// </summary>
 192        /// <value>The name.</value>
 193        [JsonIgnore]
 194        public virtual string Name
 195        {
 1010196            get => _name;
 197            set
 198            {
 397199                _name = value;
 200
 201                // lazy load this again
 397202                _sortName = null;
 397203            }
 204        }
 205
 206        [JsonIgnore]
 0207        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 208
 209        [JsonIgnore]
 210        public int? TotalBitrate { get; set; }
 211
 212        [JsonIgnore]
 213        public ExtraType? ExtraType { get; set; }
 214
 215        [JsonIgnore]
 0216        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 217
 218        [JsonIgnore]
 219        public string OriginalTitle { get; set; }
 220
 221        [JsonIgnore]
 222        public string OriginalLanguage
 223        {
 401224            get => _originalLanguage;
 230225            set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
 226        }
 227
 228        /// <summary>
 229        /// Gets or sets the id.
 230        /// </summary>
 231        /// <value>The id.</value>
 232        [JsonIgnore]
 233        public Guid Id { get; set; }
 234
 235        [JsonIgnore]
 236        public Guid OwnerId { get; set; }
 237
 238        /// <summary>
 239        /// Gets or sets the audio.
 240        /// </summary>
 241        /// <value>The audio.</value>
 242        [JsonIgnore]
 243        public ProgramAudio? Audio { get; set; }
 244
 245        /// <summary>
 246        /// Gets the id that should be used to key display prefs for this item.
 247        /// Default is based on the type for everything except actual generic folders.
 248        /// </summary>
 249        /// <value>The display prefs id.</value>
 250        [JsonIgnore]
 251        public virtual Guid DisplayPreferencesId
 252        {
 253            get
 254            {
 6255                var thisType = GetType();
 6256                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 257            }
 258        }
 259
 260        /// <summary>
 261        /// Gets or sets the path.
 262        /// </summary>
 263        /// <value>The path.</value>
 264        [JsonIgnore]
 265        public virtual string Path { get; set; }
 266
 267        [JsonIgnore]
 268        public virtual SourceType SourceType
 269        {
 270            get
 271            {
 2117272                if (!ChannelId.IsEmpty())
 273                {
 0274                    return SourceType.Channel;
 275                }
 276
 2117277                return SourceType.Library;
 278            }
 279        }
 280
 281        /// <summary>
 282        /// Gets the folder containing the item.
 283        /// If the item is a folder, it returns the folder itself.
 284        /// </summary>
 285        [JsonIgnore]
 286        public virtual string ContainingFolderPath
 287        {
 288            get
 289            {
 468290                if (IsFolder)
 291                {
 408292                    return Path;
 293                }
 294
 60295                return System.IO.Path.GetDirectoryName(Path);
 296            }
 297        }
 298
 299        /// <summary>
 300        /// Gets or sets the name of the service.
 301        /// </summary>
 302        /// <value>The name of the service.</value>
 303        [JsonIgnore]
 304        public string ServiceName { get; set; }
 305
 306        /// <summary>
 307        /// Gets or sets the external id.
 308        /// </summary>
 309        /// <remarks>
 310        /// If this content came from an external service, the id of the content on that service.
 311        /// </remarks>
 312        [JsonIgnore]
 313        public string ExternalId { get; set; }
 314
 315        [JsonIgnore]
 316        public string ExternalSeriesId { get; set; }
 317
 318        [JsonIgnore]
 0319        public virtual bool IsHidden => false;
 320
 321        /// <summary>
 322        /// Gets the type of the location.
 323        /// </summary>
 324        /// <value>The type of the location.</value>
 325        [JsonIgnore]
 326        public virtual LocationType LocationType
 327        {
 328            get
 329            {
 9330                var path = Path;
 9331                if (string.IsNullOrEmpty(path))
 332                {
 0333                    if (SourceType == SourceType.Channel)
 334                    {
 0335                        return LocationType.Remote;
 336                    }
 337
 0338                    return LocationType.Virtual;
 339                }
 340
 9341                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 342            }
 343        }
 344
 345        [JsonIgnore]
 346        public MediaProtocol? PathProtocol
 347        {
 348            get
 349            {
 2507350                var path = Path;
 351
 2507352                if (string.IsNullOrEmpty(path))
 353                {
 116354                    return null;
 355                }
 356
 2391357                return MediaSourceManager.GetPathProtocol(path);
 358            }
 359        }
 360
 361        [JsonIgnore]
 2486362        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 363
 364        [JsonIgnore]
 0365        public bool HasPathProtocol => PathProtocol.HasValue;
 366
 367        [JsonIgnore]
 368        public virtual bool SupportsLocalMetadata
 369        {
 370            get
 371            {
 1316372                if (SourceType == SourceType.Channel)
 373                {
 0374                    return false;
 375                }
 376
 1316377                return IsFileProtocol;
 378            }
 379        }
 380
 381        [JsonIgnore]
 382        public virtual string FileNameWithoutExtension
 383        {
 384            get
 385            {
 0386                if (IsFileProtocol)
 387                {
 0388                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 389                }
 390
 0391                return null;
 392            }
 393        }
 394
 395        [JsonIgnore]
 101396        public virtual bool EnableAlphaNumericSorting => true;
 397
 141398        public virtual bool IsHD => Height >= 720;
 399
 400        public bool IsShortcut { get; set; }
 401
 402        public string ShortcutPath { get; set; }
 403
 404        public int Width { get; set; }
 405
 406        public int Height { get; set; }
 407
 408        /// <summary>
 409        /// Gets the primary image path.
 410        /// </summary>
 411        /// <remarks>
 412        /// This is just a helper for convenience.
 413        /// </remarks>
 414        /// <value>The primary image path.</value>
 415        [JsonIgnore]
 0416        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 417
 418        /// <summary>
 419        /// Gets or sets the date created.
 420        /// </summary>
 421        /// <value>The date created.</value>
 422        [JsonIgnore]
 423        public DateTime DateCreated { get; set; }
 424
 425        /// <summary>
 426        /// Gets or sets the date modified.
 427        /// </summary>
 428        /// <value>The date modified.</value>
 429        [JsonIgnore]
 430        public DateTime DateModified { get; set; }
 431
 432        public DateTime DateLastSaved { get; set; }
 433
 434        [JsonIgnore]
 435        public DateTime DateLastRefreshed { get; set; }
 436
 437        [JsonIgnore]
 438        public bool IsLocked { get; set; }
 439
 440        /// <summary>
 441        /// Gets or sets the locked fields.
 442        /// </summary>
 443        /// <value>The locked fields.</value>
 444        [JsonIgnore]
 445        public MetadataField[] LockedFields { get; set; }
 446
 447        /// <summary>
 448        /// Gets the type of the media.
 449        /// </summary>
 450        /// <value>The type of the media.</value>
 451        [JsonIgnore]
 132452        public virtual MediaType MediaType => MediaType.Unknown;
 453
 454        [JsonIgnore]
 455        public virtual string[] PhysicalLocations
 456        {
 457            get
 458            {
 0459                if (!IsFileProtocol)
 460                {
 0461                    return Array.Empty<string>();
 462                }
 463
 0464                return [Path];
 465            }
 466        }
 467
 468        [JsonIgnore]
 469        public bool EnableMediaSourceDisplay
 470        {
 471            get
 472            {
 6473                if (SourceType == SourceType.Channel)
 474                {
 0475                    return ChannelManager.EnableMediaSourceDisplay(this);
 476                }
 477
 6478                return true;
 479            }
 480        }
 481
 482        [JsonIgnore]
 483        public Guid ParentId { get; set; }
 484
 485        /// <summary>
 486        /// Gets or sets the logger.
 487        /// </summary>
 488        public static ILogger<BaseItem> Logger { get; set; }
 489
 490        public static ILibraryManager LibraryManager { get; set; }
 491
 492        public static IServerConfigurationManager ConfigurationManager { get; set; }
 493
 494        public static IProviderManager ProviderManager { get; set; }
 495
 496        public static ILocalizationManager LocalizationManager { get; set; }
 497
 498        public static IItemRepository ItemRepository { get; set; }
 499
 500        public static IItemCountService ItemCountService { get; set; }
 501
 502        public static IChapterManager ChapterManager { get; set; }
 503
 504        public static IFileSystem FileSystem { get; set; }
 505
 506        public static IUserDataManager UserDataManager { get; set; }
 507
 508        public static IChannelManager ChannelManager { get; set; }
 509
 510        public static IMediaSourceManager MediaSourceManager { get; set; }
 511
 512        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 513
 514        /// <summary>
 515        /// Gets or sets the name of the forced sort.
 516        /// </summary>
 517        /// <value>The name of the forced sort.</value>
 518        [JsonIgnore]
 519        public string ForcedSortName
 520        {
 481521            get => _forcedSortName;
 522            set
 523            {
 90524                _forcedSortName = value;
 90525                _sortName = null;
 90526            }
 527        }
 528
 529        /// <summary>
 530        /// Gets or sets the name of the sort.
 531        /// </summary>
 532        /// <value>The name of the sort.</value>
 533        [JsonIgnore]
 534        public string SortName
 535        {
 536            get
 537            {
 172538                if (_sortName is null)
 539                {
 101540                    if (!string.IsNullOrEmpty(ForcedSortName))
 541                    {
 542                        // Need the ToLower because that's what CreateSortName does
 0543                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 544                    }
 545                    else
 546                    {
 101547                        _sortName = CreateSortName();
 548                    }
 549                }
 550
 172551                return _sortName;
 552            }
 553
 129554            set => _sortName = value;
 555        }
 556
 557        [JsonIgnore]
 342558        public virtual Guid DisplayParentId => ParentId;
 559
 560        [JsonIgnore]
 561        public BaseItem DisplayParent
 562        {
 563            get
 564            {
 336565                var id = DisplayParentId;
 336566                if (id.IsEmpty())
 567                {
 256568                    return null;
 569                }
 570
 80571                return LibraryManager.GetItemById(id);
 572            }
 573        }
 574
 575        /// <summary>
 576        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 577        /// </summary>
 578        /// <value>The premiere date.</value>
 579        [JsonIgnore]
 580        public DateTime? PremiereDate { get; set; }
 581
 582        /// <summary>
 583        /// Gets or sets the end date.
 584        /// </summary>
 585        /// <value>The end date.</value>
 586        [JsonIgnore]
 587        public DateTime? EndDate { get; set; }
 588
 589        /// <summary>
 590        /// Gets or sets the official rating.
 591        /// </summary>
 592        /// <value>The official rating.</value>
 593        [JsonIgnore]
 594        public string OfficialRating { get; set; }
 595
 596        [JsonIgnore]
 597        public int? InheritedParentalRatingValue { get; set; }
 598
 599        [JsonIgnore]
 600        public int? InheritedParentalRatingSubValue { get; set; }
 601
 602        /// <summary>
 603        /// Gets or sets the critic rating.
 604        /// </summary>
 605        /// <value>The critic rating.</value>
 606        [JsonIgnore]
 607        public float? CriticRating { get; set; }
 608
 609        /// <summary>
 610        /// Gets or sets the custom rating.
 611        /// </summary>
 612        /// <value>The custom rating.</value>
 613        [JsonIgnore]
 614        public string CustomRating { get; set; }
 615
 616        /// <summary>
 617        /// Gets or sets the overview.
 618        /// </summary>
 619        /// <value>The overview.</value>
 620        [JsonIgnore]
 621        public string Overview { get; set; }
 622
 623        /// <summary>
 624        /// Gets or sets the studios.
 625        /// </summary>
 626        /// <value>The studios.</value>
 627        [JsonIgnore]
 628        public string[] Studios { get; set; }
 629
 630        /// <summary>
 631        /// Gets or sets the genres.
 632        /// </summary>
 633        /// <value>The genres.</value>
 634        [JsonIgnore]
 635        public string[] Genres { get; set; }
 636
 637        /// <summary>
 638        /// Gets or sets the tags.
 639        /// </summary>
 640        /// <value>The tags.</value>
 641        [JsonIgnore]
 642        public string[] Tags { get; set; }
 643
 644        [JsonIgnore]
 645        public string[] ProductionLocations { get; set; }
 646
 647        /// <summary>
 648        /// Gets or sets the home page URL.
 649        /// </summary>
 650        /// <value>The home page URL.</value>
 651        [JsonIgnore]
 652        public string HomePageUrl { get; set; }
 653
 654        /// <summary>
 655        /// Gets or sets the community rating.
 656        /// </summary>
 657        /// <value>The community rating.</value>
 658        [JsonIgnore]
 659        public float? CommunityRating { get; set; }
 660
 661        /// <summary>
 662        /// Gets or sets the run time ticks.
 663        /// </summary>
 664        /// <value>The run time ticks.</value>
 665        [JsonIgnore]
 666        public long? RunTimeTicks { get; set; }
 667
 668        /// <summary>
 669        /// Gets or sets the production year.
 670        /// </summary>
 671        /// <value>The production year.</value>
 672        [JsonIgnore]
 673        public int? ProductionYear { get; set; }
 674
 675        /// <summary>
 676        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 677        /// This could be episode number, album track number, etc.
 678        /// </summary>
 679        /// <value>The index number.</value>
 680        [JsonIgnore]
 681        public int? IndexNumber { get; set; }
 682
 683        /// <summary>
 684        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 685        /// </summary>
 686        /// <value>The parent index number.</value>
 687        [JsonIgnore]
 688        public int? ParentIndexNumber { get; set; }
 689
 690        [JsonIgnore]
 0691        public virtual bool HasLocalAlternateVersions => false;
 692
 693        [JsonIgnore]
 694        public string OfficialRatingForComparison
 695        {
 696            get
 697            {
 168698                var officialRating = OfficialRating;
 168699                if (!string.IsNullOrEmpty(officialRating))
 700                {
 0701                    return officialRating;
 702                }
 703
 168704                var parent = DisplayParent;
 168705                if (parent is not null)
 706                {
 40707                    return parent.OfficialRatingForComparison;
 708                }
 709
 128710                return null;
 711            }
 712        }
 713
 714        [JsonIgnore]
 715        public string CustomRatingForComparison
 716        {
 717            get
 718            {
 128719                return GetCustomRatingForComparision();
 720            }
 721        }
 722
 723        /// <summary>
 724        /// Gets or sets the provider ids.
 725        /// </summary>
 726        /// <value>The provider ids.</value>
 727        [JsonIgnore]
 728        public Dictionary<string, string> ProviderIds { get; set; }
 729
 730        [JsonIgnore]
 0731        public virtual Folder LatestItemsIndexContainer => null;
 732
 733        [JsonIgnore]
 734        public string PresentationUniqueKey { get; set; }
 735
 736        [JsonIgnore]
 22737        public virtual bool EnableRememberingTrackSelections => true;
 738
 739        [JsonIgnore]
 740        public virtual bool IsTopParent
 741        {
 742            get
 743            {
 356744                if (this is BasePluginFolder || this is Channel)
 745                {
 60746                    return true;
 747                }
 748
 296749                if (this is IHasCollectionType view)
 750                {
 14751                    if (view.CollectionType == CollectionType.livetv)
 752                    {
 0753                        return true;
 754                    }
 755                }
 756
 296757                if (GetParent() is AggregateFolder)
 758                {
 0759                    return true;
 760                }
 761
 296762                return false;
 763            }
 764        }
 765
 766        [JsonIgnore]
 452767        public virtual bool SupportsAncestors => true;
 768
 769        [JsonIgnore]
 96770        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 771
 772        [JsonIgnore]
 70773        public virtual bool SupportsPeople => false;
 774
 775        [JsonIgnore]
 0776        public virtual bool SupportsThemeMedia => false;
 777
 778        [JsonIgnore]
 0779        public virtual bool SupportsInheritedParentImages => false;
 780
 781        /// <summary>
 782        /// Gets a value indicating whether this instance is folder.
 783        /// </summary>
 784        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 785        [JsonIgnore]
 80786        public virtual bool IsFolder => false;
 787
 788        [JsonIgnore]
 0789        public virtual bool IsDisplayedAsFolder => false;
 790
 791        /// <summary>
 792        /// Gets or sets the remote trailers.
 793        /// </summary>
 794        /// <value>The remote trailers.</value>
 795        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 796
 797        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 798        {
 168799            callstack ??= new();
 168800            var customRating = CustomRating;
 168801            if (!string.IsNullOrEmpty(customRating))
 802            {
 0803                return customRating;
 804            }
 805
 168806            callstack.Add(Id);
 807
 168808            var parent = DisplayParent;
 168809            if (parent is not null && !callstack.Contains(parent.Id))
 810            {
 40811                return parent.GetCustomRatingForComparision(callstack);
 812            }
 813
 128814            return null;
 815        }
 816
 817        public virtual double GetDefaultPrimaryImageAspectRatio()
 818        {
 0819            return 0;
 820        }
 821
 822        public virtual string CreatePresentationUniqueKey()
 823        {
 59824            return Id.ToString("N", CultureInfo.InvariantCulture);
 825        }
 826
 827        public virtual bool CanDelete()
 828        {
 0829            if (SourceType == SourceType.Channel)
 830            {
 0831                return ChannelManager.CanDelete(this);
 832            }
 833
 0834            return IsFileProtocol;
 835        }
 836
 837        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 838        {
 0839            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 840            {
 0841                return true;
 842            }
 843
 0844            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 845
 0846            if (SourceType == SourceType.Channel)
 847            {
 0848                return allowed.Contains(ChannelId);
 849            }
 850
 0851            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 852
 0853            foreach (var folder in collectionFolders)
 854            {
 0855                if (allowed.Contains(folder.Id))
 856                {
 0857                    return true;
 858                }
 859            }
 860
 0861            return false;
 0862        }
 863
 864        public BaseItem GetOwner()
 865        {
 644866            var ownerId = OwnerId;
 644867            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 868        }
 869
 870        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 871        {
 6872            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 873        }
 874
 875        public virtual bool CanDelete(User user)
 876        {
 6877            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 878
 6879            return CanDelete(user, allCollectionFolders);
 880        }
 881
 882        public virtual bool CanDownload()
 883        {
 6884            return false;
 885        }
 886
 887        public virtual bool IsAuthorizedToDownload(User user)
 888        {
 0889            return user.HasPermission(PermissionKind.EnableContentDownloading);
 890        }
 891
 892        public bool CanDownload(User user)
 893        {
 6894            return CanDownload() && IsAuthorizedToDownload(user);
 895        }
 896
 897        /// <inheritdoc />
 898        public override string ToString()
 899        {
 69900            return Name;
 901        }
 902
 903        public virtual string GetInternalMetadataPath()
 904        {
 72905            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 906
 72907            return GetInternalMetadataPath(basePath);
 908        }
 909
 910        protected virtual string GetInternalMetadataPath(string basePath)
 911        {
 72912            if (SourceType == SourceType.Channel)
 913            {
 0914                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 915            }
 916
 72917            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 918
 72919            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 920        }
 921
 922        /// <summary>
 923        /// Creates the name of the sort.
 924        /// </summary>
 925        /// <returns>System.String.</returns>
 926        protected virtual string CreateSortName()
 927        {
 101928            if (Name is null)
 929            {
 0930                return null; // some items may not have name filled in properly
 931            }
 932
 101933            if (!EnableAlphaNumericSorting)
 934            {
 0935                return Name.TrimStart();
 936            }
 937
 101938            var sortable = Name.Trim().ToLowerInvariant();
 939
 808940            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 941            {
 942                // Remove from beginning if a space follows
 303943                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 944                {
 0945                    sortable = sortable.Remove(0, search.Length + 1);
 946                }
 947
 948                // Remove from middle if surrounded by spaces
 303949                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 950
 951                // Remove from end if preceeded by a space
 303952                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 953                {
 0954                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 955                }
 956            }
 957
 1414958            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 959            {
 606960                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 961            }
 962
 808963            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 964            {
 303965                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 966            }
 967
 101968            return ModifySortChunks(sortable);
 969        }
 970
 971        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 972        {
 973            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 974            {
 975                if (isDigitChunk && chunk.Length < 10)
 976                {
 977                    builder.Append('0', 10 - chunk.Length);
 978                }
 979
 980                builder.Append(chunk);
 981            }
 982
 107983            if (name.IsEmpty)
 984            {
 1985                return string.Empty;
 986            }
 987
 106988            var builder = new StringBuilder(name.Length);
 989
 106990            int chunkStart = 0;
 106991            bool isDigitChunk = char.IsDigit(name[0]);
 1702992            for (int i = 0; i < name.Length; i++)
 993            {
 745994                var isDigit = char.IsDigit(name[i]);
 745995                if (isDigit != isDigitChunk)
 996                {
 5997                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5998                    chunkStart = i;
 5999                    isDigitChunk = isDigit;
 1000                }
 1001            }
 1002
 1061003            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 1004
 1005            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 1061006            var result = builder.ToString().RemoveDiacritics();
 1061007            if (!result.All(char.IsAscii))
 1008            {
 01009                result = result.Transliterated();
 1010            }
 1011
 1061012            return result;
 1013        }
 1014
 1015        public BaseItem GetParent()
 1016        {
 19421017            var parentId = ParentId;
 19421018            if (parentId.IsEmpty())
 1019            {
 17051020                return null;
 1021            }
 1022
 2371023            return LibraryManager.GetItemById(parentId);
 1024        }
 1025
 1026        public IEnumerable<BaseItem> GetParents()
 1027        {
 6951028            var parent = GetParent();
 1029
 7671030            while (parent is not null)
 1031            {
 721032                yield return parent;
 1033
 721034                parent = parent.GetParent();
 1035            }
 6951036        }
 1037
 1038        /// <summary>
 1039        /// Finds a parent of a given type.
 1040        /// </summary>
 1041        /// <typeparam name="T">Type of parent.</typeparam>
 1042        /// <returns>``0.</returns>
 1043        public T FindParent<T>()
 1044            where T : Folder
 1045        {
 01046            foreach (var parent in GetParents())
 1047            {
 01048                if (parent is T item)
 1049                {
 01050                    return item;
 1051                }
 1052            }
 1053
 01054            return null;
 01055        }
 1056
 1057        /// <summary>
 1058        /// Gets the play access.
 1059        /// </summary>
 1060        /// <param name="user">The user.</param>
 1061        /// <returns>PlayAccess.</returns>
 1062        public PlayAccess GetPlayAccess(User user)
 1063        {
 61064            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1065            {
 01066                return PlayAccess.None;
 1067            }
 1068
 1069            // if (!user.IsParentalScheduleAllowed())
 1070            // {
 1071            //    return PlayAccess.None;
 1072            // }
 1073
 61074            return PlayAccess.Full;
 1075        }
 1076
 1077        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1078        {
 01079            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01080            {
 01081                ItemId = Id
 01082            });
 1083        }
 1084
 1085        protected virtual bool IsActiveRecording()
 1086        {
 01087            return false;
 1088        }
 1089
 1090        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1091        {
 01092            if (SourceType == SourceType.Channel)
 1093            {
 01094                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01095                           .ToList();
 1096
 01097                if (sources.Count > 0)
 1098                {
 01099                    return sources;
 1100                }
 1101            }
 1102
 01103            var list = GetAllItemsForMediaSources();
 01104            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
 1105
 01106            if (IsActiveRecording())
 1107            {
 01108                foreach (var mediaSource in result)
 1109                {
 01110                    mediaSource.Type = MediaSourceType.Placeholder;
 1111                }
 1112            }
 1113
 01114            return result.OrderBy(i =>
 01115            {
 01116                if (i.VideoType == VideoType.VideoFile)
 01117                {
 01118                    return 0;
 01119                }
 01120
 01121                return 1;
 01122            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 01123            .ThenByDescending(i => i, new MediaSourceWidthComparator())
 01124            .ToArray();
 1125        }
 1126
 1127        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1128        {
 01129            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1130        }
 1131
 1132        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
 1133        {
 01134            ArgumentNullException.ThrowIfNull(item);
 1135
 01136            var protocol = item.PathProtocol;
 1137
 1138            // Resolve the item path so everywhere we use the media source it will always point to
 1139            // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
 1140            // path will return null, so it's safe to check for all paths.
 01141            var itemPath = item.Path;
 01142            if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) 
 1143            {
 01144                itemPath = linkInfo.FullName;
 1145            }
 1146
 01147            var info = new MediaSourceInfo
 01148            {
 01149                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 01150                Protocol = protocol ?? MediaProtocol.File,
 01151                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 01152                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 01153                Name = GetMediaSourceName(item),
 01154                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 01155                RunTimeTicks = item.RunTimeTicks,
 01156                Container = item.Container,
 01157                Size = item.Size,
 01158                Type = type,
 01159                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 01160                    && (protocol is null or MediaProtocol.File)
 01161                    && MediaSegmentManager.HasSegments(item.Id)
 01162            };
 1163
 01164            if (string.IsNullOrEmpty(info.Path))
 1165            {
 01166                info.Type = MediaSourceType.Placeholder;
 1167            }
 1168
 01169            if (info.Protocol == MediaProtocol.File)
 1170            {
 01171                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1172            }
 1173
 01174            var video = item as Video;
 01175            if (video is not null)
 1176            {
 01177                info.IsoType = video.IsoType;
 01178                info.VideoType = video.VideoType;
 01179                info.Video3DFormat = video.Video3DFormat;
 01180                info.Timestamp = video.Timestamp;
 1181
 01182                if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
 1183                {
 01184                    var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
 1185
 1186                    // Only allow remote shortcut paths — local file paths in .strm files
 1187                    // could be used to read arbitrary files from the server.
 01188                    if (shortcutProtocol != MediaProtocol.File)
 1189                    {
 01190                        info.IsRemote = true;
 01191                        info.Path = video.ShortcutPath;
 01192                        info.Protocol = shortcutProtocol;
 1193                    }
 1194                }
 1195
 01196                if (string.IsNullOrEmpty(info.Container))
 1197                {
 01198                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1199                    {
 01200                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1201                        {
 01202                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1203                        }
 1204                    }
 1205                }
 1206            }
 1207
 01208            if (string.IsNullOrEmpty(info.Container))
 1209            {
 01210                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1211                {
 01212                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1213                }
 1214            }
 1215
 01216            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1217            {
 01218                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1219            }
 1220
 01221            if (video is not null && video.VideoType != VideoType.VideoFile)
 1222            {
 01223                info.SupportsDirectStream = false;
 1224            }
 1225
 01226            info.Bitrate = item.TotalBitrate;
 01227            info.InferTotalBitrate();
 1228
 01229            return info;
 1230        }
 1231
 1232        internal string GetMediaSourceName(BaseItem item)
 1233        {
 41234            var terms = new List<string>();
 1235
 41236            var path = item.Path;
 41237            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1238            {
 41239                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 41240                if (HasLocalAlternateVersions)
 1241                {
 41242                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 41243                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1244                    {
 21245                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
 21246                        if (!name.IsWhiteSpace())
 1247                        {
 21248                            terms.Add(name.ToString());
 1249                        }
 1250                    }
 1251                }
 1252
 41253                if (terms.Count == 0)
 1254                {
 21255                    terms.Add(displayName);
 1256                }
 1257            }
 1258
 41259            if (terms.Count == 0)
 1260            {
 01261                terms.Add(item.Name);
 1262            }
 1263
 41264            if (item is Video video)
 1265            {
 41266                if (video.Video3DFormat.HasValue)
 1267                {
 01268                    terms.Add("3D");
 1269                }
 1270
 41271                if (video.VideoType == VideoType.BluRay)
 1272                {
 01273                    terms.Add("Bluray");
 1274                }
 41275                else if (video.VideoType == VideoType.Dvd)
 1276                {
 01277                    terms.Add("DVD");
 1278                }
 41279                else if (video.VideoType == VideoType.Iso)
 1280                {
 01281                    if (video.IsoType.HasValue)
 1282                    {
 01283                        if (video.IsoType.Value == IsoType.BluRay)
 1284                        {
 01285                            terms.Add("Bluray");
 1286                        }
 01287                        else if (video.IsoType.Value == IsoType.Dvd)
 1288                        {
 01289                            terms.Add("DVD");
 1290                        }
 1291                    }
 1292                    else
 1293                    {
 01294                        terms.Add("ISO");
 1295                    }
 1296                }
 1297            }
 1298
 41299            return string.Join('/', terms);
 1300        }
 1301
 1302        public Task RefreshMetadata(CancellationToken cancellationToken)
 1303        {
 521304            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1305        }
 1306
 1307        /// <summary>
 1308        /// The base implementation to refresh metadata.
 1309        /// </summary>
 1310        /// <param name="options">The options.</param>
 1311        /// <param name="cancellationToken">The cancellation token.</param>
 1312        /// <returns>true if a provider reports we changed.</returns>
 1313        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1314        {
 591315            var requiresSave = false;
 1316
 591317            if (SupportsOwnedItems)
 1318            {
 1319                try
 1320                {
 371321                    if (IsFileProtocol)
 1322                    {
 371323                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1324                    }
 1325
 371326                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 371327                }
 01328                catch (Exception ex)
 1329                {
 01330                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01331                }
 1332            }
 1333
 591334            var refreshOptions = requiresSave
 591335                ? new MetadataRefreshOptions(options)
 591336                {
 591337                    ForceSave = true
 591338                }
 591339                : options;
 1340
 591341            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 581342        }
 1343
 1344        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1345        {
 01346            if (!IsVisible(user))
 1347            {
 01348                return false;
 1349            }
 1350
 01351            var parents = GetParents().ToList();
 01352            if (parents.Any(i => !i.IsVisible(user, true)))
 1353            {
 01354                return false;
 1355            }
 1356
 01357            if (checkFolders)
 1358            {
 01359                var topParent = parents.Count > 0 ? parents[^1] : this;
 1360
 01361                if (string.IsNullOrEmpty(topParent.Path))
 1362                {
 01363                    return true;
 1364                }
 1365
 01366                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1367
 01368                if (itemCollectionFolders.Count > 0)
 1369                {
 01370                    var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 1371                    IEnumerable<Guid> userCollectionFolderIds;
 01372                    if (blockedMediaFolders.Length > 0)
 1373                    {
 1374                        // User has blocked folders - get all library folders and exclude blocked ones
 01375                        userCollectionFolderIds = LibraryManager.GetUserRootFolder().Children
 01376                            .Select(i => i.Id)
 01377                            .Where(id => !blockedMediaFolders.Contains(id));
 1378                    }
 01379                    else if (user.HasPermission(PermissionKind.EnableAllFolders))
 1380                    {
 1381                        // User can access all folders - no need to filter
 01382                        return true;
 1383                    }
 1384                    else
 1385                    {
 1386                        // User has specific enabled folders
 01387                        userCollectionFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders);
 1388                    }
 1389
 01390                    if (!itemCollectionFolders.Any(userCollectionFolderIds.Contains))
 1391                    {
 01392                        return false;
 1393                    }
 1394                }
 1395            }
 1396
 01397            return true;
 1398        }
 1399
 1400        public void SetParent(Folder parent)
 1401        {
 101402            ParentId = parent is null ? Guid.Empty : parent.Id;
 101403        }
 1404
 1405        /// <summary>
 1406        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1407        /// Returns true or false indicating if changes were found.
 1408        /// </summary>
 1409        /// <param name="options">The metadata refresh options.</param>
 1410        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1411        /// <param name="cancellationToken">The cancellation token.</param>
 1412        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1413        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1414        {
 371415            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1416            {
 371417                return false;
 1418            }
 1419
 01420            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 371421        }
 1422
 1423        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1424        {
 431425            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1426        }
 1427
 1428        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1429        {
 01430            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 01431            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1432
 01433            var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
 01434            {
 01435                OwnerIds = [item.Id]
 01436            }).Select(e => e.Id).ToArray();
 1437
 01438            var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
 1439
 01440            if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.Full
 1441            {
 01442                return false;
 1443            }
 1444
 01445            var ownerId = item.Id;
 1446
 01447            var tasks = extras.Select(i =>
 01448            {
 01449                var subOptions = new MetadataRefreshOptions(options);
 01450                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
 01451                {
 01452                    subOptions.ForceSave = true;
 01453                }
 01454
 01455                i.OwnerId = ownerId;
 01456                i.ParentId = Guid.Empty;
 01457                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 01458            });
 1459
 01460            var removedExtraIds = currentExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 01461            if (removedExtraIds.Length > 0)
 1462            {
 01463                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 01464                {
 01465                    ItemIds = removedExtraIds
 01466                });
 01467                foreach (var removedExtra in removedExtras)
 1468                {
 1469                    // Only delete items that are actual extras (have ExtraType set)
 1470                    // Items with OwnerId but no ExtraType might be alternate versions, not extras
 01471                    if (removedExtra.ExtraType.HasValue)
 1472                    {
 01473                        LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 01474                        {
 01475                            DeleteFileLocation = false
 01476                        });
 1477                    }
 1478                }
 1479            }
 1480
 01481            await Task.WhenAll(tasks).ConfigureAwait(false);
 1482
 01483            return true;
 01484        }
 1485
 1486        public string GetPresentationUniqueKey()
 1487        {
 01488            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1489        }
 1490
 1491        public virtual bool RequiresRefresh()
 1492        {
 591493            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1494            {
 591495                return false;
 1496            }
 1497
 01498            var info = FileSystem.GetFileSystemInfo(Path);
 1499
 01500            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1501        }
 1502
 1503        public virtual List<string> GetUserDataKeys()
 1504        {
 1531505            var list = new List<string>();
 1506
 1531507            if (SourceType == SourceType.Channel)
 1508            {
 01509                if (!string.IsNullOrEmpty(ExternalId))
 1510                {
 01511                    list.Add(ExternalId);
 1512                }
 1513            }
 1514
 1531515            list.Add(Id.ToString());
 1531516            return list;
 1517        }
 1518
 1519        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1520        {
 61521            var updateType = ItemUpdateType.None;
 1522
 61523            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1524            {
 01525                IsInMixedFolder = newItem.IsInMixedFolder;
 01526                updateType |= ItemUpdateType.MetadataImport;
 1527            }
 1528
 61529            return updateType;
 1530        }
 1531
 1532        public void AfterMetadataRefresh()
 1533        {
 581534            _sortName = null;
 581535        }
 1536
 1537        /// <summary>
 1538        /// Gets the preferred metadata language.
 1539        /// </summary>
 1540        /// <returns>System.String.</returns>
 1541        public string GetPreferredMetadataLanguage()
 1542        {
 351543            string lang = PreferredMetadataLanguage;
 1544
 351545            if (string.IsNullOrEmpty(lang))
 1546            {
 351547                lang = GetParents()
 351548                    .Select(i => i.PreferredMetadataLanguage)
 351549                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1550            }
 1551
 351552            if (string.IsNullOrEmpty(lang))
 1553            {
 351554                lang = LibraryManager.GetCollectionFolders(this)
 351555                    .Select(i => i.PreferredMetadataLanguage)
 351556                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1557            }
 1558
 351559            if (string.IsNullOrEmpty(lang))
 1560            {
 351561                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1562            }
 1563
 351564            if (string.IsNullOrEmpty(lang))
 1565            {
 351566                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1567            }
 1568
 351569            return lang;
 1570        }
 1571
 1572        /// <summary>
 1573        /// Gets the preferred metadata country code.
 1574        /// </summary>
 1575        /// <returns>System.String.</returns>
 1576        public string GetPreferredMetadataCountryCode()
 1577        {
 351578            string lang = PreferredMetadataCountryCode;
 1579
 351580            if (string.IsNullOrEmpty(lang))
 1581            {
 351582                lang = GetParents()
 351583                    .Select(i => i.PreferredMetadataCountryCode)
 351584                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1585            }
 1586
 351587            if (string.IsNullOrEmpty(lang))
 1588            {
 351589                lang = LibraryManager.GetCollectionFolders(this)
 351590                    .Select(i => i.PreferredMetadataCountryCode)
 351591                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1592            }
 1593
 351594            if (string.IsNullOrEmpty(lang))
 1595            {
 351596                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1597            }
 1598
 351599            if (string.IsNullOrEmpty(lang))
 1600            {
 351601                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1602            }
 1603
 351604            return lang;
 1605        }
 1606
 1607        /// <summary>
 1608        /// Gets the original language of the item, inheriting from parent items if necessary.
 1609        /// </summary>
 1610        /// <returns>System.String.</returns>
 1611        public virtual string GetInheritedOriginalLanguage()
 1612        {
 01613            return OriginalLanguage;
 1614        }
 1615
 1616        public virtual bool IsSaveLocalMetadataEnabled()
 1617        {
 881618            if (SourceType == SourceType.Channel)
 1619            {
 01620                return false;
 1621            }
 1622
 881623            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1624
 881625            return libraryOptions.SaveLocalMetadata;
 1626        }
 1627
 1628        /// <summary>
 1629        /// Determines if a given user has access to this item.
 1630        /// </summary>
 1631        /// <param name="user">The user.</param>
 1632        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1633        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1634        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1635        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1636        {
 101637            ArgumentNullException.ThrowIfNull(user);
 1638
 101639            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1640            {
 01641                return false;
 1642            }
 1643
 101644            var maxAllowedRating = user.MaxParentalRatingScore;
 101645            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 101646            var rating = CustomRatingForComparison;
 1647
 101648            if (string.IsNullOrEmpty(rating))
 1649            {
 101650                rating = OfficialRatingForComparison;
 1651            }
 1652
 101653            if (string.IsNullOrEmpty(rating))
 1654            {
 101655                return !GetBlockUnratedValue(user);
 1656            }
 1657
 01658            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1659
 1660            // Could not determine rating level
 01661            if (ratingScore is null)
 1662            {
 01663                var isAllowed = !GetBlockUnratedValue(user);
 1664
 01665                if (!isAllowed)
 1666                {
 01667                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1668                }
 1669
 01670                return isAllowed;
 1671            }
 1672
 01673            if (!maxAllowedRating.HasValue)
 1674            {
 01675                return true;
 1676            }
 1677
 01678            if (ratingScore.Score != maxAllowedRating.Value)
 1679            {
 01680                return ratingScore.Score < maxAllowedRating.Value;
 1681            }
 1682
 01683            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1684        }
 1685
 1686        public ParentalRatingScore GetParentalRatingScore()
 1687        {
 1181688            var rating = CustomRatingForComparison;
 1689
 1181690            if (string.IsNullOrEmpty(rating))
 1691            {
 1181692                rating = OfficialRatingForComparison;
 1693            }
 1694
 1181695            if (string.IsNullOrEmpty(rating))
 1696            {
 1181697                return null;
 1698            }
 1699
 01700            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1701        }
 1702
 1703        public List<string> GetInheritedTags()
 1704        {
 1131705            var list = new List<string>();
 1131706            list.AddRange(Tags);
 1707
 2801708            foreach (var parent in GetParents())
 1709            {
 271710                list.AddRange(parent.Tags);
 1711            }
 1712
 2261713            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1714            {
 01715                list.AddRange(folder.Tags);
 1716            }
 1717
 1131718            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1719        }
 1720
 1721        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1722        {
 101723            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 101724            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 1725
 101726            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 1727            {
 101728                return true;
 1729            }
 1730
 1731            // Normalize tags using the same logic as database queries
 01732            var normalizedBlockedTags = blockedTags
 01733                .Where(t => !string.IsNullOrWhiteSpace(t))
 01734                .Select(t => t.GetCleanValue())
 01735                .ToHashSet(StringComparer.Ordinal);
 1736
 01737            var normalizedItemTags = GetInheritedTags()
 01738                .Select(t => t.GetCleanValue())
 01739                .ToHashSet(StringComparer.Ordinal);
 1740
 1741            // Check blocked tags - item is hidden if it has any blocked tag
 01742            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 1743            {
 01744                return false;
 1745            }
 1746
 01747            var parent = GetParents().FirstOrDefault() ?? this;
 01748            if (parent is UserRootFolder or AggregateFolder or UserView)
 1749            {
 01750                return true;
 1751            }
 1752
 1753            // Check allowed tags - item must have at least one allowed tag
 01754            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 1755            {
 01756                var normalizedAllowedTags = allowedTags
 01757                    .Where(t => !string.IsNullOrWhiteSpace(t))
 01758                    .Select(t => t.GetCleanValue())
 01759                    .ToHashSet(StringComparer.Ordinal);
 1760
 01761                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 1762                {
 01763                    return false;
 1764                }
 1765            }
 1766
 01767            return true;
 1768        }
 1769
 1770        public virtual UnratedItem GetBlockUnratedType()
 1771        {
 1131772            if (SourceType == SourceType.Channel)
 1773            {
 01774                return UnratedItem.ChannelContent;
 1775            }
 1776
 1131777            return UnratedItem.Other;
 1778        }
 1779
 1780        /// <summary>
 1781        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1782        /// </summary>
 1783        /// <param name="user">The configuration.</param>
 1784        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1785        protected virtual bool GetBlockUnratedValue(User user)
 1786        {
 1787            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1788            // Special folders like series and albums will override this method.
 101789            if (IsFolder || this is IItemByName)
 1790            {
 101791                return false;
 1792            }
 1793
 01794            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1795        }
 1796
 1797        /// <summary>
 1798        /// Determines if this folder should be visible to a given user.
 1799        /// Default is just parental allowed. Can be overridden for more functionality.
 1800        /// </summary>
 1801        /// <param name="user">The user.</param>
 1802        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1803        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1804        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1805        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1806        {
 101807            ArgumentNullException.ThrowIfNull(user);
 1808
 101809            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1810        }
 1811
 1812        public virtual bool IsVisibleStandalone(User user)
 1813        {
 01814            if (SourceType == SourceType.Channel)
 1815            {
 01816                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1817            }
 1818
 01819            return IsVisibleStandaloneInternal(user, true);
 1820        }
 1821
 1822        public virtual string GetClientTypeName()
 1823        {
 1111824            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1825            {
 01826                return "ChannelFolderItem";
 1827            }
 1828
 1111829            return GetType().Name;
 1830        }
 1831
 1832        public BaseItemKind GetBaseItemKind()
 1833        {
 2441834            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1835        }
 1836
 1837        /// <summary>
 1838        /// Gets the linked child.
 1839        /// </summary>
 1840        /// <param name="info">The info.</param>
 1841        /// <returns>BaseItem.</returns>
 1842        protected BaseItem GetLinkedChild(LinkedChild info)
 1843        {
 1844            // First get using the cached Id
 01845            if (info.ItemId.HasValue)
 1846            {
 01847                if (info.ItemId.Value.IsEmpty())
 1848                {
 01849                    return null;
 1850                }
 1851
 01852                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1853
 01854                if (itemById is not null)
 1855                {
 01856                    return itemById;
 1857                }
 1858            }
 1859
 01860            var item = FindLinkedChild(info);
 1861
 1862            // If still null, log
 01863            if (item is null)
 1864            {
 1865                // Don't keep searching over and over
 01866                info.ItemId = Guid.Empty;
 1867            }
 1868            else
 1869            {
 1870                // Cache the id for next time
 01871                info.ItemId = item.Id;
 1872            }
 1873
 01874            return item;
 1875        }
 1876
 1877#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 1878        private BaseItem FindLinkedChild(LinkedChild info)
 1879        {
 1880            // First try to find by ItemId (new preferred method)
 01881            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 1882            {
 01883                var item = LibraryManager.GetItemById(info.ItemId.Value);
 01884                if (item is not null)
 1885                {
 01886                    return item;
 1887                }
 1888
 01889                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 1890            }
 1891
 1892            // Fall back to Path (legacy method)
 01893            var path = info.Path;
 01894            if (!string.IsNullOrEmpty(path))
 1895            {
 01896                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 1897
 01898                var itemByPath = LibraryManager.FindByPath(path, null);
 1899
 01900                if (itemByPath is null)
 1901                {
 01902                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1903                }
 1904
 01905                return itemByPath;
 1906            }
 1907
 1908            // Fall back to LibraryItemId (legacy method)
 01909            if (!string.IsNullOrEmpty(info.LibraryItemId))
 1910            {
 01911                var item = LibraryManager.GetItemById(info.LibraryItemId);
 1912
 01913                if (item is null)
 1914                {
 01915                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 1916                }
 1917
 01918                return item;
 1919            }
 1920
 01921            return null;
 1922        }
 1923#pragma warning restore CS0618
 1924
 1925        /// <summary>
 1926        /// Adds a studio to the item.
 1927        /// </summary>
 1928        /// <param name="name">The name.</param>
 1929        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1930        public void AddStudio(string name)
 1931        {
 41932            ArgumentException.ThrowIfNullOrEmpty(name);
 41933            var current = Studios;
 1934
 41935            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 1936            {
 41937                int curLen = current.Length;
 41938                if (curLen == 0)
 1939                {
 41940                    Studios = [name];
 1941                }
 1942                else
 1943                {
 01944                    Studios = [.. current, name];
 1945                }
 1946            }
 01947        }
 1948
 1949        public void SetStudios(IEnumerable<string> names)
 1950        {
 01951            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 01952        }
 1953
 1954        /// <summary>
 1955        /// Adds a genre to the item.
 1956        /// </summary>
 1957        /// <param name="name">The name.</param>
 1958        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1959        public void AddGenre(string name)
 1960        {
 131961            ArgumentException.ThrowIfNullOrEmpty(name);
 1962
 131963            var genres = Genres;
 131964            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 1965            {
 131966                Genres = [.. genres, name];
 1967            }
 131968        }
 1969
 1970        /// <summary>
 1971        /// Marks the played.
 1972        /// </summary>
 1973        /// <param name="user">The user.</param>
 1974        /// <param name="datePlayed">The date played.</param>
 1975        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1976        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1977        public virtual void MarkPlayed(
 1978            User user,
 1979            DateTime? datePlayed,
 1980            bool resetPosition)
 1981        {
 01982            ArgumentNullException.ThrowIfNull(user);
 1983
 01984            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 01985            {
 01986                Key = GetUserDataKeys().First(),
 01987            };
 1988
 01989            if (datePlayed.HasValue)
 1990            {
 1991                // Increment
 01992                data.PlayCount++;
 1993            }
 1994
 1995            // Ensure it's at least one
 01996            data.PlayCount = Math.Max(data.PlayCount, 1);
 1997
 01998            if (resetPosition)
 1999            {
 02000                data.PlaybackPositionTicks = 0;
 2001            }
 2002
 02003            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 02004            data.Played = true;
 2005
 02006            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02007        }
 2008
 2009        /// <summary>
 2010        /// Marks the unplayed.
 2011        /// </summary>
 2012        /// <param name="user">The user.</param>
 2013        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2014        public virtual void MarkUnplayed(User user)
 2015        {
 02016            ArgumentNullException.ThrowIfNull(user);
 2017
 02018            var data = UserDataManager.GetUserData(user, this);
 2019
 2020            // I think it is okay to do this here.
 2021            // if this is only called when a user is manually forcing something to un-played
 2022            // then it probably is what we want to do...
 02023            data.PlayCount = 0;
 02024            data.PlaybackPositionTicks = 0;
 02025            data.LastPlayedDate = null;
 02026            data.Played = false;
 2027
 02028            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02029        }
 2030
 2031        /// <summary>
 2032        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2033        /// </summary>
 2034        public virtual void ChangedExternally()
 2035        {
 02036            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02037        }
 2038
 2039        /// <summary>
 2040        /// Gets an image.
 2041        /// </summary>
 2042        /// <param name="type">The type.</param>
 2043        /// <param name="imageIndex">Index of the image.</param>
 2044        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2045        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2046        public bool HasImage(ImageType type, int imageIndex)
 2047        {
 1432048            return GetImageInfo(type, imageIndex) is not null;
 2049        }
 2050
 2051        public void SetImage(ItemImageInfo image, int index)
 2052        {
 132053            if (image.Type == ImageType.Chapter)
 2054            {
 02055                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2056            }
 2057
 132058            var existingImage = GetImageInfo(image.Type, index);
 2059
 132060            if (existingImage is null)
 2061            {
 112062                AddImage(image);
 2063            }
 2064            else
 2065            {
 22066                existingImage.Path = image.Path;
 22067                existingImage.DateModified = image.DateModified;
 22068                existingImage.Width = image.Width;
 22069                existingImage.Height = image.Height;
 22070                existingImage.BlurHash = image.BlurHash;
 2071            }
 22072        }
 2073
 2074        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2075        {
 462076            if (type == ImageType.Chapter)
 2077            {
 02078                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2079            }
 2080
 462081            var image = GetImageInfo(type, index);
 2082
 462083            if (image is null)
 2084            {
 452085                AddImage(GetImageInfo(file, type));
 2086            }
 2087            else
 2088            {
 12089                var imageInfo = GetImageInfo(file, type);
 2090
 12091                image.Path = file.FullName;
 12092                image.DateModified = imageInfo.DateModified;
 2093
 2094                // reset these values
 12095                image.Width = 0;
 12096                image.Height = 0;
 2097            }
 12098        }
 2099
 2100        /// <summary>
 2101        /// Deletes the image.
 2102        /// </summary>
 2103        /// <param name="type">The type.</param>
 2104        /// <param name="index">The index.</param>
 2105        /// <returns>A task.</returns>
 2106        public async Task DeleteImageAsync(ImageType type, int index)
 2107        {
 02108            var info = GetImageInfo(type, index);
 2109
 02110            if (info is null)
 2111            {
 2112                // Nothing to do
 02113                return;
 2114            }
 2115
 2116            // Remove from file system
 02117            var path = info.Path;
 02118            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2119            {
 02120                FileSystem.DeleteFile(path);
 2121            }
 2122
 2123            // Remove from item
 02124            RemoveImage(info);
 2125
 02126            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02127        }
 2128
 2129        public void RemoveImage(ItemImageInfo image)
 2130        {
 02131            RemoveImages([image]);
 02132        }
 2133
 2134        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2135        {
 92136            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92137        }
 2138
 2139        public void AddImage(ItemImageInfo image)
 2140        {
 562141            ImageInfos = [.. ImageInfos, image];
 562142        }
 2143
 2144        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1112145         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2146
 2147        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 342148            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2149
 2150        /// <summary>
 2151        /// Validates that images within the item are still on the filesystem.
 2152        /// </summary>
 2153        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2154        public bool ValidateImages()
 2155        {
 732156            List<ItemImageInfo> deletedImages = null;
 1762157            foreach (var imageInfo in ImageInfos)
 2158            {
 152159                if (!imageInfo.IsLocalFile)
 2160                {
 2161                    continue;
 2162                }
 2163
 152164                if (File.Exists(imageInfo.Path))
 2165                {
 2166                    continue;
 2167                }
 2168
 32169                (deletedImages ??= []).Add(imageInfo);
 2170            }
 2171
 732172            var anyImagesRemoved = deletedImages?.Count > 0;
 732173            if (anyImagesRemoved)
 2174            {
 22175                RemoveImages(deletedImages);
 2176            }
 2177
 732178            return anyImagesRemoved;
 2179        }
 2180
 2181        /// <summary>
 2182        /// Gets the image path.
 2183        /// </summary>
 2184        /// <param name="imageType">Type of the image.</param>
 2185        /// <param name="imageIndex">Index of the image.</param>
 2186        /// <returns>System.String.</returns>
 2187        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2188        public string GetImagePath(ImageType imageType, int imageIndex)
 22189            => GetImageInfo(imageType, imageIndex)?.Path;
 2190
 2191        /// <summary>
 2192        /// Gets the image information.
 2193        /// </summary>
 2194        /// <param name="imageType">Type of the image.</param>
 2195        /// <param name="imageIndex">Index of the image.</param>
 2196        /// <returns>ItemImageInfo.</returns>
 2197        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2198        {
 3002199            if (imageType == ImageType.Chapter)
 2200            {
 02201                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2202
 02203                if (chapter is null)
 2204                {
 02205                    return null;
 2206                }
 2207
 02208                var path = chapter.ImagePath;
 2209
 02210                if (string.IsNullOrEmpty(path))
 2211                {
 02212                    return null;
 2213                }
 2214
 02215                return new ItemImageInfo
 02216                {
 02217                    Path = path,
 02218                    DateModified = chapter.ImageDateModified,
 02219                    Type = imageType
 02220                };
 2221            }
 2222
 3002223            return GetImages(imageType)
 3002224                .ElementAtOrDefault(imageIndex);
 2225        }
 2226
 2227        /// <summary>
 2228        /// Computes image index for given image or raises if no matching image found.
 2229        /// </summary>
 2230        /// <param name="image">Image to compute index for.</param>
 2231        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2232        /// </exception>
 2233        /// <returns>Image index.</returns>
 2234        public int GetImageIndex(ItemImageInfo image)
 2235        {
 02236            ArgumentNullException.ThrowIfNull(image);
 2237
 02238            if (image.Type == ImageType.Chapter)
 2239            {
 02240                var chapters = ChapterManager.GetChapters(Id);
 02241                for (var i = 0; i < chapters.Count; i++)
 2242                {
 02243                    if (chapters[i].ImagePath == image.Path)
 2244                    {
 02245                        return i;
 2246                    }
 2247                }
 2248
 02249                throw new ArgumentException("No chapter index found for image path", image.Path);
 2250            }
 2251
 02252            var images = GetImages(image.Type).ToArray();
 02253            for (var i = 0; i < images.Length; i++)
 2254            {
 02255                if (images[i].Path == image.Path)
 2256                {
 02257                    return i;
 2258                }
 2259            }
 2260
 02261            throw new ArgumentException("No image index found for image path", image.Path);
 2262        }
 2263
 2264        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2265        {
 4032266            if (imageType == ImageType.Chapter)
 2267            {
 02268                throw new ArgumentException("No image info for chapter images");
 2269            }
 2270
 2271            // Yield return is more performant than LINQ Where on an Array
 13522272            for (var i = 0; i < ImageInfos.Length; i++)
 2273            {
 2912274                var imageInfo = ImageInfos[i];
 2912275                if (imageInfo.Type == imageType)
 2276                {
 1502277                    yield return imageInfo;
 2278                }
 2279            }
 3852280        }
 2281
 2282        /// <summary>
 2283        /// Adds the images, updating metadata if they already are part of this item.
 2284        /// </summary>
 2285        /// <param name="imageType">Type of the image.</param>
 2286        /// <param name="images">The images.</param>
 2287        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2288        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2289        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2290        {
 42291            if (imageType == ImageType.Chapter)
 2292            {
 02293                throw new ArgumentException("Cannot call AddImages with chapter images");
 2294            }
 2295
 42296            var existingImages = GetImages(imageType)
 42297                .ToList();
 2298
 42299            var newImageList = new List<FileSystemMetadata>();
 42300            var imageUpdated = false;
 2301
 242302            foreach (var newImage in images)
 2303            {
 82304                if (newImage is null)
 2305                {
 02306                    throw new ArgumentException("null image found in list");
 2307                }
 2308
 82309                var existing = existingImages
 82310                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2311
 82312                if (existing is null)
 2313                {
 42314                    newImageList.Add(newImage);
 2315                }
 2316                else
 2317                {
 42318                    if (existing.IsLocalFile)
 2319                    {
 42320                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2321
 2322                        // If date changed then we need to reset saved image dimensions
 42323                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2324                        {
 22325                            existing.Width = 0;
 22326                            existing.Height = 0;
 22327                            imageUpdated = true;
 2328                        }
 2329
 42330                        existing.DateModified = newDateModified;
 2331                    }
 2332                }
 2333            }
 2334
 42335            if (newImageList.Count > 0)
 2336            {
 22337                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2338            }
 2339
 42340            return imageUpdated || newImageList.Count > 0;
 2341        }
 2342
 2343        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2344        {
 502345            return new ItemImageInfo
 502346            {
 502347                Path = file.FullName,
 502348                Type = type,
 502349                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502350            };
 2351        }
 2352
 2353        /// <summary>
 2354        /// Gets the file system path to delete when the item is to be deleted.
 2355        /// </summary>
 2356        /// <returns>The metadata for the deleted paths.</returns>
 2357        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2358        {
 02359            return new[]
 02360            {
 02361                FileSystem.GetFileSystemInfo(Path)
 02362            }.Concat(GetLocalMetadataFilesToDelete());
 2363        }
 2364
 2365        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2366        {
 02367            if (IsFolder || !IsInMixedFolder)
 2368            {
 02369                return [];
 2370            }
 2371
 02372            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2373
 02374            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02375                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02376                .ToList();
 2377        }
 2378
 2379        public bool AllowsMultipleImages(ImageType type)
 2380        {
 182381            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2382        }
 2383
 2384        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2385        {
 02386            if (!AllowsMultipleImages(type))
 2387            {
 02388                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2389            }
 2390
 02391            var info1 = GetImageInfo(type, index1);
 02392            var info2 = GetImageInfo(type, index2);
 2393
 02394            if (info1 is null || info2 is null)
 2395            {
 2396                // Nothing to do
 02397                return Task.CompletedTask;
 2398            }
 2399
 02400            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2401            {
 2402                // TODO: Not supported  yet
 02403                return Task.CompletedTask;
 2404            }
 2405
 02406            var path1 = info1.Path;
 02407            var path2 = info2.Path;
 2408
 02409            FileSystem.SwapFiles(path1, path2);
 2410
 2411            // Refresh these values
 02412            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02413            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2414
 02415            info1.Width = 0;
 02416            info1.Height = 0;
 02417            info2.Width = 0;
 02418            info2.Height = 0;
 2419
 02420            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2421        }
 2422
 2423        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2424        {
 02425            userItemData ??= UserDataManager.GetUserData(user, this);
 2426
 02427            return userItemData is not null && userItemData.Played;
 2428        }
 2429
 2430        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2431        {
 02432            userItemData ??= UserDataManager.GetUserData(user, this);
 2433
 02434            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2435        }
 2436
 2437        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2438        {
 02439            ArgumentNullException.ThrowIfNull(user);
 2440
 02441            userItemData ??= UserDataManager.GetUserData(user, this);
 2442
 02443            return userItemData is null || !userItemData.Played;
 2444        }
 2445
 2446        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2447        {
 352448            return GetItemLookupInfo<ItemLookupInfo>();
 2449        }
 2450
 2451        protected T GetItemLookupInfo<T>()
 2452            where T : ItemLookupInfo, new()
 2453        {
 352454            return new T
 352455            {
 352456                Path = Path,
 352457                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 352458                MetadataLanguage = GetPreferredMetadataLanguage(),
 352459                Name = GetNameForMetadataLookup(),
 352460                OriginalTitle = OriginalTitle,
 352461                ProviderIds = ProviderIds,
 352462                IndexNumber = IndexNumber,
 352463                ParentIndexNumber = ParentIndexNumber,
 352464                Year = ProductionYear,
 352465                PremiereDate = PremiereDate
 352466            };
 2467        }
 2468
 2469        protected virtual string GetNameForMetadataLookup()
 2470        {
 352471            return Name;
 2472        }
 2473
 2474        /// <summary>
 2475        /// This is called before any metadata refresh and returns true if changes were made.
 2476        /// </summary>
 2477        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2478        /// <returns>true if the item has change, else false.</returns>
 2479        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2480        {
 352481            _sortName = null;
 2482
 352483            var hasChanges = false;
 2484
 352485            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2486            {
 02487                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02488                hasChanges = true;
 2489            }
 2490
 352491            return hasChanges;
 2492        }
 2493
 2494        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2495        {
 02496            if (protocol == MediaProtocol.File)
 2497            {
 02498                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2499            }
 2500
 02501            return path;
 2502        }
 2503
 2504        public virtual void FillUserDataDtoValues(
 2505            UserItemDataDto dto,
 2506            UserItemData userData,
 2507            BaseItemDto itemDto,
 2508            User user,
 2509            DtoOptions fields,
 2510            (int Played, int Total)? precomputedCounts = null)
 2511        {
 02512            if (RunTimeTicks.HasValue)
 2513            {
 02514                double pct = RunTimeTicks.Value;
 2515
 02516                if (pct > 0)
 2517                {
 02518                    pct = userData.PlaybackPositionTicks / pct;
 2519
 02520                    if (pct > 0)
 2521                    {
 02522                        dto.PlayedPercentage = 100 * pct;
 2523                    }
 2524                }
 2525            }
 02526        }
 2527
 2528        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2529        {
 02530            var newOptions = new MetadataRefreshOptions(options)
 02531            {
 02532                SearchResult = null
 02533            };
 2534
 02535            var item = this;
 2536
 02537            if (copyTitleMetadata)
 2538            {
 2539                // Take some data from the main item, for querying purposes
 02540                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2541                {
 02542                    newOptions.ForceSave = true;
 02543                    ownedItem.Genres = item.Genres;
 2544                }
 2545
 02546                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2547                {
 02548                    newOptions.ForceSave = true;
 02549                    ownedItem.Studios = item.Studios;
 2550                }
 2551
 02552                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2553                {
 02554                    newOptions.ForceSave = true;
 02555                    ownedItem.ProductionLocations = item.ProductionLocations;
 2556                }
 2557
 02558                if (item.CommunityRating != ownedItem.CommunityRating)
 2559                {
 02560                    ownedItem.CommunityRating = item.CommunityRating;
 02561                    newOptions.ForceSave = true;
 2562                }
 2563
 02564                if (item.CriticRating != ownedItem.CriticRating)
 2565                {
 02566                    ownedItem.CriticRating = item.CriticRating;
 02567                    newOptions.ForceSave = true;
 2568                }
 2569
 02570                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2571                {
 02572                    ownedItem.Overview = item.Overview;
 02573                    newOptions.ForceSave = true;
 2574                }
 2575
 02576                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2577                {
 02578                    ownedItem.OfficialRating = item.OfficialRating;
 02579                    newOptions.ForceSave = true;
 2580                }
 2581
 02582                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2583                {
 02584                    ownedItem.CustomRating = item.CustomRating;
 02585                    newOptions.ForceSave = true;
 2586                }
 2587            }
 2588
 02589            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02590        }
 2591
 2592        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2593        {
 02594            var newOptions = new MetadataRefreshOptions(options)
 02595            {
 02596                SearchResult = null
 02597            };
 2598
 02599            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2600
 2601            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02602            if (LibraryManager.GetItemById(id) is not Video video)
 2603            {
 02604                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2605
 02606                newOptions.ForceSave = true;
 2607            }
 2608
 02609            if (video is null)
 2610            {
 02611                return;
 2612            }
 2613
 02614            if (video.OwnerId.IsEmpty())
 2615            {
 02616                video.OwnerId = Id;
 2617            }
 2618
 02619            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02620        }
 2621
 2622        public string GetEtag(User user)
 2623        {
 62624            var list = GetEtagValues(user);
 2625
 62626            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2627        }
 2628
 2629        protected virtual List<string> GetEtagValues(User user)
 2630        {
 62631            return
 62632            [
 62633                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62634            ];
 2635        }
 2636
 2637        public virtual IEnumerable<Guid> GetAncestorIds()
 2638        {
 1132639            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2640        }
 2641
 2642        public BaseItem GetTopParent()
 2643        {
 1972644            if (IsTopParent)
 2645            {
 212646                return this;
 2647            }
 2648
 1762649            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2650        }
 2651
 2652        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2653        {
 452654            return [Id];
 2655        }
 2656
 2657        public virtual double? GetRefreshProgress()
 2658        {
 02659            return null;
 2660        }
 2661
 2662        public virtual ItemUpdateType OnMetadataChanged()
 2663        {
 1182664            var updateType = ItemUpdateType.None;
 2665
 1182666            var item = this;
 2667
 1182668            var rating = item.GetParentalRatingScore();
 1182669            if (rating is not null)
 2670            {
 02671                if (rating.Score != item.InheritedParentalRatingValue)
 2672                {
 02673                    item.InheritedParentalRatingValue = rating.Score;
 02674                    updateType |= ItemUpdateType.MetadataImport;
 2675                }
 2676
 02677                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2678                {
 02679                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02680                    updateType |= ItemUpdateType.MetadataImport;
 2681                }
 2682            }
 2683            else
 2684            {
 1182685                if (item.InheritedParentalRatingValue is not null)
 2686                {
 02687                    item.InheritedParentalRatingValue = null;
 02688                    item.InheritedParentalRatingSubValue = null;
 02689                    updateType |= ItemUpdateType.MetadataImport;
 2690                }
 2691            }
 2692
 1182693            return updateType;
 2694        }
 2695
 2696        /// <summary>
 2697        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2698        /// </summary>
 2699        /// <param name="children">Media children.</param>
 2700        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2701        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2702        {
 02703            var currentOfficialRating = OfficialRating;
 2704
 2705            // Gather all possible ratings
 02706            var ratings = children
 02707                .Select(i => i.OfficialRating)
 02708                .Where(i => !string.IsNullOrEmpty(i))
 02709                .Distinct(StringComparer.OrdinalIgnoreCase)
 02710                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 02711                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02712                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02713                .Select(i => i.rating);
 2714
 02715            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2716
 02717            return !string.Equals(
 02718                currentOfficialRating ?? string.Empty,
 02719                OfficialRating ?? string.Empty,
 02720                StringComparison.OrdinalIgnoreCase);
 2721        }
 2722
 2723        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2724        {
 02725            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2726        }
 2727
 2728        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2729        {
 02730            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, 
 2731        }
 2732
 2733        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2734        {
 02735            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2736        }
 2737
 2738        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2739        {
 02740            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user,
 2741        }
 2742
 2743        /// <summary>
 2744        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2745        /// </summary>
 2746        /// <returns>An enumerable containing the items.</returns>
 2747        public IEnumerable<BaseItem> GetExtras()
 2748        {
 62749            return LibraryManager.GetItemList(new InternalItemsQuery()
 62750            {
 62751                OwnerIds = [Id],
 62752                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 62753            });
 2754        }
 2755
 2756        /// <summary>
 2757        /// Get all extras with specific types that are associated with this item.
 2758        /// </summary>
 2759        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2760        /// <returns>An enumerable containing the extras.</returns>
 2761        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
 2762        {
 02763            return LibraryManager.GetItemList(new InternalItemsQuery()
 02764            {
 02765                OwnerIds = [Id],
 02766                ExtraTypes = extraTypes.ToArray(),
 02767                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 02768            });
 2769        }
 2770
 2771        public virtual long GetRunTimeTicksForPlayState()
 2772        {
 02773            return RunTimeTicks ?? 0;
 2774        }
 2775
 2776        /// <inheritdoc />
 2777        public override bool Equals(object obj)
 2778        {
 02779            return obj is BaseItem baseItem && this.Equals(baseItem);
 2780        }
 2781
 2782        /// <inheritdoc />
 1172783        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2784
 2785        /// <inheritdoc />
 1042786        public override int GetHashCode() => HashCode.Combine(Id);
 2787    }
 2788}

Methods/Properties

.cctor()
.ctor()
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_HasLocalAlternateVersions()
get_OfficialRatingForComparison()
get_CustomRatingForComparison()
get_LatestItemsIndexContainer()
get_EnableRememberingTrackSelections()
get_IsTopParent()
get_SupportsAncestors()
get_SupportsOwnedItems()
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()
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)
GetMediaSourceName(MediaBrowser.Controller.Entities.BaseItem)
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)
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>>)
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>>)
GetExtras()
GetExtras(System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Model.Entities.ExtraType>)
GetRunTimeTicksForPlayState()
Equals(System.Object)
Equals(MediaBrowser.Controller.Entities.BaseItem)
GetHashCode()