< 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: 408
Uncovered lines: 480
Coverable lines: 888
Total lines: 2779
Line coverage: 45.9%
Branch coverage
33%
Covered branches: 183
Total branches: 550
Branch coverage: 33.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/20/2026 - 12:10:58 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: 27886/2/2026 - 12:15:49 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 2779 2/20/2026 - 12:10:58 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: 27886/2/2026 - 12:15:49 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 2779

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%7666.66%
get_PathProtocol()100%22100%
get_IsFileProtocol()100%11100%
get_HasPathProtocol()100%210%
get_SupportsLocalMetadata()50%2266.66%
get_FileNameWithoutExtension()0%620%
get_EnableAlphaNumericSorting()100%11100%
get_IsHD()100%11100%
get_PrimaryImagePath()100%210%
get_MediaType()100%11100%
get_PhysicalLocations()0%620%
get_EnableMediaSourceDisplay()50%2266.66%
get_ForcedSortName()100%11100%
set_ForcedSortName(...)100%11100%
get_SortName()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%2162460%
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.Library;
 27using MediaBrowser.Controller.MediaSegments;
 28using MediaBrowser.Controller.Persistence;
 29using MediaBrowser.Controller.Providers;
 30using MediaBrowser.Model.Dto;
 31using MediaBrowser.Model.Entities;
 32using MediaBrowser.Model.Globalization;
 33using MediaBrowser.Model.IO;
 34using MediaBrowser.Model.Library;
 35using MediaBrowser.Model.LiveTv;
 36using MediaBrowser.Model.MediaInfo;
 37using Microsoft.Extensions.Logging;
 38
 39namespace MediaBrowser.Controller.Entities
 40{
 41    /// <summary>
 42    /// Class BaseItem.
 43    /// </summary>
 44    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 45    {
 46        private BaseItemKind? _baseItemKind;
 47
 48        public const string ThemeSongFileName = "theme";
 49
 50        /// <summary>
 51        /// The supported image extensions.
 52        /// </summary>
 453        public static readonly string[] SupportedImageExtensions
 454            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 55
 456        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 457        {
 458            ".nfo",
 459            ".xml",
 460            ".srt",
 461            ".vtt",
 462            ".sub",
 463            ".sup",
 464            ".idx",
 465            ".txt",
 466            ".edl",
 467            ".bif",
 468            ".smi",
 469            ".ttml",
 470            ".lrc",
 471            ".elrc"
 472        };
 73
 74        /// <summary>
 75        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 76        /// </summary>
 477        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 478        {
 479            Model.Entities.ExtraType.Unknown,
 480            Model.Entities.ExtraType.BehindTheScenes,
 481            Model.Entities.ExtraType.Clip,
 482            Model.Entities.ExtraType.DeletedScene,
 483            Model.Entities.ExtraType.Interview,
 484            Model.Entities.ExtraType.Sample,
 485            Model.Entities.ExtraType.Scene,
 486            Model.Entities.ExtraType.Featurette,
 487            Model.Entities.ExtraType.Short
 488        };
 89
 90        private string _sortName;
 91
 92        private string _forcedSortName;
 93
 94        private string _name;
 95
 96        private string _originalLanguage;
 97
 98        public const char SlugChar = '-';
 99
 100        protected BaseItem()
 101        {
 1132102            Tags = Array.Empty<string>();
 1132103            Genres = Array.Empty<string>();
 1132104            Studios = Array.Empty<string>();
 1132105            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 1132106            LockedFields = Array.Empty<MetadataField>();
 1132107            ImageInfos = Array.Empty<ItemImageInfo>();
 1132108            ProductionLocations = Array.Empty<string>();
 1132109            RemoteTrailers = Array.Empty<MediaUrl>();
 1132110            UserData = [];
 1132111        }
 112
 113        /// <summary>
 114        /// Gets or Sets the user data collection as cached from the last Db query.
 115        /// </summary>
 116        [JsonIgnore]
 117        public ICollection<UserData> UserData { get; set; }
 118
 119        [JsonIgnore]
 120        public string PreferredMetadataCountryCode { get; set; }
 121
 122        [JsonIgnore]
 123        public string PreferredMetadataLanguage { get; set; }
 124
 125        public long? Size { get; set; }
 126
 127        public string Container { get; set; }
 128
 129        [JsonIgnore]
 130        public string Tagline { get; set; }
 131
 132        [JsonIgnore]
 133        public virtual ItemImageInfo[] ImageInfos { get; set; }
 134
 135        [JsonIgnore]
 136        public bool IsVirtualItem { get; set; }
 137
 138        /// <summary>
 139        /// Gets or sets the album.
 140        /// </summary>
 141        /// <value>The album.</value>
 142        [JsonIgnore]
 143        public string Album { get; set; }
 144
 145        /// <summary>
 146        /// Gets or sets the LUFS value.
 147        /// </summary>
 148        /// <value>The LUFS Value.</value>
 149        [JsonIgnore]
 150        public float? LUFS { get; set; }
 151
 152        /// <summary>
 153        /// Gets or sets the gain required for audio normalization.
 154        /// </summary>
 155        /// <value>The gain required for audio normalization.</value>
 156        [JsonIgnore]
 157        public float? NormalizationGain { get; set; }
 158
 159        /// <summary>
 160        /// Gets or sets the channel identifier.
 161        /// </summary>
 162        /// <value>The channel identifier.</value>
 163        [JsonIgnore]
 164        public Guid ChannelId { get; set; }
 165
 166        [JsonIgnore]
 0167        public virtual bool SupportsAddingToPlaylist => false;
 168
 169        [JsonIgnore]
 16170        public virtual bool AlwaysScanInternalMetadataPath => false;
 171
 172        /// <summary>
 173        /// Gets or sets a value indicating whether this instance is in mixed folder.
 174        /// </summary>
 175        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 176        [JsonIgnore]
 177        public bool IsInMixedFolder { get; set; }
 178
 179        [JsonIgnore]
 0180        public virtual bool SupportsPlayedStatus => false;
 181
 182        [JsonIgnore]
 0183        public virtual bool SupportsPositionTicksResume => false;
 184
 185        [JsonIgnore]
 17186        public virtual bool SupportsRemoteImageDownloading => true;
 187
 188        /// <summary>
 189        /// Gets or sets the name.
 190        /// </summary>
 191        /// <value>The name.</value>
 192        [JsonIgnore]
 193        public virtual string Name
 194        {
 1004195            get => _name;
 196            set
 197            {
 405198                _name = value;
 199
 200                // lazy load this again
 405201                _sortName = null;
 405202            }
 203        }
 204
 205        [JsonIgnore]
 0206        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 207
 208        [JsonIgnore]
 209        public int? TotalBitrate { get; set; }
 210
 211        [JsonIgnore]
 212        public ExtraType? ExtraType { get; set; }
 213
 214        [JsonIgnore]
 0215        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 216
 217        [JsonIgnore]
 218        public string OriginalTitle { get; set; }
 219
 220        [JsonIgnore]
 221        public string OriginalLanguage
 222        {
 402223            get => _originalLanguage;
 230224            set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
 225        }
 226
 227        /// <summary>
 228        /// Gets or sets the id.
 229        /// </summary>
 230        /// <value>The id.</value>
 231        [JsonIgnore]
 232        public Guid Id { get; set; }
 233
 234        [JsonIgnore]
 235        public Guid OwnerId { get; set; }
 236
 237        /// <summary>
 238        /// Gets or sets the audio.
 239        /// </summary>
 240        /// <value>The audio.</value>
 241        [JsonIgnore]
 242        public ProgramAudio? Audio { get; set; }
 243
 244        /// <summary>
 245        /// Gets the id that should be used to key display prefs for this item.
 246        /// Default is based on the type for everything except actual generic folders.
 247        /// </summary>
 248        /// <value>The display prefs id.</value>
 249        [JsonIgnore]
 250        public virtual Guid DisplayPreferencesId
 251        {
 252            get
 253            {
 6254                var thisType = GetType();
 6255                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 256            }
 257        }
 258
 259        /// <summary>
 260        /// Gets or sets the path.
 261        /// </summary>
 262        /// <value>The path.</value>
 263        [JsonIgnore]
 264        public virtual string Path { get; set; }
 265
 266        [JsonIgnore]
 267        public virtual SourceType SourceType
 268        {
 269            get
 270            {
 2093271                if (!ChannelId.IsEmpty())
 272                {
 0273                    return SourceType.Channel;
 274                }
 275
 2093276                return SourceType.Library;
 277            }
 278        }
 279
 280        /// <summary>
 281        /// Gets the folder containing the item.
 282        /// If the item is a folder, it returns the folder itself.
 283        /// </summary>
 284        [JsonIgnore]
 285        public virtual string ContainingFolderPath
 286        {
 287            get
 288            {
 472289                if (IsFolder)
 290                {
 412291                    return Path;
 292                }
 293
 60294                return System.IO.Path.GetDirectoryName(Path);
 295            }
 296        }
 297
 298        /// <summary>
 299        /// Gets or sets the name of the service.
 300        /// </summary>
 301        /// <value>The name of the service.</value>
 302        [JsonIgnore]
 303        public string ServiceName { get; set; }
 304
 305        /// <summary>
 306        /// Gets or sets the external id.
 307        /// </summary>
 308        /// <remarks>
 309        /// If this content came from an external service, the id of the content on that service.
 310        /// </remarks>
 311        [JsonIgnore]
 312        public string ExternalId { get; set; }
 313
 314        [JsonIgnore]
 315        public string ExternalSeriesId { get; set; }
 316
 317        [JsonIgnore]
 0318        public virtual bool IsHidden => false;
 319
 320        /// <summary>
 321        /// Gets the type of the location.
 322        /// </summary>
 323        /// <value>The type of the location.</value>
 324        [JsonIgnore]
 325        public virtual LocationType LocationType
 326        {
 327            get
 328            {
 12329                var path = Path;
 12330                if (string.IsNullOrEmpty(path))
 331                {
 3332                    if (SourceType == SourceType.Channel)
 333                    {
 0334                        return LocationType.Remote;
 335                    }
 336
 3337                    return LocationType.Virtual;
 338                }
 339
 9340                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 341            }
 342        }
 343
 344        [JsonIgnore]
 345        public MediaProtocol? PathProtocol
 346        {
 347            get
 348            {
 2487349                var path = Path;
 350
 2487351                if (string.IsNullOrEmpty(path))
 352                {
 116353                    return null;
 354                }
 355
 2371356                return MediaSourceManager.GetPathProtocol(path);
 357            }
 358        }
 359
 360        [JsonIgnore]
 2466361        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 362
 363        [JsonIgnore]
 0364        public bool HasPathProtocol => PathProtocol.HasValue;
 365
 366        [JsonIgnore]
 367        public virtual bool SupportsLocalMetadata
 368        {
 369            get
 370            {
 1294371                if (SourceType == SourceType.Channel)
 372                {
 0373                    return false;
 374                }
 375
 1294376                return IsFileProtocol;
 377            }
 378        }
 379
 380        [JsonIgnore]
 381        public virtual string FileNameWithoutExtension
 382        {
 383            get
 384            {
 0385                if (IsFileProtocol)
 386                {
 0387                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 388                }
 389
 0390                return null;
 391            }
 392        }
 393
 394        [JsonIgnore]
 100395        public virtual bool EnableAlphaNumericSorting => true;
 396
 139397        public virtual bool IsHD => Height >= 720;
 398
 399        public bool IsShortcut { get; set; }
 400
 401        public string ShortcutPath { get; set; }
 402
 403        public int Width { get; set; }
 404
 405        public int Height { get; set; }
 406
 407        /// <summary>
 408        /// Gets the primary image path.
 409        /// </summary>
 410        /// <remarks>
 411        /// This is just a helper for convenience.
 412        /// </remarks>
 413        /// <value>The primary image path.</value>
 414        [JsonIgnore]
 0415        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 416
 417        /// <summary>
 418        /// Gets or sets the date created.
 419        /// </summary>
 420        /// <value>The date created.</value>
 421        [JsonIgnore]
 422        public DateTime DateCreated { get; set; }
 423
 424        /// <summary>
 425        /// Gets or sets the date modified.
 426        /// </summary>
 427        /// <value>The date modified.</value>
 428        [JsonIgnore]
 429        public DateTime DateModified { get; set; }
 430
 431        public DateTime DateLastSaved { get; set; }
 432
 433        [JsonIgnore]
 434        public DateTime DateLastRefreshed { get; set; }
 435
 436        [JsonIgnore]
 437        public bool IsLocked { get; set; }
 438
 439        /// <summary>
 440        /// Gets or sets the locked fields.
 441        /// </summary>
 442        /// <value>The locked fields.</value>
 443        [JsonIgnore]
 444        public MetadataField[] LockedFields { get; set; }
 445
 446        /// <summary>
 447        /// Gets the type of the media.
 448        /// </summary>
 449        /// <value>The type of the media.</value>
 450        [JsonIgnore]
 130451        public virtual MediaType MediaType => MediaType.Unknown;
 452
 453        [JsonIgnore]
 454        public virtual string[] PhysicalLocations
 455        {
 456            get
 457            {
 0458                if (!IsFileProtocol)
 459                {
 0460                    return Array.Empty<string>();
 461                }
 462
 0463                return [Path];
 464            }
 465        }
 466
 467        [JsonIgnore]
 468        public bool EnableMediaSourceDisplay
 469        {
 470            get
 471            {
 6472                if (SourceType == SourceType.Channel)
 473                {
 0474                    return ChannelManager.EnableMediaSourceDisplay(this);
 475                }
 476
 6477                return true;
 478            }
 479        }
 480
 481        [JsonIgnore]
 482        public Guid ParentId { get; set; }
 483
 484        /// <summary>
 485        /// Gets or sets the logger.
 486        /// </summary>
 487        public static ILogger<BaseItem> Logger { get; set; }
 488
 489        public static ILibraryManager LibraryManager { get; set; }
 490
 491        public static IServerConfigurationManager ConfigurationManager { get; set; }
 492
 493        public static IProviderManager ProviderManager { get; set; }
 494
 495        public static ILocalizationManager LocalizationManager { get; set; }
 496
 497        public static IItemRepository ItemRepository { get; set; }
 498
 499        public static IItemCountService ItemCountService { get; set; }
 500
 501        public static IChapterManager ChapterManager { get; set; }
 502
 503        public static IFileSystem FileSystem { get; set; }
 504
 505        public static IUserDataManager UserDataManager { get; set; }
 506
 507        public static IChannelManager ChannelManager { get; set; }
 508
 509        public static IMediaSourceManager MediaSourceManager { get; set; }
 510
 511        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 512
 513        /// <summary>
 514        /// Gets or sets the name of the forced sort.
 515        /// </summary>
 516        /// <value>The name of the forced sort.</value>
 517        [JsonIgnore]
 518        public string ForcedSortName
 519        {
 478520            get => _forcedSortName;
 521            set
 522            {
 90523                _forcedSortName = value;
 90524                _sortName = null;
 90525            }
 526        }
 527
 528        /// <summary>
 529        /// Gets or sets the name of the sort.
 530        /// </summary>
 531        /// <value>The name of the sort.</value>
 532        [JsonIgnore]
 533        public string SortName
 534        {
 535            get
 536            {
 170537                if (_sortName is null)
 538                {
 100539                    if (!string.IsNullOrEmpty(ForcedSortName))
 540                    {
 541                        // Need the ToLower because that's what CreateSortName does
 0542                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 543                    }
 544                    else
 545                    {
 100546                        _sortName = CreateSortName();
 547                    }
 548                }
 549
 170550                return _sortName;
 551            }
 552
 129553            set => _sortName = value;
 554        }
 555
 556        [JsonIgnore]
 346557        public virtual Guid DisplayParentId => ParentId;
 558
 559        [JsonIgnore]
 560        public BaseItem DisplayParent
 561        {
 562            get
 563            {
 346564                var id = DisplayParentId;
 346565                if (id.IsEmpty())
 566                {
 256567                    return null;
 568                }
 569
 90570                return LibraryManager.GetItemById(id);
 571            }
 572        }
 573
 574        /// <summary>
 575        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 576        /// </summary>
 577        /// <value>The premiere date.</value>
 578        [JsonIgnore]
 579        public DateTime? PremiereDate { get; set; }
 580
 581        /// <summary>
 582        /// Gets or sets the end date.
 583        /// </summary>
 584        /// <value>The end date.</value>
 585        [JsonIgnore]
 586        public DateTime? EndDate { get; set; }
 587
 588        /// <summary>
 589        /// Gets or sets the official rating.
 590        /// </summary>
 591        /// <value>The official rating.</value>
 592        [JsonIgnore]
 593        public string OfficialRating { get; set; }
 594
 595        [JsonIgnore]
 596        public int? InheritedParentalRatingValue { get; set; }
 597
 598        [JsonIgnore]
 599        public int? InheritedParentalRatingSubValue { get; set; }
 600
 601        /// <summary>
 602        /// Gets or sets the critic rating.
 603        /// </summary>
 604        /// <value>The critic rating.</value>
 605        [JsonIgnore]
 606        public float? CriticRating { get; set; }
 607
 608        /// <summary>
 609        /// Gets or sets the custom rating.
 610        /// </summary>
 611        /// <value>The custom rating.</value>
 612        [JsonIgnore]
 613        public string CustomRating { get; set; }
 614
 615        /// <summary>
 616        /// Gets or sets the overview.
 617        /// </summary>
 618        /// <value>The overview.</value>
 619        [JsonIgnore]
 620        public string Overview { get; set; }
 621
 622        /// <summary>
 623        /// Gets or sets the studios.
 624        /// </summary>
 625        /// <value>The studios.</value>
 626        [JsonIgnore]
 627        public string[] Studios { get; set; }
 628
 629        /// <summary>
 630        /// Gets or sets the genres.
 631        /// </summary>
 632        /// <value>The genres.</value>
 633        [JsonIgnore]
 634        public string[] Genres { get; set; }
 635
 636        /// <summary>
 637        /// Gets or sets the tags.
 638        /// </summary>
 639        /// <value>The tags.</value>
 640        [JsonIgnore]
 641        public string[] Tags { get; set; }
 642
 643        [JsonIgnore]
 644        public string[] ProductionLocations { get; set; }
 645
 646        /// <summary>
 647        /// Gets or sets the home page URL.
 648        /// </summary>
 649        /// <value>The home page URL.</value>
 650        [JsonIgnore]
 651        public string HomePageUrl { get; set; }
 652
 653        /// <summary>
 654        /// Gets or sets the community rating.
 655        /// </summary>
 656        /// <value>The community rating.</value>
 657        [JsonIgnore]
 658        public float? CommunityRating { get; set; }
 659
 660        /// <summary>
 661        /// Gets or sets the run time ticks.
 662        /// </summary>
 663        /// <value>The run time ticks.</value>
 664        [JsonIgnore]
 665        public long? RunTimeTicks { get; set; }
 666
 667        /// <summary>
 668        /// Gets or sets the production year.
 669        /// </summary>
 670        /// <value>The production year.</value>
 671        [JsonIgnore]
 672        public int? ProductionYear { get; set; }
 673
 674        /// <summary>
 675        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 676        /// This could be episode number, album track number, etc.
 677        /// </summary>
 678        /// <value>The index number.</value>
 679        [JsonIgnore]
 680        public int? IndexNumber { get; set; }
 681
 682        /// <summary>
 683        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 684        /// </summary>
 685        /// <value>The parent index number.</value>
 686        [JsonIgnore]
 687        public int? ParentIndexNumber { get; set; }
 688
 689        [JsonIgnore]
 0690        public virtual bool HasLocalAlternateVersions => false;
 691
 692        [JsonIgnore]
 693        public string OfficialRatingForComparison
 694        {
 695            get
 696            {
 170697                var officialRating = OfficialRating;
 170698                if (!string.IsNullOrEmpty(officialRating))
 699                {
 0700                    return officialRating;
 701                }
 702
 170703                var parent = DisplayParent;
 170704                if (parent is not null)
 705                {
 42706                    return parent.OfficialRatingForComparison;
 707                }
 708
 128709                return null;
 710            }
 711        }
 712
 713        [JsonIgnore]
 714        public string CustomRatingForComparison
 715        {
 716            get
 717            {
 128718                return GetCustomRatingForComparision();
 719            }
 720        }
 721
 722        /// <summary>
 723        /// Gets or sets the provider ids.
 724        /// </summary>
 725        /// <value>The provider ids.</value>
 726        [JsonIgnore]
 727        public Dictionary<string, string> ProviderIds { get; set; }
 728
 729        [JsonIgnore]
 0730        public virtual Folder LatestItemsIndexContainer => null;
 731
 732        [JsonIgnore]
 733        public string PresentationUniqueKey { get; set; }
 734
 735        [JsonIgnore]
 22736        public virtual bool EnableRememberingTrackSelections => true;
 737
 738        [JsonIgnore]
 739        public virtual bool IsTopParent
 740        {
 741            get
 742            {
 352743                if (this is BasePluginFolder || this is Channel)
 744                {
 60745                    return true;
 746                }
 747
 292748                if (this is IHasCollectionType view)
 749                {
 14750                    if (view.CollectionType == CollectionType.livetv)
 751                    {
 0752                        return true;
 753                    }
 754                }
 755
 292756                if (GetParent() is AggregateFolder)
 757                {
 0758                    return true;
 759                }
 760
 292761                return false;
 762            }
 763        }
 764
 765        [JsonIgnore]
 444766        public virtual bool SupportsAncestors => true;
 767
 768        [JsonIgnore]
 96769        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 770
 771        [JsonIgnore]
 69772        public virtual bool SupportsPeople => false;
 773
 774        [JsonIgnore]
 0775        public virtual bool SupportsThemeMedia => false;
 776
 777        [JsonIgnore]
 0778        public virtual bool SupportsInheritedParentImages => false;
 779
 780        /// <summary>
 781        /// Gets a value indicating whether this instance is folder.
 782        /// </summary>
 783        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 784        [JsonIgnore]
 86785        public virtual bool IsFolder => false;
 786
 787        [JsonIgnore]
 0788        public virtual bool IsDisplayedAsFolder => false;
 789
 790        /// <summary>
 791        /// Gets or sets the remote trailers.
 792        /// </summary>
 793        /// <value>The remote trailers.</value>
 794        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 795
 796        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 797        {
 170798            callstack ??= new();
 170799            var customRating = CustomRating;
 170800            if (!string.IsNullOrEmpty(customRating))
 801            {
 0802                return customRating;
 803            }
 804
 170805            callstack.Add(Id);
 806
 170807            var parent = DisplayParent;
 170808            if (parent is not null && !callstack.Contains(parent.Id))
 809            {
 42810                return parent.GetCustomRatingForComparision(callstack);
 811            }
 812
 128813            return null;
 814        }
 815
 816        public virtual double GetDefaultPrimaryImageAspectRatio()
 817        {
 0818            return 0;
 819        }
 820
 821        public virtual string CreatePresentationUniqueKey()
 822        {
 59823            return Id.ToString("N", CultureInfo.InvariantCulture);
 824        }
 825
 826        public virtual bool CanDelete()
 827        {
 0828            if (SourceType == SourceType.Channel)
 829            {
 0830                return ChannelManager.CanDelete(this);
 831            }
 832
 0833            return IsFileProtocol;
 834        }
 835
 836        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 837        {
 0838            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 839            {
 0840                return true;
 841            }
 842
 0843            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 844
 0845            if (SourceType == SourceType.Channel)
 846            {
 0847                return allowed.Contains(ChannelId);
 848            }
 849
 0850            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 851
 0852            foreach (var folder in collectionFolders)
 853            {
 0854                if (allowed.Contains(folder.Id))
 855                {
 0856                    return true;
 857                }
 858            }
 859
 0860            return false;
 0861        }
 862
 863        public BaseItem GetOwner()
 864        {
 631865            var ownerId = OwnerId;
 631866            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 867        }
 868
 869        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 870        {
 6871            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 872        }
 873
 874        public virtual bool CanDelete(User user)
 875        {
 6876            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 877
 6878            return CanDelete(user, allCollectionFolders);
 879        }
 880
 881        public virtual bool CanDownload()
 882        {
 6883            return false;
 884        }
 885
 886        public virtual bool IsAuthorizedToDownload(User user)
 887        {
 0888            return user.HasPermission(PermissionKind.EnableContentDownloading);
 889        }
 890
 891        public bool CanDownload(User user)
 892        {
 6893            return CanDownload() && IsAuthorizedToDownload(user);
 894        }
 895
 896        /// <inheritdoc />
 897        public override string ToString()
 898        {
 69899            return Name;
 900        }
 901
 902        public virtual string GetInternalMetadataPath()
 903        {
 71904            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 905
 71906            return GetInternalMetadataPath(basePath);
 907        }
 908
 909        protected virtual string GetInternalMetadataPath(string basePath)
 910        {
 71911            if (SourceType == SourceType.Channel)
 912            {
 0913                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 914            }
 915
 71916            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 917
 71918            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 919        }
 920
 921        /// <summary>
 922        /// Creates the name of the sort.
 923        /// </summary>
 924        /// <returns>System.String.</returns>
 925        protected virtual string CreateSortName()
 926        {
 100927            if (Name is null)
 928            {
 0929                return null; // some items may not have name filled in properly
 930            }
 931
 100932            if (!EnableAlphaNumericSorting)
 933            {
 0934                return Name.TrimStart();
 935            }
 936
 100937            var sortable = Name.Trim().ToLowerInvariant();
 938
 800939            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 940            {
 941                // Remove from beginning if a space follows
 300942                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 943                {
 0944                    sortable = sortable.Remove(0, search.Length + 1);
 945                }
 946
 947                // Remove from middle if surrounded by spaces
 300948                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 949
 950                // Remove from end if preceeded by a space
 300951                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 952                {
 0953                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 954                }
 955            }
 956
 1400957            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 958            {
 600959                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 960            }
 961
 800962            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 963            {
 300964                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 965            }
 966
 100967            return ModifySortChunks(sortable);
 968        }
 969
 970        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 971        {
 972            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 973            {
 974                if (isDigitChunk && chunk.Length < 10)
 975                {
 976                    builder.Append('0', 10 - chunk.Length);
 977                }
 978
 979                builder.Append(chunk);
 980            }
 981
 106982            if (name.IsEmpty)
 983            {
 1984                return string.Empty;
 985            }
 986
 105987            var builder = new StringBuilder(name.Length);
 988
 105989            int chunkStart = 0;
 105990            bool isDigitChunk = char.IsDigit(name[0]);
 1674991            for (int i = 0; i < name.Length; i++)
 992            {
 732993                var isDigit = char.IsDigit(name[i]);
 732994                if (isDigit != isDigitChunk)
 995                {
 5996                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5997                    chunkStart = i;
 5998                    isDigitChunk = isDigit;
 999                }
 1000            }
 1001
 1051002            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 1003
 1004            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 1051005            var result = builder.ToString().RemoveDiacritics();
 1051006            if (!result.All(char.IsAscii))
 1007            {
 01008                result = result.Transliterated();
 1009            }
 1010
 1051011            return result;
 1012        }
 1013
 1014        public BaseItem GetParent()
 1015        {
 19201016            var parentId = ParentId;
 19201017            if (parentId.IsEmpty())
 1018            {
 16801019                return null;
 1020            }
 1021
 2401022            return LibraryManager.GetItemById(parentId);
 1023        }
 1024
 1025        public IEnumerable<BaseItem> GetParents()
 1026        {
 6891027            var parent = GetParent();
 1028
 7611029            while (parent is not null)
 1030            {
 721031                yield return parent;
 1032
 721033                parent = parent.GetParent();
 1034            }
 6891035        }
 1036
 1037        /// <summary>
 1038        /// Finds a parent of a given type.
 1039        /// </summary>
 1040        /// <typeparam name="T">Type of parent.</typeparam>
 1041        /// <returns>``0.</returns>
 1042        public T FindParent<T>()
 1043            where T : Folder
 1044        {
 01045            foreach (var parent in GetParents())
 1046            {
 01047                if (parent is T item)
 1048                {
 01049                    return item;
 1050                }
 1051            }
 1052
 01053            return null;
 01054        }
 1055
 1056        /// <summary>
 1057        /// Gets the play access.
 1058        /// </summary>
 1059        /// <param name="user">The user.</param>
 1060        /// <returns>PlayAccess.</returns>
 1061        public PlayAccess GetPlayAccess(User user)
 1062        {
 61063            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1064            {
 01065                return PlayAccess.None;
 1066            }
 1067
 1068            // if (!user.IsParentalScheduleAllowed())
 1069            // {
 1070            //    return PlayAccess.None;
 1071            // }
 1072
 61073            return PlayAccess.Full;
 1074        }
 1075
 1076        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1077        {
 01078            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01079            {
 01080                ItemId = Id
 01081            });
 1082        }
 1083
 1084        protected virtual bool IsActiveRecording()
 1085        {
 01086            return false;
 1087        }
 1088
 1089        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1090        {
 01091            if (SourceType == SourceType.Channel)
 1092            {
 01093                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01094                           .ToList();
 1095
 01096                if (sources.Count > 0)
 1097                {
 01098                    return sources;
 1099                }
 1100            }
 1101
 01102            var list = GetAllItemsForMediaSources();
 01103            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
 1104
 01105            if (IsActiveRecording())
 1106            {
 01107                foreach (var mediaSource in result)
 1108                {
 01109                    mediaSource.Type = MediaSourceType.Placeholder;
 1110                }
 1111            }
 1112
 01113            return result.OrderBy(i =>
 01114            {
 01115                if (i.VideoType == VideoType.VideoFile)
 01116                {
 01117                    return 0;
 01118                }
 01119
 01120                return 1;
 01121            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 01122            .ThenByDescending(i => i, new MediaSourceWidthComparator())
 01123            .ToArray();
 1124        }
 1125
 1126        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1127        {
 01128            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1129        }
 1130
 1131        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
 1132        {
 01133            ArgumentNullException.ThrowIfNull(item);
 1134
 01135            var protocol = item.PathProtocol;
 01136            var itemPath = item.Path;
 1137
 01138            var info = new MediaSourceInfo
 01139            {
 01140                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 01141                Protocol = protocol ?? MediaProtocol.File,
 01142                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 01143                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 01144                Name = GetMediaSourceName(item),
 01145                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 01146                RunTimeTicks = item.RunTimeTicks,
 01147                Container = item.Container,
 01148                Size = item.Size,
 01149                Type = type,
 01150                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 01151                    && (protocol is null or MediaProtocol.File)
 01152                    && MediaSegmentManager.HasSegments(item.Id)
 01153            };
 1154
 01155            if (string.IsNullOrEmpty(info.Path))
 1156            {
 01157                info.Type = MediaSourceType.Placeholder;
 1158            }
 1159
 01160            if (info.Protocol == MediaProtocol.File)
 1161            {
 01162                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1163            }
 1164
 01165            var video = item as Video;
 01166            if (video is not null)
 1167            {
 01168                info.IsoType = video.IsoType;
 01169                info.VideoType = video.VideoType;
 01170                info.Video3DFormat = video.Video3DFormat;
 01171                info.Timestamp = video.Timestamp;
 1172
 01173                if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
 1174                {
 01175                    var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
 1176
 1177                    // Only allow remote shortcut paths — local file paths in .strm files
 1178                    // could be used to read arbitrary files from the server.
 01179                    if (shortcutProtocol != MediaProtocol.File)
 1180                    {
 01181                        info.IsRemote = true;
 01182                        info.Path = video.ShortcutPath;
 01183                        info.Protocol = shortcutProtocol;
 1184                    }
 1185                }
 1186
 01187                if (string.IsNullOrEmpty(info.Container))
 1188                {
 01189                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1190                    {
 01191                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1192                        {
 01193                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1194                        }
 1195                    }
 1196                }
 1197            }
 1198
 01199            if (string.IsNullOrEmpty(info.Container))
 1200            {
 01201                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1202                {
 01203                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1204                }
 1205            }
 1206
 01207            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1208            {
 01209                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1210            }
 1211
 01212            if (video is not null && video.VideoType != VideoType.VideoFile)
 1213            {
 01214                info.SupportsDirectStream = false;
 1215            }
 1216
 01217            info.Bitrate = item.TotalBitrate;
 01218            info.InferTotalBitrate();
 1219
 01220            return info;
 1221        }
 1222
 1223        internal string GetMediaSourceName(BaseItem item)
 1224        {
 41225            var terms = new List<string>();
 1226
 41227            var path = item.Path;
 41228            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1229            {
 41230                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 41231                if (HasLocalAlternateVersions)
 1232                {
 41233                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 41234                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1235                    {
 21236                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
 21237                        if (!name.IsWhiteSpace())
 1238                        {
 21239                            terms.Add(name.ToString());
 1240                        }
 1241                    }
 1242                }
 1243
 41244                if (terms.Count == 0)
 1245                {
 21246                    terms.Add(displayName);
 1247                }
 1248            }
 1249
 41250            if (terms.Count == 0)
 1251            {
 01252                terms.Add(item.Name);
 1253            }
 1254
 41255            if (item is Video video)
 1256            {
 41257                if (video.Video3DFormat.HasValue)
 1258                {
 01259                    terms.Add("3D");
 1260                }
 1261
 41262                if (video.VideoType == VideoType.BluRay)
 1263                {
 01264                    terms.Add("Bluray");
 1265                }
 41266                else if (video.VideoType == VideoType.Dvd)
 1267                {
 01268                    terms.Add("DVD");
 1269                }
 41270                else if (video.VideoType == VideoType.Iso)
 1271                {
 01272                    if (video.IsoType.HasValue)
 1273                    {
 01274                        if (video.IsoType.Value == IsoType.BluRay)
 1275                        {
 01276                            terms.Add("Bluray");
 1277                        }
 01278                        else if (video.IsoType.Value == IsoType.Dvd)
 1279                        {
 01280                            terms.Add("DVD");
 1281                        }
 1282                    }
 1283                    else
 1284                    {
 01285                        terms.Add("ISO");
 1286                    }
 1287                }
 1288            }
 1289
 41290            return string.Join('/', terms);
 1291        }
 1292
 1293        public Task RefreshMetadata(CancellationToken cancellationToken)
 1294        {
 511295            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1296        }
 1297
 1298        /// <summary>
 1299        /// The base implementation to refresh metadata.
 1300        /// </summary>
 1301        /// <param name="options">The options.</param>
 1302        /// <param name="cancellationToken">The cancellation token.</param>
 1303        /// <returns>true if a provider reports we changed.</returns>
 1304        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1305        {
 591306            var requiresSave = false;
 1307
 591308            if (SupportsOwnedItems)
 1309            {
 1310                try
 1311                {
 371312                    if (IsFileProtocol)
 1313                    {
 371314                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1315                    }
 1316
 371317                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 371318                }
 01319                catch (Exception ex)
 1320                {
 01321                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01322                }
 1323            }
 1324
 591325            var refreshOptions = requiresSave
 591326                ? new MetadataRefreshOptions(options)
 591327                {
 591328                    ForceSave = true
 591329                }
 591330                : options;
 1331
 591332            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 591333        }
 1334
 1335        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1336        {
 01337            if (!IsVisible(user))
 1338            {
 01339                return false;
 1340            }
 1341
 01342            var parents = GetParents().ToList();
 01343            if (parents.Any(i => !i.IsVisible(user, true)))
 1344            {
 01345                return false;
 1346            }
 1347
 01348            if (checkFolders)
 1349            {
 01350                var topParent = parents.Count > 0 ? parents[^1] : this;
 1351
 01352                if (string.IsNullOrEmpty(topParent.Path))
 1353                {
 01354                    return true;
 1355                }
 1356
 01357                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1358
 01359                if (itemCollectionFolders.Count > 0)
 1360                {
 01361                    var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 1362                    IEnumerable<Guid> userCollectionFolderIds;
 01363                    if (blockedMediaFolders.Length > 0)
 1364                    {
 1365                        // User has blocked folders - get all library folders and exclude blocked ones
 01366                        userCollectionFolderIds = LibraryManager.GetUserRootFolder().Children
 01367                            .Select(i => i.Id)
 01368                            .Where(id => !blockedMediaFolders.Contains(id));
 1369                    }
 01370                    else if (user.HasPermission(PermissionKind.EnableAllFolders))
 1371                    {
 1372                        // User can access all folders - no need to filter
 01373                        return true;
 1374                    }
 1375                    else
 1376                    {
 1377                        // User has specific enabled folders
 01378                        userCollectionFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders);
 1379                    }
 1380
 01381                    if (!itemCollectionFolders.Any(userCollectionFolderIds.Contains))
 1382                    {
 01383                        return false;
 1384                    }
 1385                }
 1386            }
 1387
 01388            return true;
 1389        }
 1390
 1391        public void SetParent(Folder parent)
 1392        {
 101393            ParentId = parent is null ? Guid.Empty : parent.Id;
 101394        }
 1395
 1396        /// <summary>
 1397        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1398        /// Returns true or false indicating if changes were found.
 1399        /// </summary>
 1400        /// <param name="options">The metadata refresh options.</param>
 1401        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1402        /// <param name="cancellationToken">The cancellation token.</param>
 1403        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1404        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1405        {
 371406            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1407            {
 371408                return false;
 1409            }
 1410
 01411            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 371412        }
 1413
 1414        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1415        {
 421416            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1417        }
 1418
 1419        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1420        {
 01421            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 01422            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1423
 01424            var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
 01425            {
 01426                OwnerIds = [item.Id]
 01427            }).Select(e => e.Id).ToArray();
 1428
 01429            var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
 1430
 01431            if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.Full
 1432            {
 01433                return false;
 1434            }
 1435
 01436            var ownerId = item.Id;
 1437
 01438            var tasks = extras.Select(i =>
 01439            {
 01440                var subOptions = new MetadataRefreshOptions(options);
 01441                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
 01442                {
 01443                    subOptions.ForceSave = true;
 01444                }
 01445
 01446                i.OwnerId = ownerId;
 01447                i.ParentId = Guid.Empty;
 01448                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 01449            });
 1450
 01451            var removedExtraIds = currentExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 01452            if (removedExtraIds.Length > 0)
 1453            {
 01454                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 01455                {
 01456                    ItemIds = removedExtraIds
 01457                });
 01458                foreach (var removedExtra in removedExtras)
 1459                {
 1460                    // Only delete items that are actual extras (have ExtraType set)
 1461                    // Items with OwnerId but no ExtraType might be alternate versions, not extras
 01462                    if (removedExtra.ExtraType.HasValue)
 1463                    {
 01464                        LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 01465                        {
 01466                            DeleteFileLocation = false
 01467                        });
 1468                    }
 1469                }
 1470            }
 1471
 01472            await Task.WhenAll(tasks).ConfigureAwait(false);
 1473
 01474            return true;
 01475        }
 1476
 1477        public string GetPresentationUniqueKey()
 1478        {
 01479            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1480        }
 1481
 1482        public virtual bool RequiresRefresh()
 1483        {
 591484            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1485            {
 591486                return false;
 1487            }
 1488
 01489            var info = FileSystem.GetFileSystemInfo(Path);
 1490
 01491            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1492        }
 1493
 1494        public virtual List<string> GetUserDataKeys()
 1495        {
 1511496            var list = new List<string>();
 1497
 1511498            if (SourceType == SourceType.Channel)
 1499            {
 01500                if (!string.IsNullOrEmpty(ExternalId))
 1501                {
 01502                    list.Add(ExternalId);
 1503                }
 1504            }
 1505
 1511506            list.Add(Id.ToString());
 1511507            return list;
 1508        }
 1509
 1510        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1511        {
 61512            var updateType = ItemUpdateType.None;
 1513
 61514            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1515            {
 01516                IsInMixedFolder = newItem.IsInMixedFolder;
 01517                updateType |= ItemUpdateType.MetadataImport;
 1518            }
 1519
 61520            return updateType;
 1521        }
 1522
 1523        public void AfterMetadataRefresh()
 1524        {
 591525            _sortName = null;
 591526        }
 1527
 1528        /// <summary>
 1529        /// Gets the preferred metadata language.
 1530        /// </summary>
 1531        /// <returns>System.String.</returns>
 1532        public string GetPreferredMetadataLanguage()
 1533        {
 341534            string lang = PreferredMetadataLanguage;
 1535
 341536            if (string.IsNullOrEmpty(lang))
 1537            {
 341538                lang = GetParents()
 341539                    .Select(i => i.PreferredMetadataLanguage)
 341540                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1541            }
 1542
 341543            if (string.IsNullOrEmpty(lang))
 1544            {
 341545                lang = LibraryManager.GetCollectionFolders(this)
 341546                    .Select(i => i.PreferredMetadataLanguage)
 341547                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1548            }
 1549
 341550            if (string.IsNullOrEmpty(lang))
 1551            {
 341552                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1553            }
 1554
 341555            if (string.IsNullOrEmpty(lang))
 1556            {
 341557                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1558            }
 1559
 341560            return lang;
 1561        }
 1562
 1563        /// <summary>
 1564        /// Gets the preferred metadata country code.
 1565        /// </summary>
 1566        /// <returns>System.String.</returns>
 1567        public string GetPreferredMetadataCountryCode()
 1568        {
 341569            string lang = PreferredMetadataCountryCode;
 1570
 341571            if (string.IsNullOrEmpty(lang))
 1572            {
 341573                lang = GetParents()
 341574                    .Select(i => i.PreferredMetadataCountryCode)
 341575                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1576            }
 1577
 341578            if (string.IsNullOrEmpty(lang))
 1579            {
 341580                lang = LibraryManager.GetCollectionFolders(this)
 341581                    .Select(i => i.PreferredMetadataCountryCode)
 341582                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1583            }
 1584
 341585            if (string.IsNullOrEmpty(lang))
 1586            {
 341587                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1588            }
 1589
 341590            if (string.IsNullOrEmpty(lang))
 1591            {
 341592                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1593            }
 1594
 341595            return lang;
 1596        }
 1597
 1598        /// <summary>
 1599        /// Gets the original language of the item, inheriting from parent items if necessary.
 1600        /// </summary>
 1601        /// <returns>System.String.</returns>
 1602        public virtual string GetInheritedOriginalLanguage()
 1603        {
 01604            return OriginalLanguage;
 1605        }
 1606
 1607        public virtual bool IsSaveLocalMetadataEnabled()
 1608        {
 861609            if (SourceType == SourceType.Channel)
 1610            {
 01611                return false;
 1612            }
 1613
 861614            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1615
 861616            return libraryOptions.SaveLocalMetadata;
 1617        }
 1618
 1619        /// <summary>
 1620        /// Determines if a given user has access to this item.
 1621        /// </summary>
 1622        /// <param name="user">The user.</param>
 1623        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1624        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1625        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1626        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1627        {
 101628            ArgumentNullException.ThrowIfNull(user);
 1629
 101630            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1631            {
 01632                return false;
 1633            }
 1634
 101635            var maxAllowedRating = user.MaxParentalRatingScore;
 101636            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 101637            var rating = CustomRatingForComparison;
 1638
 101639            if (string.IsNullOrEmpty(rating))
 1640            {
 101641                rating = OfficialRatingForComparison;
 1642            }
 1643
 101644            if (string.IsNullOrEmpty(rating))
 1645            {
 101646                return !GetBlockUnratedValue(user);
 1647            }
 1648
 01649            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1650
 1651            // Could not determine rating level
 01652            if (ratingScore is null)
 1653            {
 01654                var isAllowed = !GetBlockUnratedValue(user);
 1655
 01656                if (!isAllowed)
 1657                {
 01658                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1659                }
 1660
 01661                return isAllowed;
 1662            }
 1663
 01664            if (!maxAllowedRating.HasValue)
 1665            {
 01666                return true;
 1667            }
 1668
 01669            if (ratingScore.Score != maxAllowedRating.Value)
 1670            {
 01671                return ratingScore.Score < maxAllowedRating.Value;
 1672            }
 1673
 01674            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1675        }
 1676
 1677        public ParentalRatingScore GetParentalRatingScore()
 1678        {
 1181679            var rating = CustomRatingForComparison;
 1680
 1181681            if (string.IsNullOrEmpty(rating))
 1682            {
 1181683                rating = OfficialRatingForComparison;
 1684            }
 1685
 1181686            if (string.IsNullOrEmpty(rating))
 1687            {
 1181688                return null;
 1689            }
 1690
 01691            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1692        }
 1693
 1694        public List<string> GetInheritedTags()
 1695        {
 1111696            var list = new List<string>();
 1111697            list.AddRange(Tags);
 1698
 2761699            foreach (var parent in GetParents())
 1700            {
 271701                list.AddRange(parent.Tags);
 1702            }
 1703
 2221704            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1705            {
 01706                list.AddRange(folder.Tags);
 1707            }
 1708
 1111709            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1710        }
 1711
 1712        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1713        {
 101714            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 101715            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 1716
 101717            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 1718            {
 101719                return true;
 1720            }
 1721
 1722            // Normalize tags using the same logic as database queries
 01723            var normalizedBlockedTags = blockedTags
 01724                .Where(t => !string.IsNullOrWhiteSpace(t))
 01725                .Select(t => t.GetCleanValue())
 01726                .ToHashSet(StringComparer.Ordinal);
 1727
 01728            var normalizedItemTags = GetInheritedTags()
 01729                .Select(t => t.GetCleanValue())
 01730                .ToHashSet(StringComparer.Ordinal);
 1731
 1732            // Check blocked tags - item is hidden if it has any blocked tag
 01733            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 1734            {
 01735                return false;
 1736            }
 1737
 01738            var parent = GetParents().FirstOrDefault() ?? this;
 01739            if (parent is UserRootFolder or AggregateFolder or UserView)
 1740            {
 01741                return true;
 1742            }
 1743
 1744            // Check allowed tags - item must have at least one allowed tag
 01745            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 1746            {
 01747                var normalizedAllowedTags = allowedTags
 01748                    .Where(t => !string.IsNullOrWhiteSpace(t))
 01749                    .Select(t => t.GetCleanValue())
 01750                    .ToHashSet(StringComparer.Ordinal);
 1751
 01752                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 1753                {
 01754                    return false;
 1755                }
 1756            }
 1757
 01758            return true;
 1759        }
 1760
 1761        public virtual UnratedItem GetBlockUnratedType()
 1762        {
 1111763            if (SourceType == SourceType.Channel)
 1764            {
 01765                return UnratedItem.ChannelContent;
 1766            }
 1767
 1111768            return UnratedItem.Other;
 1769        }
 1770
 1771        /// <summary>
 1772        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1773        /// </summary>
 1774        /// <param name="user">The configuration.</param>
 1775        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1776        protected virtual bool GetBlockUnratedValue(User user)
 1777        {
 1778            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1779            // Special folders like series and albums will override this method.
 101780            if (IsFolder || this is IItemByName)
 1781            {
 101782                return false;
 1783            }
 1784
 01785            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1786        }
 1787
 1788        /// <summary>
 1789        /// Determines if this folder should be visible to a given user.
 1790        /// Default is just parental allowed. Can be overridden for more functionality.
 1791        /// </summary>
 1792        /// <param name="user">The user.</param>
 1793        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1794        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1795        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1796        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1797        {
 101798            ArgumentNullException.ThrowIfNull(user);
 1799
 101800            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1801        }
 1802
 1803        public virtual bool IsVisibleStandalone(User user)
 1804        {
 01805            if (SourceType == SourceType.Channel)
 1806            {
 01807                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1808            }
 1809
 01810            return IsVisibleStandaloneInternal(user, true);
 1811        }
 1812
 1813        public virtual string GetClientTypeName()
 1814        {
 1131815            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1816            {
 01817                return "ChannelFolderItem";
 1818            }
 1819
 1131820            return GetType().Name;
 1821        }
 1822
 1823        public BaseItemKind GetBaseItemKind()
 1824        {
 2461825            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1826        }
 1827
 1828        /// <summary>
 1829        /// Gets the linked child.
 1830        /// </summary>
 1831        /// <param name="info">The info.</param>
 1832        /// <returns>BaseItem.</returns>
 1833        protected BaseItem GetLinkedChild(LinkedChild info)
 1834        {
 1835            // First get using the cached Id
 01836            if (info.ItemId.HasValue)
 1837            {
 01838                if (info.ItemId.Value.IsEmpty())
 1839                {
 01840                    return null;
 1841                }
 1842
 01843                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1844
 01845                if (itemById is not null)
 1846                {
 01847                    return itemById;
 1848                }
 1849            }
 1850
 01851            var item = FindLinkedChild(info);
 1852
 1853            // If still null, log
 01854            if (item is null)
 1855            {
 1856                // Don't keep searching over and over
 01857                info.ItemId = Guid.Empty;
 1858            }
 1859            else
 1860            {
 1861                // Cache the id for next time
 01862                info.ItemId = item.Id;
 1863            }
 1864
 01865            return item;
 1866        }
 1867
 1868#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 1869        private BaseItem FindLinkedChild(LinkedChild info)
 1870        {
 1871            // First try to find by ItemId (new preferred method)
 01872            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 1873            {
 01874                var item = LibraryManager.GetItemById(info.ItemId.Value);
 01875                if (item is not null)
 1876                {
 01877                    return item;
 1878                }
 1879
 01880                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 1881            }
 1882
 1883            // Fall back to Path (legacy method)
 01884            var path = info.Path;
 01885            if (!string.IsNullOrEmpty(path))
 1886            {
 01887                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 1888
 01889                var itemByPath = LibraryManager.FindByPath(path, null);
 1890
 01891                if (itemByPath is null)
 1892                {
 01893                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1894                }
 1895
 01896                return itemByPath;
 1897            }
 1898
 1899            // Fall back to LibraryItemId (legacy method)
 01900            if (!string.IsNullOrEmpty(info.LibraryItemId))
 1901            {
 01902                var item = LibraryManager.GetItemById(info.LibraryItemId);
 1903
 01904                if (item is null)
 1905                {
 01906                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 1907                }
 1908
 01909                return item;
 1910            }
 1911
 01912            return null;
 1913        }
 1914#pragma warning restore CS0618
 1915
 1916        /// <summary>
 1917        /// Adds a studio to the item.
 1918        /// </summary>
 1919        /// <param name="name">The name.</param>
 1920        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1921        public void AddStudio(string name)
 1922        {
 41923            ArgumentException.ThrowIfNullOrEmpty(name);
 41924            var current = Studios;
 1925
 41926            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 1927            {
 41928                int curLen = current.Length;
 41929                if (curLen == 0)
 1930                {
 41931                    Studios = [name];
 1932                }
 1933                else
 1934                {
 01935                    Studios = [.. current, name];
 1936                }
 1937            }
 01938        }
 1939
 1940        public void SetStudios(IEnumerable<string> names)
 1941        {
 01942            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 01943        }
 1944
 1945        /// <summary>
 1946        /// Adds a genre to the item.
 1947        /// </summary>
 1948        /// <param name="name">The name.</param>
 1949        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1950        public void AddGenre(string name)
 1951        {
 131952            ArgumentException.ThrowIfNullOrEmpty(name);
 1953
 131954            var genres = Genres;
 131955            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 1956            {
 131957                Genres = [.. genres, name];
 1958            }
 131959        }
 1960
 1961        /// <summary>
 1962        /// Marks the played.
 1963        /// </summary>
 1964        /// <param name="user">The user.</param>
 1965        /// <param name="datePlayed">The date played.</param>
 1966        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1967        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1968        public virtual void MarkPlayed(
 1969            User user,
 1970            DateTime? datePlayed,
 1971            bool resetPosition)
 1972        {
 01973            ArgumentNullException.ThrowIfNull(user);
 1974
 01975            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 01976            {
 01977                Key = GetUserDataKeys().First(),
 01978            };
 1979
 01980            if (datePlayed.HasValue)
 1981            {
 1982                // Increment
 01983                data.PlayCount++;
 1984            }
 1985
 1986            // Ensure it's at least one
 01987            data.PlayCount = Math.Max(data.PlayCount, 1);
 1988
 01989            if (resetPosition)
 1990            {
 01991                data.PlaybackPositionTicks = 0;
 1992            }
 1993
 01994            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 01995            data.Played = true;
 1996
 01997            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01998        }
 1999
 2000        /// <summary>
 2001        /// Marks the unplayed.
 2002        /// </summary>
 2003        /// <param name="user">The user.</param>
 2004        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2005        public virtual void MarkUnplayed(User user)
 2006        {
 02007            ArgumentNullException.ThrowIfNull(user);
 2008
 02009            var data = UserDataManager.GetUserData(user, this);
 2010
 2011            // I think it is okay to do this here.
 2012            // if this is only called when a user is manually forcing something to un-played
 2013            // then it probably is what we want to do...
 02014            data.PlayCount = 0;
 02015            data.PlaybackPositionTicks = 0;
 02016            data.LastPlayedDate = null;
 02017            data.Played = false;
 2018
 02019            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02020        }
 2021
 2022        /// <summary>
 2023        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2024        /// </summary>
 2025        public virtual void ChangedExternally()
 2026        {
 02027            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02028        }
 2029
 2030        /// <summary>
 2031        /// Gets an image.
 2032        /// </summary>
 2033        /// <param name="type">The type.</param>
 2034        /// <param name="imageIndex">Index of the image.</param>
 2035        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2036        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2037        public bool HasImage(ImageType type, int imageIndex)
 2038        {
 1432039            return GetImageInfo(type, imageIndex) is not null;
 2040        }
 2041
 2042        public void SetImage(ItemImageInfo image, int index)
 2043        {
 212044            if (image.Type == ImageType.Chapter)
 2045            {
 02046                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2047            }
 2048
 212049            var existingImage = GetImageInfo(image.Type, index);
 2050
 212051            if (existingImage is null)
 2052            {
 192053                AddImage(image);
 2054            }
 2055            else
 2056            {
 22057                existingImage.Path = image.Path;
 22058                existingImage.DateModified = image.DateModified;
 22059                existingImage.Width = image.Width;
 22060                existingImage.Height = image.Height;
 22061                existingImage.BlurHash = image.BlurHash;
 2062            }
 22063        }
 2064
 2065        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2066        {
 462067            if (type == ImageType.Chapter)
 2068            {
 02069                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2070            }
 2071
 462072            var image = GetImageInfo(type, index);
 2073
 462074            if (image is null)
 2075            {
 452076                AddImage(GetImageInfo(file, type));
 2077            }
 2078            else
 2079            {
 12080                var imageInfo = GetImageInfo(file, type);
 2081
 12082                image.Path = file.FullName;
 12083                image.DateModified = imageInfo.DateModified;
 2084
 2085                // reset these values
 12086                image.Width = 0;
 12087                image.Height = 0;
 2088            }
 12089        }
 2090
 2091        /// <summary>
 2092        /// Deletes the image.
 2093        /// </summary>
 2094        /// <param name="type">The type.</param>
 2095        /// <param name="index">The index.</param>
 2096        /// <returns>A task.</returns>
 2097        public async Task DeleteImageAsync(ImageType type, int index)
 2098        {
 02099            var info = GetImageInfo(type, index);
 2100
 02101            if (info is null)
 2102            {
 2103                // Nothing to do
 02104                return;
 2105            }
 2106
 2107            // Remove from file system
 02108            var path = info.Path;
 02109            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2110            {
 02111                FileSystem.DeleteFile(path);
 2112            }
 2113
 2114            // Remove from item
 02115            RemoveImage(info);
 2116
 02117            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02118        }
 2119
 2120        public void RemoveImage(ItemImageInfo image)
 2121        {
 02122            RemoveImages([image]);
 02123        }
 2124
 2125        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2126        {
 92127            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92128        }
 2129
 2130        public void AddImage(ItemImageInfo image)
 2131        {
 642132            ImageInfos = [.. ImageInfos, image];
 642133        }
 2134
 2135        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1092136         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2137
 2138        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 332139            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2140
 2141        /// <summary>
 2142        /// Validates that images within the item are still on the filesystem.
 2143        /// </summary>
 2144        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2145        public bool ValidateImages()
 2146        {
 732147            List<ItemImageInfo> deletedImages = null;
 1762148            foreach (var imageInfo in ImageInfos)
 2149            {
 152150                if (!imageInfo.IsLocalFile)
 2151                {
 2152                    continue;
 2153                }
 2154
 152155                if (File.Exists(imageInfo.Path))
 2156                {
 2157                    continue;
 2158                }
 2159
 32160                (deletedImages ??= []).Add(imageInfo);
 2161            }
 2162
 732163            var anyImagesRemoved = deletedImages?.Count > 0;
 732164            if (anyImagesRemoved)
 2165            {
 22166                RemoveImages(deletedImages);
 2167            }
 2168
 732169            return anyImagesRemoved;
 2170        }
 2171
 2172        /// <summary>
 2173        /// Gets the image path.
 2174        /// </summary>
 2175        /// <param name="imageType">Type of the image.</param>
 2176        /// <param name="imageIndex">Index of the image.</param>
 2177        /// <returns>System.String.</returns>
 2178        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2179        public string GetImagePath(ImageType imageType, int imageIndex)
 22180            => GetImageInfo(imageType, imageIndex)?.Path;
 2181
 2182        /// <summary>
 2183        /// Gets the image information.
 2184        /// </summary>
 2185        /// <param name="imageType">Type of the image.</param>
 2186        /// <param name="imageIndex">Index of the image.</param>
 2187        /// <returns>ItemImageInfo.</returns>
 2188        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2189        {
 3172190            if (imageType == ImageType.Chapter)
 2191            {
 02192                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2193
 02194                if (chapter is null)
 2195                {
 02196                    return null;
 2197                }
 2198
 02199                var path = chapter.ImagePath;
 2200
 02201                if (string.IsNullOrEmpty(path))
 2202                {
 02203                    return null;
 2204                }
 2205
 02206                return new ItemImageInfo
 02207                {
 02208                    Path = path,
 02209                    DateModified = chapter.ImageDateModified,
 02210                    Type = imageType
 02211                };
 2212            }
 2213
 3172214            return GetImages(imageType)
 3172215                .ElementAtOrDefault(imageIndex);
 2216        }
 2217
 2218        /// <summary>
 2219        /// Computes image index for given image or raises if no matching image found.
 2220        /// </summary>
 2221        /// <param name="image">Image to compute index for.</param>
 2222        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2223        /// </exception>
 2224        /// <returns>Image index.</returns>
 2225        public int GetImageIndex(ItemImageInfo image)
 2226        {
 02227            ArgumentNullException.ThrowIfNull(image);
 2228
 02229            if (image.Type == ImageType.Chapter)
 2230            {
 02231                var chapters = ChapterManager.GetChapters(Id);
 02232                for (var i = 0; i < chapters.Count; i++)
 2233                {
 02234                    if (chapters[i].ImagePath == image.Path)
 2235                    {
 02236                        return i;
 2237                    }
 2238                }
 2239
 02240                throw new ArgumentException("No chapter index found for image path", image.Path);
 2241            }
 2242
 02243            var images = GetImages(image.Type).ToArray();
 02244            for (var i = 0; i < images.Length; i++)
 2245            {
 02246                if (images[i].Path == image.Path)
 2247                {
 02248                    return i;
 2249                }
 2250            }
 2251
 02252            throw new ArgumentException("No image index found for image path", image.Path);
 2253        }
 2254
 2255        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2256        {
 4232257            if (imageType == ImageType.Chapter)
 2258            {
 02259                throw new ArgumentException("No image info for chapter images");
 2260            }
 2261
 2262            // Yield return is more performant than LINQ Where on an Array
 13982263            for (var i = 0; i < ImageInfos.Length; i++)
 2264            {
 3022265                var imageInfo = ImageInfos[i];
 3022266                if (imageInfo.Type == imageType)
 2267                {
 1582268                    yield return imageInfo;
 2269                }
 2270            }
 3972271        }
 2272
 2273        /// <summary>
 2274        /// Adds the images, updating metadata if they already are part of this item.
 2275        /// </summary>
 2276        /// <param name="imageType">Type of the image.</param>
 2277        /// <param name="images">The images.</param>
 2278        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2279        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2280        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2281        {
 42282            if (imageType == ImageType.Chapter)
 2283            {
 02284                throw new ArgumentException("Cannot call AddImages with chapter images");
 2285            }
 2286
 42287            var existingImages = GetImages(imageType)
 42288                .ToList();
 2289
 42290            var newImageList = new List<FileSystemMetadata>();
 42291            var imageUpdated = false;
 2292
 242293            foreach (var newImage in images)
 2294            {
 82295                if (newImage is null)
 2296                {
 02297                    throw new ArgumentException("null image found in list");
 2298                }
 2299
 82300                var existing = existingImages
 82301                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2302
 82303                if (existing is null)
 2304                {
 42305                    newImageList.Add(newImage);
 2306                }
 2307                else
 2308                {
 42309                    if (existing.IsLocalFile)
 2310                    {
 42311                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2312
 2313                        // If date changed then we need to reset saved image dimensions
 42314                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2315                        {
 22316                            existing.Width = 0;
 22317                            existing.Height = 0;
 22318                            imageUpdated = true;
 2319                        }
 2320
 42321                        existing.DateModified = newDateModified;
 2322                    }
 2323                }
 2324            }
 2325
 42326            if (newImageList.Count > 0)
 2327            {
 22328                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2329            }
 2330
 42331            return imageUpdated || newImageList.Count > 0;
 2332        }
 2333
 2334        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2335        {
 502336            return new ItemImageInfo
 502337            {
 502338                Path = file.FullName,
 502339                Type = type,
 502340                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502341            };
 2342        }
 2343
 2344        /// <summary>
 2345        /// Gets the file system path to delete when the item is to be deleted.
 2346        /// </summary>
 2347        /// <returns>The metadata for the deleted paths.</returns>
 2348        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2349        {
 02350            return new[]
 02351            {
 02352                FileSystem.GetFileSystemInfo(Path)
 02353            }.Concat(GetLocalMetadataFilesToDelete());
 2354        }
 2355
 2356        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2357        {
 02358            if (IsFolder || !IsInMixedFolder)
 2359            {
 02360                return [];
 2361            }
 2362
 02363            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2364
 02365            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02366                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02367                .ToList();
 2368        }
 2369
 2370        public bool AllowsMultipleImages(ImageType type)
 2371        {
 212372            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2373        }
 2374
 2375        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2376        {
 02377            if (!AllowsMultipleImages(type))
 2378            {
 02379                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2380            }
 2381
 02382            var info1 = GetImageInfo(type, index1);
 02383            var info2 = GetImageInfo(type, index2);
 2384
 02385            if (info1 is null || info2 is null)
 2386            {
 2387                // Nothing to do
 02388                return Task.CompletedTask;
 2389            }
 2390
 02391            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2392            {
 2393                // TODO: Not supported  yet
 02394                return Task.CompletedTask;
 2395            }
 2396
 02397            var path1 = info1.Path;
 02398            var path2 = info2.Path;
 2399
 02400            FileSystem.SwapFiles(path1, path2);
 2401
 2402            // Refresh these values
 02403            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02404            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2405
 02406            info1.Width = 0;
 02407            info1.Height = 0;
 02408            info2.Width = 0;
 02409            info2.Height = 0;
 2410
 02411            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2412        }
 2413
 2414        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2415        {
 02416            userItemData ??= UserDataManager.GetUserData(user, this);
 2417
 02418            return userItemData is not null && userItemData.Played;
 2419        }
 2420
 2421        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2422        {
 02423            userItemData ??= UserDataManager.GetUserData(user, this);
 2424
 02425            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2426        }
 2427
 2428        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2429        {
 02430            ArgumentNullException.ThrowIfNull(user);
 2431
 02432            userItemData ??= UserDataManager.GetUserData(user, this);
 2433
 02434            return userItemData is null || !userItemData.Played;
 2435        }
 2436
 2437        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2438        {
 342439            return GetItemLookupInfo<ItemLookupInfo>();
 2440        }
 2441
 2442        protected T GetItemLookupInfo<T>()
 2443            where T : ItemLookupInfo, new()
 2444        {
 342445            return new T
 342446            {
 342447                Path = Path,
 342448                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 342449                MetadataLanguage = GetPreferredMetadataLanguage(),
 342450                Name = GetNameForMetadataLookup(),
 342451                OriginalTitle = OriginalTitle,
 342452                ProviderIds = ProviderIds,
 342453                IndexNumber = IndexNumber,
 342454                ParentIndexNumber = ParentIndexNumber,
 342455                Year = ProductionYear,
 342456                PremiereDate = PremiereDate
 342457            };
 2458        }
 2459
 2460        protected virtual string GetNameForMetadataLookup()
 2461        {
 342462            return Name;
 2463        }
 2464
 2465        /// <summary>
 2466        /// This is called before any metadata refresh and returns true if changes were made.
 2467        /// </summary>
 2468        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2469        /// <returns>true if the item has change, else false.</returns>
 2470        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2471        {
 342472            _sortName = null;
 2473
 342474            var hasChanges = false;
 2475
 342476            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2477            {
 02478                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02479                hasChanges = true;
 2480            }
 2481
 342482            return hasChanges;
 2483        }
 2484
 2485        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2486        {
 02487            if (protocol == MediaProtocol.File)
 2488            {
 02489                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2490            }
 2491
 02492            return path;
 2493        }
 2494
 2495        public virtual void FillUserDataDtoValues(
 2496            UserItemDataDto dto,
 2497            UserItemData userData,
 2498            BaseItemDto itemDto,
 2499            User user,
 2500            DtoOptions fields,
 2501            (int Played, int Total)? precomputedCounts = null)
 2502        {
 02503            if (RunTimeTicks.HasValue)
 2504            {
 02505                double pct = RunTimeTicks.Value;
 2506
 02507                if (pct > 0)
 2508                {
 02509                    pct = userData.PlaybackPositionTicks / pct;
 2510
 02511                    if (pct > 0)
 2512                    {
 02513                        dto.PlayedPercentage = 100 * pct;
 2514                    }
 2515                }
 2516            }
 02517        }
 2518
 2519        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2520        {
 02521            var newOptions = new MetadataRefreshOptions(options)
 02522            {
 02523                SearchResult = null
 02524            };
 2525
 02526            var item = this;
 2527
 02528            if (copyTitleMetadata)
 2529            {
 2530                // Take some data from the main item, for querying purposes
 02531                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2532                {
 02533                    newOptions.ForceSave = true;
 02534                    ownedItem.Genres = item.Genres;
 2535                }
 2536
 02537                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2538                {
 02539                    newOptions.ForceSave = true;
 02540                    ownedItem.Studios = item.Studios;
 2541                }
 2542
 02543                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2544                {
 02545                    newOptions.ForceSave = true;
 02546                    ownedItem.ProductionLocations = item.ProductionLocations;
 2547                }
 2548
 02549                if (item.CommunityRating != ownedItem.CommunityRating)
 2550                {
 02551                    ownedItem.CommunityRating = item.CommunityRating;
 02552                    newOptions.ForceSave = true;
 2553                }
 2554
 02555                if (item.CriticRating != ownedItem.CriticRating)
 2556                {
 02557                    ownedItem.CriticRating = item.CriticRating;
 02558                    newOptions.ForceSave = true;
 2559                }
 2560
 02561                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2562                {
 02563                    ownedItem.Overview = item.Overview;
 02564                    newOptions.ForceSave = true;
 2565                }
 2566
 02567                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2568                {
 02569                    ownedItem.OfficialRating = item.OfficialRating;
 02570                    newOptions.ForceSave = true;
 2571                }
 2572
 02573                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2574                {
 02575                    ownedItem.CustomRating = item.CustomRating;
 02576                    newOptions.ForceSave = true;
 2577                }
 2578            }
 2579
 02580            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02581        }
 2582
 2583        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2584        {
 02585            var newOptions = new MetadataRefreshOptions(options)
 02586            {
 02587                SearchResult = null
 02588            };
 2589
 02590            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2591
 2592            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02593            if (LibraryManager.GetItemById(id) is not Video video)
 2594            {
 02595                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2596
 02597                newOptions.ForceSave = true;
 2598            }
 2599
 02600            if (video is null)
 2601            {
 02602                return;
 2603            }
 2604
 02605            if (video.OwnerId.IsEmpty())
 2606            {
 02607                video.OwnerId = Id;
 2608            }
 2609
 02610            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02611        }
 2612
 2613        public string GetEtag(User user)
 2614        {
 62615            var list = GetEtagValues(user);
 2616
 62617            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2618        }
 2619
 2620        protected virtual List<string> GetEtagValues(User user)
 2621        {
 62622            return
 62623            [
 62624                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62625            ];
 2626        }
 2627
 2628        public virtual IEnumerable<Guid> GetAncestorIds()
 2629        {
 1112630            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2631        }
 2632
 2633        public BaseItem GetTopParent()
 2634        {
 1942635            if (IsTopParent)
 2636            {
 212637                return this;
 2638            }
 2639
 1732640            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2641        }
 2642
 2643        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2644        {
 442645            return [Id];
 2646        }
 2647
 2648        public virtual double? GetRefreshProgress()
 2649        {
 02650            return null;
 2651        }
 2652
 2653        public virtual ItemUpdateType OnMetadataChanged()
 2654        {
 1182655            var updateType = ItemUpdateType.None;
 2656
 1182657            var item = this;
 2658
 1182659            var rating = item.GetParentalRatingScore();
 1182660            if (rating is not null)
 2661            {
 02662                if (rating.Score != item.InheritedParentalRatingValue)
 2663                {
 02664                    item.InheritedParentalRatingValue = rating.Score;
 02665                    updateType |= ItemUpdateType.MetadataImport;
 2666                }
 2667
 02668                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2669                {
 02670                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02671                    updateType |= ItemUpdateType.MetadataImport;
 2672                }
 2673            }
 2674            else
 2675            {
 1182676                if (item.InheritedParentalRatingValue is not null)
 2677                {
 02678                    item.InheritedParentalRatingValue = null;
 02679                    item.InheritedParentalRatingSubValue = null;
 02680                    updateType |= ItemUpdateType.MetadataImport;
 2681                }
 2682            }
 2683
 1182684            return updateType;
 2685        }
 2686
 2687        /// <summary>
 2688        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2689        /// </summary>
 2690        /// <param name="children">Media children.</param>
 2691        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2692        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2693        {
 02694            var currentOfficialRating = OfficialRating;
 2695
 2696            // Gather all possible ratings
 02697            var ratings = children
 02698                .Select(i => i.OfficialRating)
 02699                .Where(i => !string.IsNullOrEmpty(i))
 02700                .Distinct(StringComparer.OrdinalIgnoreCase)
 02701                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 02702                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02703                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02704                .Select(i => i.rating);
 2705
 02706            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2707
 02708            return !string.Equals(
 02709                currentOfficialRating ?? string.Empty,
 02710                OfficialRating ?? string.Empty,
 02711                StringComparison.OrdinalIgnoreCase);
 2712        }
 2713
 2714        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2715        {
 02716            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2717        }
 2718
 2719        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2720        {
 02721            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, 
 2722        }
 2723
 2724        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2725        {
 02726            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2727        }
 2728
 2729        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2730        {
 02731            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user,
 2732        }
 2733
 2734        /// <summary>
 2735        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2736        /// </summary>
 2737        /// <returns>An enumerable containing the items.</returns>
 2738        public IEnumerable<BaseItem> GetExtras()
 2739        {
 62740            return LibraryManager.GetItemList(new InternalItemsQuery()
 62741            {
 62742                OwnerIds = [Id],
 62743                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 62744            });
 2745        }
 2746
 2747        /// <summary>
 2748        /// Get all extras with specific types that are associated with this item.
 2749        /// </summary>
 2750        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2751        /// <returns>An enumerable containing the extras.</returns>
 2752        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
 2753        {
 02754            return LibraryManager.GetItemList(new InternalItemsQuery()
 02755            {
 02756                OwnerIds = [Id],
 02757                ExtraTypes = extraTypes.ToArray(),
 02758                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 02759            });
 2760        }
 2761
 2762        public virtual long GetRunTimeTicksForPlayState()
 2763        {
 02764            return RunTimeTicks ?? 0;
 2765        }
 2766
 2767        /// <inheritdoc />
 2768        public override bool Equals(object obj)
 2769        {
 02770            return obj is BaseItem baseItem && this.Equals(baseItem);
 2771        }
 2772
 2773        /// <inheritdoc />
 1152774        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2775
 2776        /// <inheritdoc />
 1042777        public override int GetHashCode() => HashCode.Combine(Id);
 2778    }
 2779}

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()