< 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: 403
Uncovered lines: 484
Coverable lines: 887
Total lines: 2770
Line coverage: 45.4%
Branch coverage
36%
Covered branches: 202
Total branches: 552
Branch coverage: 36.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 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: 2770 1/23/2026 - 12:11:06 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: 2770

Coverage delta

Coverage delta 4 -4

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_DisplayPreferencesId()50%22100%
get_SourceType()50%2266.66%
get_ContainingFolderPath()100%22100%
get_IsHidden()100%210%
get_LocationType()33.33%11650%
get_PathProtocol()100%22100%
get_IsFileProtocol()100%11100%
get_HasPathProtocol()100%210%
get_SupportsLocalMetadata()50%2266.66%
get_FileNameWithoutExtension()0%620%
get_EnableAlphaNumericSorting()100%11100%
get_IsHD()100%11100%
get_PrimaryImagePath()100%210%
get_MediaType()100%11100%
get_PhysicalLocations()0%620%
get_EnableMediaSourceDisplay()50%2266.66%
get_ForcedSortName()100%11100%
set_ForcedSortName(...)100%11100%
get_SortName()75%4480%
set_SortName(...)100%11100%
get_DisplayParentId()100%11100%
get_DisplayParent()100%22100%
get_HasLocalAlternateVersions()100%210%
get_OfficialRatingForComparison()75%4485.71%
get_CustomRatingForComparison()100%11100%
get_LatestItemsIndexContainer()100%210%
get_EnableRememberingTrackSelections()100%210%
get_IsTopParent()80%121075%
get_SupportsAncestors()100%11100%
get_SupportsOwnedItems()100%22100%
get_SupportsPeople()100%11100%
get_SupportsThemeMedia()100%210%
get_SupportsInheritedParentImages()100%210%
get_IsFolder()100%11100%
get_IsDisplayedAsFolder()100%210%
GetCustomRatingForComparision(...)87.5%8888.88%
GetDefaultPrimaryImageAspectRatio()100%210%
CreatePresentationUniqueKey()100%11100%
CanDelete()0%620%
IsAuthorizedToDelete(...)0%7280%
GetOwner()50%22100%
CanDelete(...)50%22100%
CanDelete(...)100%11100%
CanDownload()100%11100%
IsAuthorizedToDownload(...)100%210%
CanDownload(...)50%22100%
ToString()100%11100%
GetInternalMetadataPath()100%11100%
GetInternalMetadataPath(...)50%2275%
CreateSortName()71.42%171475%
ModifySortChunks(...)87.5%8893.75%
GetParent()100%22100%
GetParents()100%22100%
FindParent()0%2040%
GetPlayAccess(...)50%2266.66%
GetMediaStreams()100%210%
IsActiveRecording()100%210%
GetMediaSources(...)0%7280%
GetAllItemsForMediaSources()100%210%
GetVersionInfo(...)0%2970540%
GetMediaSourceName(...)68.75%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()50%11650%
GetUserDataKeys()50%5466.66%
UpdateFromResolvedItem(...)50%2260%
AfterMetadataRefresh()100%11100%
GetPreferredMetadataLanguage()100%88100%
GetPreferredMetadataCountryCode()100%88100%
IsSaveLocalMetadataEnabled()50%2275%
IsParentalAllowed(...)25%641642.85%
GetParentalRatingScore()75%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(...)100%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()100%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(...)50%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()37.5%22840%
UpdateRatingToItems(...)0%4260%
GetThemeSongs(...)100%210%
GetThemeSongs(...)100%210%
GetThemeVideos(...)100%210%
GetThemeVideos(...)100%210%
GetExtras()100%11100%
GetExtras(...)100%210%
GetRunTimeTicksForPlayState()100%210%
Equals(...)0%620%
Equals(...)50%22100%
GetHashCode()100%11100%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591, SA1401
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Text;
 12using System.Text.Json.Serialization;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Common.Extensions;
 21using MediaBrowser.Controller.Channels;
 22using MediaBrowser.Controller.Chapters;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Dto;
 25using MediaBrowser.Controller.Entities.TV;
 26using MediaBrowser.Controller.IO;
 27using MediaBrowser.Controller.Library;
 28using MediaBrowser.Controller.MediaSegments;
 29using MediaBrowser.Controller.Persistence;
 30using MediaBrowser.Controller.Providers;
 31using MediaBrowser.Model.Dto;
 32using MediaBrowser.Model.Entities;
 33using MediaBrowser.Model.Globalization;
 34using MediaBrowser.Model.IO;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.MediaInfo;
 38using Microsoft.Extensions.Logging;
 39
 40namespace MediaBrowser.Controller.Entities
 41{
 42    /// <summary>
 43    /// Class BaseItem.
 44    /// </summary>
 45    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 46    {
 47        private BaseItemKind? _baseItemKind;
 48
 49        public const string ThemeSongFileName = "theme";
 50
 51        /// <summary>
 52        /// The supported image extensions.
 53        /// </summary>
 454        public static readonly string[] SupportedImageExtensions
 455            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 56
 457        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 458        {
 459            ".nfo",
 460            ".xml",
 461            ".srt",
 462            ".vtt",
 463            ".sub",
 464            ".sup",
 465            ".idx",
 466            ".txt",
 467            ".edl",
 468            ".bif",
 469            ".smi",
 470            ".ttml",
 471            ".lrc",
 472            ".elrc"
 473        };
 74
 75        /// <summary>
 76        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 77        /// </summary>
 478        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 479        {
 480            Model.Entities.ExtraType.Unknown,
 481            Model.Entities.ExtraType.BehindTheScenes,
 482            Model.Entities.ExtraType.Clip,
 483            Model.Entities.ExtraType.DeletedScene,
 484            Model.Entities.ExtraType.Interview,
 485            Model.Entities.ExtraType.Sample,
 486            Model.Entities.ExtraType.Scene,
 487            Model.Entities.ExtraType.Featurette,
 488            Model.Entities.ExtraType.Short
 489        };
 90
 91        private string _sortName;
 92
 93        private string _forcedSortName;
 94
 95        private string _name;
 96
 97        public const char SlugChar = '-';
 98
 99        protected BaseItem()
 100        {
 964101            Tags = Array.Empty<string>();
 964102            Genres = Array.Empty<string>();
 964103            Studios = Array.Empty<string>();
 964104            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 964105            LockedFields = Array.Empty<MetadataField>();
 964106            ImageInfos = Array.Empty<ItemImageInfo>();
 964107            ProductionLocations = Array.Empty<string>();
 964108            RemoteTrailers = Array.Empty<MediaUrl>();
 964109            UserData = [];
 964110        }
 111
 112        /// <summary>
 113        /// Gets or Sets the user data collection as cached from the last Db query.
 114        /// </summary>
 115        [JsonIgnore]
 116        public ICollection<UserData> UserData { get; set; }
 117
 118        [JsonIgnore]
 119        public string PreferredMetadataCountryCode { get; set; }
 120
 121        [JsonIgnore]
 122        public string PreferredMetadataLanguage { get; set; }
 123
 124        public long? Size { get; set; }
 125
 126        public string Container { get; set; }
 127
 128        [JsonIgnore]
 129        public string Tagline { get; set; }
 130
 131        [JsonIgnore]
 132        public virtual ItemImageInfo[] ImageInfos { get; set; }
 133
 134        [JsonIgnore]
 135        public bool IsVirtualItem { get; set; }
 136
 137        /// <summary>
 138        /// Gets or sets the album.
 139        /// </summary>
 140        /// <value>The album.</value>
 141        [JsonIgnore]
 142        public string Album { get; set; }
 143
 144        /// <summary>
 145        /// Gets or sets the LUFS value.
 146        /// </summary>
 147        /// <value>The LUFS Value.</value>
 148        [JsonIgnore]
 149        public float? LUFS { get; set; }
 150
 151        /// <summary>
 152        /// Gets or sets the gain required for audio normalization.
 153        /// </summary>
 154        /// <value>The gain required for audio normalization.</value>
 155        [JsonIgnore]
 156        public float? NormalizationGain { get; set; }
 157
 158        /// <summary>
 159        /// Gets or sets the channel identifier.
 160        /// </summary>
 161        /// <value>The channel identifier.</value>
 162        [JsonIgnore]
 163        public Guid ChannelId { get; set; }
 164
 165        [JsonIgnore]
 0166        public virtual bool SupportsAddingToPlaylist => false;
 167
 168        [JsonIgnore]
 14169        public virtual bool AlwaysScanInternalMetadataPath => false;
 170
 171        /// <summary>
 172        /// Gets or sets a value indicating whether this instance is in mixed folder.
 173        /// </summary>
 174        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 175        [JsonIgnore]
 176        public bool IsInMixedFolder { get; set; }
 177
 178        [JsonIgnore]
 0179        public virtual bool SupportsPlayedStatus => false;
 180
 181        [JsonIgnore]
 0182        public virtual bool SupportsPositionTicksResume => false;
 183
 184        [JsonIgnore]
 17185        public virtual bool SupportsRemoteImageDownloading => true;
 186
 187        /// <summary>
 188        /// Gets or sets the name.
 189        /// </summary>
 190        /// <value>The name.</value>
 191        [JsonIgnore]
 192        public virtual string Name
 193        {
 967194            get => _name;
 195            set
 196            {
 352197                _name = value;
 198
 199                // lazy load this again
 352200                _sortName = null;
 352201            }
 202        }
 203
 204        [JsonIgnore]
 0205        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 206
 207        [JsonIgnore]
 208        public int? TotalBitrate { get; set; }
 209
 210        [JsonIgnore]
 211        public ExtraType? ExtraType { get; set; }
 212
 213        [JsonIgnore]
 0214        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 215
 216        [JsonIgnore]
 217        public string OriginalTitle { get; set; }
 218
 219        /// <summary>
 220        /// Gets or sets the id.
 221        /// </summary>
 222        /// <value>The id.</value>
 223        [JsonIgnore]
 224        public Guid Id { get; set; }
 225
 226        [JsonIgnore]
 227        public Guid OwnerId { get; set; }
 228
 229        /// <summary>
 230        /// Gets or sets the audio.
 231        /// </summary>
 232        /// <value>The audio.</value>
 233        [JsonIgnore]
 234        public ProgramAudio? Audio { get; set; }
 235
 236        /// <summary>
 237        /// Gets the id that should be used to key display prefs for this item.
 238        /// Default is based on the type for everything except actual generic folders.
 239        /// </summary>
 240        /// <value>The display prefs id.</value>
 241        [JsonIgnore]
 242        public virtual Guid DisplayPreferencesId
 243        {
 244            get
 245            {
 6246                var thisType = GetType();
 6247                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 248            }
 249        }
 250
 251        /// <summary>
 252        /// Gets or sets the path.
 253        /// </summary>
 254        /// <value>The path.</value>
 255        [JsonIgnore]
 256        public virtual string Path { get; set; }
 257
 258        [JsonIgnore]
 259        public virtual SourceType SourceType
 260        {
 261            get
 262            {
 2075263                if (!ChannelId.IsEmpty())
 264                {
 0265                    return SourceType.Channel;
 266                }
 267
 2075268                return SourceType.Library;
 269            }
 270        }
 271
 272        /// <summary>
 273        /// Gets the folder containing the item.
 274        /// If the item is a folder, it returns the folder itself.
 275        /// </summary>
 276        [JsonIgnore]
 277        public virtual string ContainingFolderPath
 278        {
 279            get
 280            {
 437281                if (IsFolder)
 282                {
 378283                    return Path;
 284                }
 285
 59286                return System.IO.Path.GetDirectoryName(Path);
 287            }
 288        }
 289
 290        /// <summary>
 291        /// Gets or sets the name of the service.
 292        /// </summary>
 293        /// <value>The name of the service.</value>
 294        [JsonIgnore]
 295        public string ServiceName { get; set; }
 296
 297        /// <summary>
 298        /// Gets or sets the external id.
 299        /// </summary>
 300        /// <remarks>
 301        /// If this content came from an external service, the id of the content on that service.
 302        /// </remarks>
 303        [JsonIgnore]
 304        public string ExternalId { get; set; }
 305
 306        [JsonIgnore]
 307        public string ExternalSeriesId { get; set; }
 308
 309        [JsonIgnore]
 0310        public virtual bool IsHidden => false;
 311
 312        /// <summary>
 313        /// Gets the type of the location.
 314        /// </summary>
 315        /// <value>The type of the location.</value>
 316        [JsonIgnore]
 317        public virtual LocationType LocationType
 318        {
 319            get
 320            {
 9321                var path = Path;
 9322                if (string.IsNullOrEmpty(path))
 323                {
 0324                    if (SourceType == SourceType.Channel)
 325                    {
 0326                        return LocationType.Remote;
 327                    }
 328
 0329                    return LocationType.Virtual;
 330                }
 331
 9332                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 333            }
 334        }
 335
 336        [JsonIgnore]
 337        public MediaProtocol? PathProtocol
 338        {
 339            get
 340            {
 2441341                var path = Path;
 342
 2441343                if (string.IsNullOrEmpty(path))
 344                {
 116345                    return null;
 346                }
 347
 2325348                return MediaSourceManager.GetPathProtocol(path);
 349            }
 350        }
 351
 352        [JsonIgnore]
 2420353        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 354
 355        [JsonIgnore]
 0356        public bool HasPathProtocol => PathProtocol.HasValue;
 357
 358        [JsonIgnore]
 359        public virtual bool SupportsLocalMetadata
 360        {
 361            get
 362            {
 1285363                if (SourceType == SourceType.Channel)
 364                {
 0365                    return false;
 366                }
 367
 1285368                return IsFileProtocol;
 369            }
 370        }
 371
 372        [JsonIgnore]
 373        public virtual string FileNameWithoutExtension
 374        {
 375            get
 376            {
 0377                if (IsFileProtocol)
 378                {
 0379                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 380                }
 381
 0382                return null;
 383            }
 384        }
 385
 386        [JsonIgnore]
 100387        public virtual bool EnableAlphaNumericSorting => true;
 388
 139389        public virtual bool IsHD => Height >= 720;
 390
 391        public bool IsShortcut { get; set; }
 392
 393        public string ShortcutPath { get; set; }
 394
 395        public int Width { get; set; }
 396
 397        public int Height { get; set; }
 398
 399        /// <summary>
 400        /// Gets the primary image path.
 401        /// </summary>
 402        /// <remarks>
 403        /// This is just a helper for convenience.
 404        /// </remarks>
 405        /// <value>The primary image path.</value>
 406        [JsonIgnore]
 0407        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 408
 409        /// <summary>
 410        /// Gets or sets the date created.
 411        /// </summary>
 412        /// <value>The date created.</value>
 413        [JsonIgnore]
 414        public DateTime DateCreated { get; set; }
 415
 416        /// <summary>
 417        /// Gets or sets the date modified.
 418        /// </summary>
 419        /// <value>The date modified.</value>
 420        [JsonIgnore]
 421        public DateTime DateModified { get; set; }
 422
 423        public DateTime DateLastSaved { get; set; }
 424
 425        [JsonIgnore]
 426        public DateTime DateLastRefreshed { get; set; }
 427
 428        [JsonIgnore]
 429        public bool IsLocked { get; set; }
 430
 431        /// <summary>
 432        /// Gets or sets the locked fields.
 433        /// </summary>
 434        /// <value>The locked fields.</value>
 435        [JsonIgnore]
 436        public MetadataField[] LockedFields { get; set; }
 437
 438        /// <summary>
 439        /// Gets the type of the media.
 440        /// </summary>
 441        /// <value>The type of the media.</value>
 442        [JsonIgnore]
 130443        public virtual MediaType MediaType => MediaType.Unknown;
 444
 445        [JsonIgnore]
 446        public virtual string[] PhysicalLocations
 447        {
 448            get
 449            {
 0450                if (!IsFileProtocol)
 451                {
 0452                    return Array.Empty<string>();
 453                }
 454
 0455                return [Path];
 456            }
 457        }
 458
 459        [JsonIgnore]
 460        public bool EnableMediaSourceDisplay
 461        {
 462            get
 463            {
 6464                if (SourceType == SourceType.Channel)
 465                {
 0466                    return ChannelManager.EnableMediaSourceDisplay(this);
 467                }
 468
 6469                return true;
 470            }
 471        }
 472
 473        [JsonIgnore]
 474        public Guid ParentId { get; set; }
 475
 476        /// <summary>
 477        /// Gets or sets the logger.
 478        /// </summary>
 479        public static ILogger<BaseItem> Logger { get; set; }
 480
 481        public static ILibraryManager LibraryManager { get; set; }
 482
 483        public static IServerConfigurationManager ConfigurationManager { get; set; }
 484
 485        public static IProviderManager ProviderManager { get; set; }
 486
 487        public static ILocalizationManager LocalizationManager { get; set; }
 488
 489        public static IItemRepository ItemRepository { get; set; }
 490
 491        public static IItemCountService ItemCountService { get; set; }
 492
 493        public static IChapterManager ChapterManager { get; set; }
 494
 495        public static IFileSystem FileSystem { get; set; }
 496
 497        public static IUserDataManager UserDataManager { get; set; }
 498
 499        public static IChannelManager ChannelManager { get; set; }
 500
 501        public static IMediaSourceManager MediaSourceManager { get; set; }
 502
 503        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 504
 505        /// <summary>
 506        /// Gets or sets the name of the forced sort.
 507        /// </summary>
 508        /// <value>The name of the forced sort.</value>
 509        [JsonIgnore]
 510        public string ForcedSortName
 511        {
 478512            get => _forcedSortName;
 513            set
 514            {
 89515                _forcedSortName = value;
 89516                _sortName = null;
 89517            }
 518        }
 519
 520        /// <summary>
 521        /// Gets or sets the name of the sort.
 522        /// </summary>
 523        /// <value>The name of the sort.</value>
 524        [JsonIgnore]
 525        public string SortName
 526        {
 527            get
 528            {
 170529                if (_sortName is null)
 530                {
 100531                    if (!string.IsNullOrEmpty(ForcedSortName))
 532                    {
 533                        // Need the ToLower because that's what CreateSortName does
 0534                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 535                    }
 536                    else
 537                    {
 100538                        _sortName = CreateSortName();
 539                    }
 540                }
 541
 170542                return _sortName;
 543            }
 544
 128545            set => _sortName = value;
 546        }
 547
 548        [JsonIgnore]
 328549        public virtual Guid DisplayParentId => ParentId;
 550
 551        [JsonIgnore]
 552        public BaseItem DisplayParent
 553        {
 554            get
 555            {
 322556                var id = DisplayParentId;
 322557                if (id.IsEmpty())
 558                {
 246559                    return null;
 560                }
 561
 76562                return LibraryManager.GetItemById(id);
 563            }
 564        }
 565
 566        /// <summary>
 567        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 568        /// </summary>
 569        /// <value>The premiere date.</value>
 570        [JsonIgnore]
 571        public DateTime? PremiereDate { get; set; }
 572
 573        /// <summary>
 574        /// Gets or sets the end date.
 575        /// </summary>
 576        /// <value>The end date.</value>
 577        [JsonIgnore]
 578        public DateTime? EndDate { get; set; }
 579
 580        /// <summary>
 581        /// Gets or sets the official rating.
 582        /// </summary>
 583        /// <value>The official rating.</value>
 584        [JsonIgnore]
 585        public string OfficialRating { get; set; }
 586
 587        [JsonIgnore]
 588        public int? InheritedParentalRatingValue { get; set; }
 589
 590        [JsonIgnore]
 591        public int? InheritedParentalRatingSubValue { get; set; }
 592
 593        /// <summary>
 594        /// Gets or sets the critic rating.
 595        /// </summary>
 596        /// <value>The critic rating.</value>
 597        [JsonIgnore]
 598        public float? CriticRating { get; set; }
 599
 600        /// <summary>
 601        /// Gets or sets the custom rating.
 602        /// </summary>
 603        /// <value>The custom rating.</value>
 604        [JsonIgnore]
 605        public string CustomRating { get; set; }
 606
 607        /// <summary>
 608        /// Gets or sets the overview.
 609        /// </summary>
 610        /// <value>The overview.</value>
 611        [JsonIgnore]
 612        public string Overview { get; set; }
 613
 614        /// <summary>
 615        /// Gets or sets the studios.
 616        /// </summary>
 617        /// <value>The studios.</value>
 618        [JsonIgnore]
 619        public string[] Studios { get; set; }
 620
 621        /// <summary>
 622        /// Gets or sets the genres.
 623        /// </summary>
 624        /// <value>The genres.</value>
 625        [JsonIgnore]
 626        public string[] Genres { get; set; }
 627
 628        /// <summary>
 629        /// Gets or sets the tags.
 630        /// </summary>
 631        /// <value>The tags.</value>
 632        [JsonIgnore]
 633        public string[] Tags { get; set; }
 634
 635        [JsonIgnore]
 636        public string[] ProductionLocations { get; set; }
 637
 638        /// <summary>
 639        /// Gets or sets the home page URL.
 640        /// </summary>
 641        /// <value>The home page URL.</value>
 642        [JsonIgnore]
 643        public string HomePageUrl { get; set; }
 644
 645        /// <summary>
 646        /// Gets or sets the community rating.
 647        /// </summary>
 648        /// <value>The community rating.</value>
 649        [JsonIgnore]
 650        public float? CommunityRating { get; set; }
 651
 652        /// <summary>
 653        /// Gets or sets the run time ticks.
 654        /// </summary>
 655        /// <value>The run time ticks.</value>
 656        [JsonIgnore]
 657        public long? RunTimeTicks { get; set; }
 658
 659        /// <summary>
 660        /// Gets or sets the production year.
 661        /// </summary>
 662        /// <value>The production year.</value>
 663        [JsonIgnore]
 664        public int? ProductionYear { get; set; }
 665
 666        /// <summary>
 667        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 668        /// This could be episode number, album track number, etc.
 669        /// </summary>
 670        /// <value>The index number.</value>
 671        [JsonIgnore]
 672        public int? IndexNumber { get; set; }
 673
 674        /// <summary>
 675        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 676        /// </summary>
 677        /// <value>The parent index number.</value>
 678        [JsonIgnore]
 679        public int? ParentIndexNumber { get; set; }
 680
 681        [JsonIgnore]
 0682        public virtual bool HasLocalAlternateVersions => false;
 683
 684        [JsonIgnore]
 685        public string OfficialRatingForComparison
 686        {
 687            get
 688            {
 161689                var officialRating = OfficialRating;
 161690                if (!string.IsNullOrEmpty(officialRating))
 691                {
 0692                    return officialRating;
 693                }
 694
 161695                var parent = DisplayParent;
 161696                if (parent is not null)
 697                {
 38698                    return parent.OfficialRatingForComparison;
 699                }
 700
 123701                return null;
 702            }
 703        }
 704
 705        [JsonIgnore]
 706        public string CustomRatingForComparison
 707        {
 708            get
 709            {
 123710                return GetCustomRatingForComparision();
 711            }
 712        }
 713
 714        /// <summary>
 715        /// Gets or sets the provider ids.
 716        /// </summary>
 717        /// <value>The provider ids.</value>
 718        [JsonIgnore]
 719        public Dictionary<string, string> ProviderIds { get; set; }
 720
 721        [JsonIgnore]
 0722        public virtual Folder LatestItemsIndexContainer => null;
 723
 724        [JsonIgnore]
 725        public string PresentationUniqueKey { get; set; }
 726
 727        [JsonIgnore]
 0728        public virtual bool EnableRememberingTrackSelections => true;
 729
 730        [JsonIgnore]
 731        public virtual bool IsTopParent
 732        {
 733            get
 734            {
 345735                if (this is BasePluginFolder || this is Channel)
 736                {
 59737                    return true;
 738                }
 739
 286740                if (this is IHasCollectionType view)
 741                {
 13742                    if (view.CollectionType == CollectionType.livetv)
 743                    {
 0744                        return true;
 745                    }
 746                }
 747
 286748                if (GetParent() is AggregateFolder)
 749                {
 0750                    return true;
 751                }
 752
 286753                return false;
 754            }
 755        }
 756
 757        [JsonIgnore]
 444758        public virtual bool SupportsAncestors => true;
 759
 760        [JsonIgnore]
 92761        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 762
 763        [JsonIgnore]
 69764        public virtual bool SupportsPeople => false;
 765
 766        [JsonIgnore]
 0767        public virtual bool SupportsThemeMedia => false;
 768
 769        [JsonIgnore]
 0770        public virtual bool SupportsInheritedParentImages => false;
 771
 772        /// <summary>
 773        /// Gets a value indicating whether this instance is folder.
 774        /// </summary>
 775        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 776        [JsonIgnore]
 79777        public virtual bool IsFolder => false;
 778
 779        [JsonIgnore]
 0780        public virtual bool IsDisplayedAsFolder => false;
 781
 782        /// <summary>
 783        /// Gets or sets the remote trailers.
 784        /// </summary>
 785        /// <value>The remote trailers.</value>
 786        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 787
 788        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 789        {
 161790            callstack ??= new();
 161791            var customRating = CustomRating;
 161792            if (!string.IsNullOrEmpty(customRating))
 793            {
 0794                return customRating;
 795            }
 796
 161797            callstack.Add(Id);
 798
 161799            var parent = DisplayParent;
 161800            if (parent is not null && !callstack.Contains(parent.Id))
 801            {
 38802                return parent.GetCustomRatingForComparision(callstack);
 803            }
 804
 123805            return null;
 806        }
 807
 808        public virtual double GetDefaultPrimaryImageAspectRatio()
 809        {
 0810            return 0;
 811        }
 812
 813        public virtual string CreatePresentationUniqueKey()
 814        {
 57815            return Id.ToString("N", CultureInfo.InvariantCulture);
 816        }
 817
 818        public virtual bool CanDelete()
 819        {
 0820            if (SourceType == SourceType.Channel)
 821            {
 0822                return ChannelManager.CanDelete(this);
 823            }
 824
 0825            return IsFileProtocol;
 826        }
 827
 828        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 829        {
 0830            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 831            {
 0832                return true;
 833            }
 834
 0835            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 836
 0837            if (SourceType == SourceType.Channel)
 838            {
 0839                return allowed.Contains(ChannelId);
 840            }
 841
 0842            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 843
 0844            foreach (var folder in collectionFolders)
 845            {
 0846                if (allowed.Contains(folder.Id))
 847                {
 0848                    return true;
 849                }
 850            }
 851
 0852            return false;
 0853        }
 854
 855        public BaseItem GetOwner()
 856        {
 626857            var ownerId = OwnerId;
 626858            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 859        }
 860
 861        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 862        {
 6863            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 864        }
 865
 866        public virtual bool CanDelete(User user)
 867        {
 6868            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 869
 6870            return CanDelete(user, allCollectionFolders);
 871        }
 872
 873        public virtual bool CanDownload()
 874        {
 6875            return false;
 876        }
 877
 878        public virtual bool IsAuthorizedToDownload(User user)
 879        {
 0880            return user.HasPermission(PermissionKind.EnableContentDownloading);
 881        }
 882
 883        public bool CanDownload(User user)
 884        {
 6885            return CanDownload() && IsAuthorizedToDownload(user);
 886        }
 887
 888        /// <inheritdoc />
 889        public override string ToString()
 890        {
 69891            return Name;
 892        }
 893
 894        public virtual string GetInternalMetadataPath()
 895        {
 71896            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 897
 71898            return GetInternalMetadataPath(basePath);
 899        }
 900
 901        protected virtual string GetInternalMetadataPath(string basePath)
 902        {
 71903            if (SourceType == SourceType.Channel)
 904            {
 0905                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 906            }
 907
 71908            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 909
 71910            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 911        }
 912
 913        /// <summary>
 914        /// Creates the name of the sort.
 915        /// </summary>
 916        /// <returns>System.String.</returns>
 917        protected virtual string CreateSortName()
 918        {
 100919            if (Name is null)
 920            {
 0921                return null; // some items may not have name filled in properly
 922            }
 923
 100924            if (!EnableAlphaNumericSorting)
 925            {
 0926                return Name.TrimStart();
 927            }
 928
 100929            var sortable = Name.Trim().ToLowerInvariant();
 930
 800931            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 932            {
 933                // Remove from beginning if a space follows
 300934                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 935                {
 0936                    sortable = sortable.Remove(0, search.Length + 1);
 937                }
 938
 939                // Remove from middle if surrounded by spaces
 300940                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 941
 942                // Remove from end if preceeded by a space
 300943                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 944                {
 0945                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 946                }
 947            }
 948
 1400949            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 950            {
 600951                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 952            }
 953
 800954            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 955            {
 300956                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 957            }
 958
 100959            return ModifySortChunks(sortable);
 960        }
 961
 962        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 963        {
 964            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 965            {
 966                if (isDigitChunk && chunk.Length < 10)
 967                {
 968                    builder.Append('0', 10 - chunk.Length);
 969                }
 970
 971                builder.Append(chunk);
 972            }
 973
 106974            if (name.IsEmpty)
 975            {
 1976                return string.Empty;
 977            }
 978
 105979            var builder = new StringBuilder(name.Length);
 980
 105981            int chunkStart = 0;
 105982            bool isDigitChunk = char.IsDigit(name[0]);
 1674983            for (int i = 0; i < name.Length; i++)
 984            {
 732985                var isDigit = char.IsDigit(name[i]);
 732986                if (isDigit != isDigitChunk)
 987                {
 5988                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5989                    chunkStart = i;
 5990                    isDigitChunk = isDigit;
 991                }
 992            }
 993
 105994            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 995
 996            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 105997            var result = builder.ToString().RemoveDiacritics();
 105998            if (!result.All(char.IsAscii))
 999            {
 01000                result = result.Transliterated();
 1001            }
 1002
 1051003            return result;
 1004        }
 1005
 1006        public BaseItem GetParent()
 1007        {
 18821008            var parentId = ParentId;
 18821009            if (parentId.IsEmpty())
 1010            {
 16501011                return null;
 1012            }
 1013
 2321014            return LibraryManager.GetItemById(parentId);
 1015        }
 1016
 1017        public IEnumerable<BaseItem> GetParents()
 1018        {
 6671019            var parent = GetParent();
 1020
 7381021            while (parent is not null)
 1022            {
 711023                yield return parent;
 1024
 711025                parent = parent.GetParent();
 1026            }
 6671027        }
 1028
 1029        /// <summary>
 1030        /// Finds a parent of a given type.
 1031        /// </summary>
 1032        /// <typeparam name="T">Type of parent.</typeparam>
 1033        /// <returns>``0.</returns>
 1034        public T FindParent<T>()
 1035            where T : Folder
 1036        {
 01037            foreach (var parent in GetParents())
 1038            {
 01039                if (parent is T item)
 1040                {
 01041                    return item;
 1042                }
 1043            }
 1044
 01045            return null;
 01046        }
 1047
 1048        /// <summary>
 1049        /// Gets the play access.
 1050        /// </summary>
 1051        /// <param name="user">The user.</param>
 1052        /// <returns>PlayAccess.</returns>
 1053        public PlayAccess GetPlayAccess(User user)
 1054        {
 61055            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1056            {
 01057                return PlayAccess.None;
 1058            }
 1059
 1060            // if (!user.IsParentalScheduleAllowed())
 1061            // {
 1062            //    return PlayAccess.None;
 1063            // }
 1064
 61065            return PlayAccess.Full;
 1066        }
 1067
 1068        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1069        {
 01070            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01071            {
 01072                ItemId = Id
 01073            });
 1074        }
 1075
 1076        protected virtual bool IsActiveRecording()
 1077        {
 01078            return false;
 1079        }
 1080
 1081        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1082        {
 01083            if (SourceType == SourceType.Channel)
 1084            {
 01085                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01086                           .ToList();
 1087
 01088                if (sources.Count > 0)
 1089                {
 01090                    return sources;
 1091                }
 1092            }
 1093
 01094            var list = GetAllItemsForMediaSources();
 01095            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
 1096
 01097            if (IsActiveRecording())
 1098            {
 01099                foreach (var mediaSource in result)
 1100                {
 01101                    mediaSource.Type = MediaSourceType.Placeholder;
 1102                }
 1103            }
 1104
 01105            return result.OrderBy(i =>
 01106            {
 01107                if (i.VideoType == VideoType.VideoFile)
 01108                {
 01109                    return 0;
 01110                }
 01111
 01112                return 1;
 01113            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 01114            .ThenByDescending(i => i, new MediaSourceWidthComparator())
 01115            .ToArray();
 1116        }
 1117
 1118        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1119        {
 01120            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1121        }
 1122
 1123        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
 1124        {
 01125            ArgumentNullException.ThrowIfNull(item);
 1126
 01127            var protocol = item.PathProtocol;
 1128
 1129            // Resolve the item path so everywhere we use the media source it will always point to
 1130            // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
 1131            // path will return null, so it's safe to check for all paths.
 01132            var itemPath = item.Path;
 01133            if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) 
 1134            {
 01135                itemPath = linkInfo.FullName;
 1136            }
 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        {
 501295            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        {
 571306            var requiresSave = false;
 1307
 571308            if (SupportsOwnedItems)
 1309            {
 1310                try
 1311                {
 351312                    if (IsFileProtocol)
 1313                    {
 351314                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1315                    }
 1316
 351317                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 351318                }
 01319                catch (Exception ex)
 1320                {
 01321                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01322                }
 1323            }
 1324
 571325            var refreshOptions = requiresSave
 571326                ? new MetadataRefreshOptions(options)
 571327                {
 571328                    ForceSave = true
 571329                }
 571330                : options;
 1331
 571332            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 561333        }
 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        {
 91393            ParentId = parent is null ? Guid.Empty : parent.Id;
 91394        }
 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        {
 351406            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1407            {
 351408                return false;
 1409            }
 1410
 01411            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 351412        }
 1413
 1414        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1415        {
 411416            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        {
 571484            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1485            {
 571486                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        {
 51512            var updateType = ItemUpdateType.None;
 1513
 51514            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1515            {
 01516                IsInMixedFolder = newItem.IsInMixedFolder;
 01517                updateType |= ItemUpdateType.MetadataImport;
 1518            }
 1519
 51520            return updateType;
 1521        }
 1522
 1523        public void AfterMetadataRefresh()
 1524        {
 561525            _sortName = null;
 561526        }
 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 language.
 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        public virtual bool IsSaveLocalMetadataEnabled()
 1599        {
 861600            if (SourceType == SourceType.Channel)
 1601            {
 01602                return false;
 1603            }
 1604
 861605            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1606
 861607            return libraryOptions.SaveLocalMetadata;
 1608        }
 1609
 1610        /// <summary>
 1611        /// Determines if a given user has access to this item.
 1612        /// </summary>
 1613        /// <param name="user">The user.</param>
 1614        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1615        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1616        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1617        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1618        {
 101619            ArgumentNullException.ThrowIfNull(user);
 1620
 101621            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1622            {
 01623                return false;
 1624            }
 1625
 101626            var maxAllowedRating = user.MaxParentalRatingScore;
 101627            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 101628            var rating = CustomRatingForComparison;
 1629
 101630            if (string.IsNullOrEmpty(rating))
 1631            {
 101632                rating = OfficialRatingForComparison;
 1633            }
 1634
 101635            if (string.IsNullOrEmpty(rating))
 1636            {
 101637                return !GetBlockUnratedValue(user);
 1638            }
 1639
 01640            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1641
 1642            // Could not determine rating level
 01643            if (ratingScore is null)
 1644            {
 01645                var isAllowed = !GetBlockUnratedValue(user);
 1646
 01647                if (!isAllowed)
 1648                {
 01649                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1650                }
 1651
 01652                return isAllowed;
 1653            }
 1654
 01655            if (!maxAllowedRating.HasValue)
 1656            {
 01657                return true;
 1658            }
 1659
 01660            if (ratingScore.Score != maxAllowedRating.Value)
 1661            {
 01662                return ratingScore.Score < maxAllowedRating.Value;
 1663            }
 1664
 01665            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1666        }
 1667
 1668        public ParentalRatingScore GetParentalRatingScore()
 1669        {
 1131670            var rating = CustomRatingForComparison;
 1671
 1131672            if (string.IsNullOrEmpty(rating))
 1673            {
 1131674                rating = OfficialRatingForComparison;
 1675            }
 1676
 1131677            if (string.IsNullOrEmpty(rating))
 1678            {
 1131679                return null;
 1680            }
 1681
 01682            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1683        }
 1684
 1685        public List<string> GetInheritedTags()
 1686        {
 1111687            var list = new List<string>();
 1111688            list.AddRange(Tags);
 1689
 2761690            foreach (var parent in GetParents())
 1691            {
 271692                list.AddRange(parent.Tags);
 1693            }
 1694
 2221695            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1696            {
 01697                list.AddRange(folder.Tags);
 1698            }
 1699
 1111700            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1701        }
 1702
 1703        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1704        {
 101705            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 101706            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 1707
 101708            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 1709            {
 101710                return true;
 1711            }
 1712
 1713            // Normalize tags using the same logic as database queries
 01714            var normalizedBlockedTags = blockedTags
 01715                .Where(t => !string.IsNullOrWhiteSpace(t))
 01716                .Select(t => t.GetCleanValue())
 01717                .ToHashSet(StringComparer.Ordinal);
 1718
 01719            var normalizedItemTags = GetInheritedTags()
 01720                .Select(t => t.GetCleanValue())
 01721                .ToHashSet(StringComparer.Ordinal);
 1722
 1723            // Check blocked tags - item is hidden if it has any blocked tag
 01724            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 1725            {
 01726                return false;
 1727            }
 1728
 01729            var parent = GetParents().FirstOrDefault() ?? this;
 01730            if (parent is UserRootFolder or AggregateFolder or UserView)
 1731            {
 01732                return true;
 1733            }
 1734
 1735            // Check allowed tags - item must have at least one allowed tag
 01736            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 1737            {
 01738                var normalizedAllowedTags = allowedTags
 01739                    .Where(t => !string.IsNullOrWhiteSpace(t))
 01740                    .Select(t => t.GetCleanValue())
 01741                    .ToHashSet(StringComparer.Ordinal);
 1742
 01743                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 1744                {
 01745                    return false;
 1746                }
 1747            }
 1748
 01749            return true;
 1750        }
 1751
 1752        public virtual UnratedItem GetBlockUnratedType()
 1753        {
 1111754            if (SourceType == SourceType.Channel)
 1755            {
 01756                return UnratedItem.ChannelContent;
 1757            }
 1758
 1111759            return UnratedItem.Other;
 1760        }
 1761
 1762        /// <summary>
 1763        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1764        /// </summary>
 1765        /// <param name="user">The configuration.</param>
 1766        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1767        protected virtual bool GetBlockUnratedValue(User user)
 1768        {
 1769            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1770            // Special folders like series and albums will override this method.
 101771            if (IsFolder || this is IItemByName)
 1772            {
 101773                return false;
 1774            }
 1775
 01776            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1777        }
 1778
 1779        /// <summary>
 1780        /// Determines if this folder should be visible to a given user.
 1781        /// Default is just parental allowed. Can be overridden for more functionality.
 1782        /// </summary>
 1783        /// <param name="user">The user.</param>
 1784        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1785        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1786        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1787        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1788        {
 101789            ArgumentNullException.ThrowIfNull(user);
 1790
 101791            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1792        }
 1793
 1794        public virtual bool IsVisibleStandalone(User user)
 1795        {
 01796            if (SourceType == SourceType.Channel)
 1797            {
 01798                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1799            }
 1800
 01801            return IsVisibleStandaloneInternal(user, true);
 1802        }
 1803
 1804        public virtual string GetClientTypeName()
 1805        {
 1101806            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1807            {
 01808                return "ChannelFolderItem";
 1809            }
 1810
 1101811            return GetType().Name;
 1812        }
 1813
 1814        public BaseItemKind GetBaseItemKind()
 1815        {
 2391816            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1817        }
 1818
 1819        /// <summary>
 1820        /// Gets the linked child.
 1821        /// </summary>
 1822        /// <param name="info">The info.</param>
 1823        /// <returns>BaseItem.</returns>
 1824        protected BaseItem GetLinkedChild(LinkedChild info)
 1825        {
 1826            // First get using the cached Id
 01827            if (info.ItemId.HasValue)
 1828            {
 01829                if (info.ItemId.Value.IsEmpty())
 1830                {
 01831                    return null;
 1832                }
 1833
 01834                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1835
 01836                if (itemById is not null)
 1837                {
 01838                    return itemById;
 1839                }
 1840            }
 1841
 01842            var item = FindLinkedChild(info);
 1843
 1844            // If still null, log
 01845            if (item is null)
 1846            {
 1847                // Don't keep searching over and over
 01848                info.ItemId = Guid.Empty;
 1849            }
 1850            else
 1851            {
 1852                // Cache the id for next time
 01853                info.ItemId = item.Id;
 1854            }
 1855
 01856            return item;
 1857        }
 1858
 1859#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 1860        private BaseItem FindLinkedChild(LinkedChild info)
 1861        {
 1862            // First try to find by ItemId (new preferred method)
 01863            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 1864            {
 01865                var item = LibraryManager.GetItemById(info.ItemId.Value);
 01866                if (item is not null)
 1867                {
 01868                    return item;
 1869                }
 1870
 01871                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 1872            }
 1873
 1874            // Fall back to Path (legacy method)
 01875            var path = info.Path;
 01876            if (!string.IsNullOrEmpty(path))
 1877            {
 01878                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 1879
 01880                var itemByPath = LibraryManager.FindByPath(path, null);
 1881
 01882                if (itemByPath is null)
 1883                {
 01884                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1885                }
 1886
 01887                return itemByPath;
 1888            }
 1889
 1890            // Fall back to LibraryItemId (legacy method)
 01891            if (!string.IsNullOrEmpty(info.LibraryItemId))
 1892            {
 01893                var item = LibraryManager.GetItemById(info.LibraryItemId);
 1894
 01895                if (item is null)
 1896                {
 01897                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 1898                }
 1899
 01900                return item;
 1901            }
 1902
 01903            return null;
 1904        }
 1905#pragma warning restore CS0618
 1906
 1907        /// <summary>
 1908        /// Adds a studio to the item.
 1909        /// </summary>
 1910        /// <param name="name">The name.</param>
 1911        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1912        public void AddStudio(string name)
 1913        {
 41914            ArgumentException.ThrowIfNullOrEmpty(name);
 41915            var current = Studios;
 1916
 41917            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 1918            {
 41919                int curLen = current.Length;
 41920                if (curLen == 0)
 1921                {
 41922                    Studios = [name];
 1923                }
 1924                else
 1925                {
 01926                    Studios = [.. current, name];
 1927                }
 1928            }
 01929        }
 1930
 1931        public void SetStudios(IEnumerable<string> names)
 1932        {
 01933            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 01934        }
 1935
 1936        /// <summary>
 1937        /// Adds a genre to the item.
 1938        /// </summary>
 1939        /// <param name="name">The name.</param>
 1940        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1941        public void AddGenre(string name)
 1942        {
 131943            ArgumentException.ThrowIfNullOrEmpty(name);
 1944
 131945            var genres = Genres;
 131946            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 1947            {
 131948                Genres = [.. genres, name];
 1949            }
 131950        }
 1951
 1952        /// <summary>
 1953        /// Marks the played.
 1954        /// </summary>
 1955        /// <param name="user">The user.</param>
 1956        /// <param name="datePlayed">The date played.</param>
 1957        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1958        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1959        public virtual void MarkPlayed(
 1960            User user,
 1961            DateTime? datePlayed,
 1962            bool resetPosition)
 1963        {
 01964            ArgumentNullException.ThrowIfNull(user);
 1965
 01966            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 01967            {
 01968                Key = GetUserDataKeys().First(),
 01969            };
 1970
 01971            if (datePlayed.HasValue)
 1972            {
 1973                // Increment
 01974                data.PlayCount++;
 1975            }
 1976
 1977            // Ensure it's at least one
 01978            data.PlayCount = Math.Max(data.PlayCount, 1);
 1979
 01980            if (resetPosition)
 1981            {
 01982                data.PlaybackPositionTicks = 0;
 1983            }
 1984
 01985            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 01986            data.Played = true;
 1987
 01988            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01989        }
 1990
 1991        /// <summary>
 1992        /// Marks the unplayed.
 1993        /// </summary>
 1994        /// <param name="user">The user.</param>
 1995        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1996        public virtual void MarkUnplayed(User user)
 1997        {
 01998            ArgumentNullException.ThrowIfNull(user);
 1999
 02000            var data = UserDataManager.GetUserData(user, this);
 2001
 2002            // I think it is okay to do this here.
 2003            // if this is only called when a user is manually forcing something to un-played
 2004            // then it probably is what we want to do...
 02005            data.PlayCount = 0;
 02006            data.PlaybackPositionTicks = 0;
 02007            data.LastPlayedDate = null;
 02008            data.Played = false;
 2009
 02010            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02011        }
 2012
 2013        /// <summary>
 2014        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2015        /// </summary>
 2016        public virtual void ChangedExternally()
 2017        {
 02018            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02019        }
 2020
 2021        /// <summary>
 2022        /// Gets an image.
 2023        /// </summary>
 2024        /// <param name="type">The type.</param>
 2025        /// <param name="imageIndex">Index of the image.</param>
 2026        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2027        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2028        public bool HasImage(ImageType type, int imageIndex)
 2029        {
 1432030            return GetImageInfo(type, imageIndex) is not null;
 2031        }
 2032
 2033        public void SetImage(ItemImageInfo image, int index)
 2034        {
 132035            if (image.Type == ImageType.Chapter)
 2036            {
 02037                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2038            }
 2039
 132040            var existingImage = GetImageInfo(image.Type, index);
 2041
 132042            if (existingImage is null)
 2043            {
 112044                AddImage(image);
 2045            }
 2046            else
 2047            {
 22048                existingImage.Path = image.Path;
 22049                existingImage.DateModified = image.DateModified;
 22050                existingImage.Width = image.Width;
 22051                existingImage.Height = image.Height;
 22052                existingImage.BlurHash = image.BlurHash;
 2053            }
 22054        }
 2055
 2056        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2057        {
 462058            if (type == ImageType.Chapter)
 2059            {
 02060                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2061            }
 2062
 462063            var image = GetImageInfo(type, index);
 2064
 462065            if (image is null)
 2066            {
 452067                AddImage(GetImageInfo(file, type));
 2068            }
 2069            else
 2070            {
 12071                var imageInfo = GetImageInfo(file, type);
 2072
 12073                image.Path = file.FullName;
 12074                image.DateModified = imageInfo.DateModified;
 2075
 2076                // reset these values
 12077                image.Width = 0;
 12078                image.Height = 0;
 2079            }
 12080        }
 2081
 2082        /// <summary>
 2083        /// Deletes the image.
 2084        /// </summary>
 2085        /// <param name="type">The type.</param>
 2086        /// <param name="index">The index.</param>
 2087        /// <returns>A task.</returns>
 2088        public async Task DeleteImageAsync(ImageType type, int index)
 2089        {
 02090            var info = GetImageInfo(type, index);
 2091
 02092            if (info is null)
 2093            {
 2094                // Nothing to do
 02095                return;
 2096            }
 2097
 2098            // Remove from file system
 02099            var path = info.Path;
 02100            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2101            {
 02102                FileSystem.DeleteFile(path);
 2103            }
 2104
 2105            // Remove from item
 02106            RemoveImage(info);
 2107
 02108            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02109        }
 2110
 2111        public void RemoveImage(ItemImageInfo image)
 2112        {
 02113            RemoveImages([image]);
 02114        }
 2115
 2116        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2117        {
 92118            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92119        }
 2120
 2121        public void AddImage(ItemImageInfo image)
 2122        {
 562123            ImageInfos = [.. ImageInfos, image];
 562124        }
 2125
 2126        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1092127         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2128
 2129        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 332130            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2131
 2132        /// <summary>
 2133        /// Validates that images within the item are still on the filesystem.
 2134        /// </summary>
 2135        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2136        public bool ValidateImages()
 2137        {
 712138            List<ItemImageInfo> deletedImages = null;
 1722139            foreach (var imageInfo in ImageInfos)
 2140            {
 152141                if (!imageInfo.IsLocalFile)
 2142                {
 2143                    continue;
 2144                }
 2145
 152146                if (File.Exists(imageInfo.Path))
 2147                {
 2148                    continue;
 2149                }
 2150
 32151                (deletedImages ??= []).Add(imageInfo);
 2152            }
 2153
 712154            var anyImagesRemoved = deletedImages?.Count > 0;
 712155            if (anyImagesRemoved)
 2156            {
 22157                RemoveImages(deletedImages);
 2158            }
 2159
 712160            return anyImagesRemoved;
 2161        }
 2162
 2163        /// <summary>
 2164        /// Gets the image path.
 2165        /// </summary>
 2166        /// <param name="imageType">Type of the image.</param>
 2167        /// <param name="imageIndex">Index of the image.</param>
 2168        /// <returns>System.String.</returns>
 2169        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2170        public string GetImagePath(ImageType imageType, int imageIndex)
 22171            => GetImageInfo(imageType, imageIndex)?.Path;
 2172
 2173        /// <summary>
 2174        /// Gets the image information.
 2175        /// </summary>
 2176        /// <param name="imageType">Type of the image.</param>
 2177        /// <param name="imageIndex">Index of the image.</param>
 2178        /// <returns>ItemImageInfo.</returns>
 2179        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2180        {
 2972181            if (imageType == ImageType.Chapter)
 2182            {
 02183                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2184
 02185                if (chapter is null)
 2186                {
 02187                    return null;
 2188                }
 2189
 02190                var path = chapter.ImagePath;
 2191
 02192                if (string.IsNullOrEmpty(path))
 2193                {
 02194                    return null;
 2195                }
 2196
 02197                return new ItemImageInfo
 02198                {
 02199                    Path = path,
 02200                    DateModified = chapter.ImageDateModified,
 02201                    Type = imageType
 02202                };
 2203            }
 2204
 2972205            return GetImages(imageType)
 2972206                .ElementAtOrDefault(imageIndex);
 2207        }
 2208
 2209        /// <summary>
 2210        /// Computes image index for given image or raises if no matching image found.
 2211        /// </summary>
 2212        /// <param name="image">Image to compute index for.</param>
 2213        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2214        /// </exception>
 2215        /// <returns>Image index.</returns>
 2216        public int GetImageIndex(ItemImageInfo image)
 2217        {
 02218            ArgumentNullException.ThrowIfNull(image);
 2219
 02220            if (image.Type == ImageType.Chapter)
 2221            {
 02222                var chapters = ChapterManager.GetChapters(Id);
 02223                for (var i = 0; i < chapters.Count; i++)
 2224                {
 02225                    if (chapters[i].ImagePath == image.Path)
 2226                    {
 02227                        return i;
 2228                    }
 2229                }
 2230
 02231                throw new ArgumentException("No chapter index found for image path", image.Path);
 2232            }
 2233
 02234            var images = GetImages(image.Type).ToArray();
 02235            for (var i = 0; i < images.Length; i++)
 2236            {
 02237                if (images[i].Path == image.Path)
 2238                {
 02239                    return i;
 2240                }
 2241            }
 2242
 02243            throw new ArgumentException("No image index found for image path", image.Path);
 2244        }
 2245
 2246        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2247        {
 4002248            if (imageType == ImageType.Chapter)
 2249            {
 02250                throw new ArgumentException("No image info for chapter images");
 2251            }
 2252
 2253            // Yield return is more performant than LINQ Where on an Array
 13462254            for (var i = 0; i < ImageInfos.Length; i++)
 2255            {
 2912256                var imageInfo = ImageInfos[i];
 2912257                if (imageInfo.Type == imageType)
 2258                {
 1502259                    yield return imageInfo;
 2260                }
 2261            }
 3822262        }
 2263
 2264        /// <summary>
 2265        /// Adds the images, updating metadata if they already are part of this item.
 2266        /// </summary>
 2267        /// <param name="imageType">Type of the image.</param>
 2268        /// <param name="images">The images.</param>
 2269        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2270        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2271        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2272        {
 42273            if (imageType == ImageType.Chapter)
 2274            {
 02275                throw new ArgumentException("Cannot call AddImages with chapter images");
 2276            }
 2277
 42278            var existingImages = GetImages(imageType)
 42279                .ToList();
 2280
 42281            var newImageList = new List<FileSystemMetadata>();
 42282            var imageUpdated = false;
 2283
 242284            foreach (var newImage in images)
 2285            {
 82286                if (newImage is null)
 2287                {
 02288                    throw new ArgumentException("null image found in list");
 2289                }
 2290
 82291                var existing = existingImages
 82292                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2293
 82294                if (existing is null)
 2295                {
 42296                    newImageList.Add(newImage);
 2297                }
 2298                else
 2299                {
 42300                    if (existing.IsLocalFile)
 2301                    {
 42302                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2303
 2304                        // If date changed then we need to reset saved image dimensions
 42305                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2306                        {
 22307                            existing.Width = 0;
 22308                            existing.Height = 0;
 22309                            imageUpdated = true;
 2310                        }
 2311
 42312                        existing.DateModified = newDateModified;
 2313                    }
 2314                }
 2315            }
 2316
 42317            if (newImageList.Count > 0)
 2318            {
 22319                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2320            }
 2321
 42322            return imageUpdated || newImageList.Count > 0;
 2323        }
 2324
 2325        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2326        {
 502327            return new ItemImageInfo
 502328            {
 502329                Path = file.FullName,
 502330                Type = type,
 502331                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502332            };
 2333        }
 2334
 2335        /// <summary>
 2336        /// Gets the file system path to delete when the item is to be deleted.
 2337        /// </summary>
 2338        /// <returns>The metadata for the deleted paths.</returns>
 2339        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2340        {
 02341            return new[]
 02342            {
 02343                FileSystem.GetFileSystemInfo(Path)
 02344            }.Concat(GetLocalMetadataFilesToDelete());
 2345        }
 2346
 2347        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2348        {
 02349            if (IsFolder || !IsInMixedFolder)
 2350            {
 02351                return [];
 2352            }
 2353
 02354            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2355
 02356            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02357                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02358                .ToList();
 2359        }
 2360
 2361        public bool AllowsMultipleImages(ImageType type)
 2362        {
 182363            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2364        }
 2365
 2366        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2367        {
 02368            if (!AllowsMultipleImages(type))
 2369            {
 02370                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2371            }
 2372
 02373            var info1 = GetImageInfo(type, index1);
 02374            var info2 = GetImageInfo(type, index2);
 2375
 02376            if (info1 is null || info2 is null)
 2377            {
 2378                // Nothing to do
 02379                return Task.CompletedTask;
 2380            }
 2381
 02382            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2383            {
 2384                // TODO: Not supported  yet
 02385                return Task.CompletedTask;
 2386            }
 2387
 02388            var path1 = info1.Path;
 02389            var path2 = info2.Path;
 2390
 02391            FileSystem.SwapFiles(path1, path2);
 2392
 2393            // Refresh these values
 02394            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02395            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2396
 02397            info1.Width = 0;
 02398            info1.Height = 0;
 02399            info2.Width = 0;
 02400            info2.Height = 0;
 2401
 02402            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2403        }
 2404
 2405        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2406        {
 02407            userItemData ??= UserDataManager.GetUserData(user, this);
 2408
 02409            return userItemData is not null && userItemData.Played;
 2410        }
 2411
 2412        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2413        {
 02414            userItemData ??= UserDataManager.GetUserData(user, this);
 2415
 02416            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2417        }
 2418
 2419        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2420        {
 02421            ArgumentNullException.ThrowIfNull(user);
 2422
 02423            userItemData ??= UserDataManager.GetUserData(user, this);
 2424
 02425            return userItemData is null || !userItemData.Played;
 2426        }
 2427
 2428        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2429        {
 342430            return GetItemLookupInfo<ItemLookupInfo>();
 2431        }
 2432
 2433        protected T GetItemLookupInfo<T>()
 2434            where T : ItemLookupInfo, new()
 2435        {
 342436            return new T
 342437            {
 342438                Path = Path,
 342439                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 342440                MetadataLanguage = GetPreferredMetadataLanguage(),
 342441                Name = GetNameForMetadataLookup(),
 342442                OriginalTitle = OriginalTitle,
 342443                ProviderIds = ProviderIds,
 342444                IndexNumber = IndexNumber,
 342445                ParentIndexNumber = ParentIndexNumber,
 342446                Year = ProductionYear,
 342447                PremiereDate = PremiereDate
 342448            };
 2449        }
 2450
 2451        protected virtual string GetNameForMetadataLookup()
 2452        {
 342453            return Name;
 2454        }
 2455
 2456        /// <summary>
 2457        /// This is called before any metadata refresh and returns true if changes were made.
 2458        /// </summary>
 2459        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2460        /// <returns>true if the item has change, else false.</returns>
 2461        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2462        {
 342463            _sortName = null;
 2464
 342465            var hasChanges = false;
 2466
 342467            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2468            {
 02469                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02470                hasChanges = true;
 2471            }
 2472
 342473            return hasChanges;
 2474        }
 2475
 2476        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2477        {
 02478            if (protocol == MediaProtocol.File)
 2479            {
 02480                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2481            }
 2482
 02483            return path;
 2484        }
 2485
 2486        public virtual void FillUserDataDtoValues(
 2487            UserItemDataDto dto,
 2488            UserItemData userData,
 2489            BaseItemDto itemDto,
 2490            User user,
 2491            DtoOptions fields,
 2492            (int Played, int Total)? precomputedCounts = null)
 2493        {
 02494            if (RunTimeTicks.HasValue)
 2495            {
 02496                double pct = RunTimeTicks.Value;
 2497
 02498                if (pct > 0)
 2499                {
 02500                    pct = userData.PlaybackPositionTicks / pct;
 2501
 02502                    if (pct > 0)
 2503                    {
 02504                        dto.PlayedPercentage = 100 * pct;
 2505                    }
 2506                }
 2507            }
 02508        }
 2509
 2510        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2511        {
 02512            var newOptions = new MetadataRefreshOptions(options)
 02513            {
 02514                SearchResult = null
 02515            };
 2516
 02517            var item = this;
 2518
 02519            if (copyTitleMetadata)
 2520            {
 2521                // Take some data from the main item, for querying purposes
 02522                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2523                {
 02524                    newOptions.ForceSave = true;
 02525                    ownedItem.Genres = item.Genres;
 2526                }
 2527
 02528                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2529                {
 02530                    newOptions.ForceSave = true;
 02531                    ownedItem.Studios = item.Studios;
 2532                }
 2533
 02534                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2535                {
 02536                    newOptions.ForceSave = true;
 02537                    ownedItem.ProductionLocations = item.ProductionLocations;
 2538                }
 2539
 02540                if (item.CommunityRating != ownedItem.CommunityRating)
 2541                {
 02542                    ownedItem.CommunityRating = item.CommunityRating;
 02543                    newOptions.ForceSave = true;
 2544                }
 2545
 02546                if (item.CriticRating != ownedItem.CriticRating)
 2547                {
 02548                    ownedItem.CriticRating = item.CriticRating;
 02549                    newOptions.ForceSave = true;
 2550                }
 2551
 02552                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2553                {
 02554                    ownedItem.Overview = item.Overview;
 02555                    newOptions.ForceSave = true;
 2556                }
 2557
 02558                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2559                {
 02560                    ownedItem.OfficialRating = item.OfficialRating;
 02561                    newOptions.ForceSave = true;
 2562                }
 2563
 02564                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2565                {
 02566                    ownedItem.CustomRating = item.CustomRating;
 02567                    newOptions.ForceSave = true;
 2568                }
 2569            }
 2570
 02571            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02572        }
 2573
 2574        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2575        {
 02576            var newOptions = new MetadataRefreshOptions(options)
 02577            {
 02578                SearchResult = null
 02579            };
 2580
 02581            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2582
 2583            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02584            if (LibraryManager.GetItemById(id) is not Video video)
 2585            {
 02586                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2587
 02588                newOptions.ForceSave = true;
 2589            }
 2590
 02591            if (video is null)
 2592            {
 02593                return;
 2594            }
 2595
 02596            if (video.OwnerId.IsEmpty())
 2597            {
 02598                video.OwnerId = Id;
 2599            }
 2600
 02601            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02602        }
 2603
 2604        public string GetEtag(User user)
 2605        {
 62606            var list = GetEtagValues(user);
 2607
 62608            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2609        }
 2610
 2611        protected virtual List<string> GetEtagValues(User user)
 2612        {
 62613            return
 62614            [
 62615                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62616            ];
 2617        }
 2618
 2619        public virtual IEnumerable<Guid> GetAncestorIds()
 2620        {
 1112621            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2622        }
 2623
 2624        public BaseItem GetTopParent()
 2625        {
 1932626            if (IsTopParent)
 2627            {
 212628                return this;
 2629            }
 2630
 1722631            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2632        }
 2633
 2634        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2635        {
 432636            return [Id];
 2637        }
 2638
 2639        public virtual double? GetRefreshProgress()
 2640        {
 02641            return null;
 2642        }
 2643
 2644        public virtual ItemUpdateType OnMetadataChanged()
 2645        {
 1132646            var updateType = ItemUpdateType.None;
 2647
 1132648            var item = this;
 2649
 1132650            var rating = item.GetParentalRatingScore();
 1132651            if (rating is not null)
 2652            {
 02653                if (rating.Score != item.InheritedParentalRatingValue)
 2654                {
 02655                    item.InheritedParentalRatingValue = rating.Score;
 02656                    updateType |= ItemUpdateType.MetadataImport;
 2657                }
 2658
 02659                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2660                {
 02661                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02662                    updateType |= ItemUpdateType.MetadataImport;
 2663                }
 2664            }
 2665            else
 2666            {
 1132667                if (item.InheritedParentalRatingValue is not null)
 2668                {
 02669                    item.InheritedParentalRatingValue = null;
 02670                    item.InheritedParentalRatingSubValue = null;
 02671                    updateType |= ItemUpdateType.MetadataImport;
 2672                }
 2673            }
 2674
 1132675            return updateType;
 2676        }
 2677
 2678        /// <summary>
 2679        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2680        /// </summary>
 2681        /// <param name="children">Media children.</param>
 2682        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2683        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2684        {
 02685            var currentOfficialRating = OfficialRating;
 2686
 2687            // Gather all possible ratings
 02688            var ratings = children
 02689                .Select(i => i.OfficialRating)
 02690                .Where(i => !string.IsNullOrEmpty(i))
 02691                .Distinct(StringComparer.OrdinalIgnoreCase)
 02692                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 02693                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02694                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02695                .Select(i => i.rating);
 2696
 02697            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2698
 02699            return !string.Equals(
 02700                currentOfficialRating ?? string.Empty,
 02701                OfficialRating ?? string.Empty,
 02702                StringComparison.OrdinalIgnoreCase);
 2703        }
 2704
 2705        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2706        {
 02707            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2708        }
 2709
 2710        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2711        {
 02712            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, 
 2713        }
 2714
 2715        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2716        {
 02717            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2718        }
 2719
 2720        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2721        {
 02722            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user,
 2723        }
 2724
 2725        /// <summary>
 2726        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2727        /// </summary>
 2728        /// <returns>An enumerable containing the items.</returns>
 2729        public IEnumerable<BaseItem> GetExtras()
 2730        {
 62731            return LibraryManager.GetItemList(new InternalItemsQuery()
 62732            {
 62733                OwnerIds = [Id],
 62734                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 62735            });
 2736        }
 2737
 2738        /// <summary>
 2739        /// Get all extras with specific types that are associated with this item.
 2740        /// </summary>
 2741        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2742        /// <returns>An enumerable containing the extras.</returns>
 2743        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
 2744        {
 02745            return LibraryManager.GetItemList(new InternalItemsQuery()
 02746            {
 02747                OwnerIds = [Id],
 02748                ExtraTypes = extraTypes.ToArray(),
 02749                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 02750            });
 2751        }
 2752
 2753        public virtual long GetRunTimeTicksForPlayState()
 2754        {
 02755            return RunTimeTicks ?? 0;
 2756        }
 2757
 2758        /// <inheritdoc />
 2759        public override bool Equals(object obj)
 2760        {
 02761            return obj is BaseItem baseItem && this.Equals(baseItem);
 2762        }
 2763
 2764        /// <inheritdoc />
 1142765        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2766
 2767        /// <inheritdoc />
 1002768        public override int GetHashCode() => HashCode.Combine(Id);
 2769    }
 2770}

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