< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.BaseItem
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs
Line coverage
51%
Covered lines: 376
Uncovered lines: 359
Coverable lines: 735
Total lines: 2697
Line coverage: 51.1%
Branch coverage
40%
Covered branches: 188
Total branches: 466
Branch coverage: 40.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/3/2025 - 12:11:15 AM Line coverage: 51.6% (377/730) Branch coverage: 41.4% (190/458) Total lines: 268210/12/2025 - 12:11:07 AM Line coverage: 51.6% (377/730) Branch coverage: 41.2% (189/458) Total lines: 268210/27/2025 - 12:11:33 AM Line coverage: 51.6% (377/730) Branch coverage: 41% (188/458) Total lines: 268210/28/2025 - 12:11:27 AM Line coverage: 51.5% (376/730) Branch coverage: 41% (188/458) Total lines: 268211/18/2025 - 12:11:25 AM Line coverage: 51.2% (376/733) Branch coverage: 40.3% (188/466) Total lines: 269212/29/2025 - 12:13:19 AM Line coverage: 51.1% (376/735) Branch coverage: 40.3% (188/466) Total lines: 2697 10/3/2025 - 12:11:15 AM Line coverage: 51.6% (377/730) Branch coverage: 41.4% (190/458) Total lines: 268210/12/2025 - 12:11:07 AM Line coverage: 51.6% (377/730) Branch coverage: 41.2% (189/458) Total lines: 268210/27/2025 - 12:11:33 AM Line coverage: 51.6% (377/730) Branch coverage: 41% (188/458) Total lines: 268210/28/2025 - 12:11:27 AM Line coverage: 51.5% (376/730) Branch coverage: 41% (188/458) Total lines: 268211/18/2025 - 12:11:25 AM Line coverage: 51.2% (376/733) Branch coverage: 40.3% (188/466) Total lines: 269212/29/2025 - 12:13:19 AM Line coverage: 51.1% (376/735) Branch coverage: 40.3% (188/466) Total lines: 2697

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%
FindParent()0%2040%
GetPlayAccess(...)50%2266.66%
GetMediaStreams()100%210%
IsActiveRecording()100%210%
GetMediaSources(...)0%7280%
GetAllItemsForMediaSources()100%210%
GetVersionInfo(...)0%2550500%
GetMediaSourceName(...)68.75%743265.51%
RefreshMetadata(...)100%11100%
IsVisibleStandaloneInternal(...)0%210140%
SetParent(...)50%22100%
GetFileSystemChildren(...)100%11100%
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%581645.45%
GetParentalRatingScore()75%4483.33%
GetInheritedTags()75%4485.71%
IsVisibleViaTags(...)38.88%591850%
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%7280%
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%
RemoveImage(...)100%210%
RemoveImages(...)100%11100%
AddImage(...)100%11100%
ValidateImages()100%1212100%
GetImagePath(...)50%22100%
GetImageInfo(...)33.33%831221.05%
GetImageIndex(...)0%110100%
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%
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.Audio;
 26using MediaBrowser.Controller.Entities.TV;
 27using MediaBrowser.Controller.IO;
 28using MediaBrowser.Controller.Library;
 29using MediaBrowser.Controller.MediaSegments;
 30using MediaBrowser.Controller.Persistence;
 31using MediaBrowser.Controller.Providers;
 32using MediaBrowser.Model.Dto;
 33using MediaBrowser.Model.Entities;
 34using MediaBrowser.Model.Globalization;
 35using MediaBrowser.Model.IO;
 36using MediaBrowser.Model.Library;
 37using MediaBrowser.Model.LiveTv;
 38using MediaBrowser.Model.MediaInfo;
 39using Microsoft.Extensions.Logging;
 40
 41namespace MediaBrowser.Controller.Entities
 42{
 43    /// <summary>
 44    /// Class BaseItem.
 45    /// </summary>
 46    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 47    {
 48        private BaseItemKind? _baseItemKind;
 49
 50        public const string ThemeSongFileName = "theme";
 51
 52        /// <summary>
 53        /// The supported image extensions.
 54        /// </summary>
 455        public static readonly string[] SupportedImageExtensions
 456            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 57
 458        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 459        {
 460            ".nfo",
 461            ".xml",
 462            ".srt",
 463            ".vtt",
 464            ".sub",
 465            ".sup",
 466            ".idx",
 467            ".txt",
 468            ".edl",
 469            ".bif",
 470            ".smi",
 471            ".ttml",
 472            ".lrc",
 473            ".elrc"
 474        };
 75
 76        /// <summary>
 77        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 78        /// </summary>
 479        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 480        {
 481            Model.Entities.ExtraType.Unknown,
 482            Model.Entities.ExtraType.BehindTheScenes,
 483            Model.Entities.ExtraType.Clip,
 484            Model.Entities.ExtraType.DeletedScene,
 485            Model.Entities.ExtraType.Interview,
 486            Model.Entities.ExtraType.Sample,
 487            Model.Entities.ExtraType.Scene,
 488            Model.Entities.ExtraType.Featurette,
 489            Model.Entities.ExtraType.Short
 490        };
 91
 92        private string _sortName;
 93
 94        private string _forcedSortName;
 95
 96        private string _name;
 97
 98        public const char SlugChar = '-';
 99
 100        protected BaseItem()
 101        {
 970102            Tags = Array.Empty<string>();
 970103            Genres = Array.Empty<string>();
 970104            Studios = Array.Empty<string>();
 970105            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 970106            LockedFields = Array.Empty<MetadataField>();
 970107            ImageInfos = Array.Empty<ItemImageInfo>();
 970108            ProductionLocations = Array.Empty<string>();
 970109            RemoteTrailers = Array.Empty<MediaUrl>();
 970110            ExtraIds = Array.Empty<Guid>();
 970111            UserData = [];
 970112        }
 113
 114        /// <summary>
 115        /// Gets or Sets the user data collection as cached from the last Db query.
 116        /// </summary>
 117        [JsonIgnore]
 118        public ICollection<UserData> UserData { get; set; }
 119
 120        [JsonIgnore]
 121        public string PreferredMetadataCountryCode { get; set; }
 122
 123        [JsonIgnore]
 124        public string PreferredMetadataLanguage { get; set; }
 125
 126        public long? Size { get; set; }
 127
 128        public string Container { get; set; }
 129
 130        [JsonIgnore]
 131        public string Tagline { get; set; }
 132
 133        [JsonIgnore]
 134        public virtual ItemImageInfo[] ImageInfos { get; set; }
 135
 136        [JsonIgnore]
 137        public bool IsVirtualItem { get; set; }
 138
 139        /// <summary>
 140        /// Gets or sets the album.
 141        /// </summary>
 142        /// <value>The album.</value>
 143        [JsonIgnore]
 144        public string Album { get; set; }
 145
 146        /// <summary>
 147        /// Gets or sets the LUFS value.
 148        /// </summary>
 149        /// <value>The LUFS Value.</value>
 150        [JsonIgnore]
 151        public float? LUFS { get; set; }
 152
 153        /// <summary>
 154        /// Gets or sets the gain required for audio normalization.
 155        /// </summary>
 156        /// <value>The gain required for audio normalization.</value>
 157        [JsonIgnore]
 158        public float? NormalizationGain { get; set; }
 159
 160        /// <summary>
 161        /// Gets or sets the channel identifier.
 162        /// </summary>
 163        /// <value>The channel identifier.</value>
 164        [JsonIgnore]
 165        public Guid ChannelId { get; set; }
 166
 167        [JsonIgnore]
 0168        public virtual bool SupportsAddingToPlaylist => false;
 169
 170        [JsonIgnore]
 11171        public virtual bool AlwaysScanInternalMetadataPath => false;
 172
 173        /// <summary>
 174        /// Gets or sets a value indicating whether this instance is in mixed folder.
 175        /// </summary>
 176        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 177        [JsonIgnore]
 178        public bool IsInMixedFolder { get; set; }
 179
 180        [JsonIgnore]
 0181        public virtual bool SupportsPlayedStatus => false;
 182
 183        [JsonIgnore]
 0184        public virtual bool SupportsPositionTicksResume => false;
 185
 186        [JsonIgnore]
 17187        public virtual bool SupportsRemoteImageDownloading => true;
 188
 189        /// <summary>
 190        /// Gets or sets the name.
 191        /// </summary>
 192        /// <value>The name.</value>
 193        [JsonIgnore]
 194        public virtual string Name
 195        {
 1020196            get => _name;
 197            set
 198            {
 349199                _name = value;
 200
 201                // lazy load this again
 349202                _sortName = null;
 349203            }
 204        }
 205
 206        [JsonIgnore]
 0207        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 208
 209        [JsonIgnore]
 210        public int? TotalBitrate { get; set; }
 211
 212        [JsonIgnore]
 213        public ExtraType? ExtraType { get; set; }
 214
 215        [JsonIgnore]
 0216        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 217
 218        [JsonIgnore]
 219        public string OriginalTitle { get; set; }
 220
 221        /// <summary>
 222        /// Gets or sets the id.
 223        /// </summary>
 224        /// <value>The id.</value>
 225        [JsonIgnore]
 226        public Guid Id { get; set; }
 227
 228        [JsonIgnore]
 229        public Guid OwnerId { get; set; }
 230
 231        /// <summary>
 232        /// Gets or sets the audio.
 233        /// </summary>
 234        /// <value>The audio.</value>
 235        [JsonIgnore]
 236        public ProgramAudio? Audio { get; set; }
 237
 238        /// <summary>
 239        /// Gets the id that should be used to key display prefs for this item.
 240        /// Default is based on the type for everything except actual generic folders.
 241        /// </summary>
 242        /// <value>The display prefs id.</value>
 243        [JsonIgnore]
 244        public virtual Guid DisplayPreferencesId
 245        {
 246            get
 247            {
 6248                var thisType = GetType();
 6249                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 250            }
 251        }
 252
 253        /// <summary>
 254        /// Gets or sets the path.
 255        /// </summary>
 256        /// <value>The path.</value>
 257        [JsonIgnore]
 258        public virtual string Path { get; set; }
 259
 260        [JsonIgnore]
 261        public virtual SourceType SourceType
 262        {
 263            get
 264            {
 2186265                if (!ChannelId.IsEmpty())
 266                {
 0267                    return SourceType.Channel;
 268                }
 269
 2186270                return SourceType.Library;
 271            }
 272        }
 273
 274        /// <summary>
 275        /// Gets the folder containing the item.
 276        /// If the item is a folder, it returns the folder itself.
 277        /// </summary>
 278        [JsonIgnore]
 279        public virtual string ContainingFolderPath
 280        {
 281            get
 282            {
 436283                if (IsFolder)
 284                {
 377285                    return Path;
 286                }
 287
 59288                return System.IO.Path.GetDirectoryName(Path);
 289            }
 290        }
 291
 292        /// <summary>
 293        /// Gets or sets the name of the service.
 294        /// </summary>
 295        /// <value>The name of the service.</value>
 296        [JsonIgnore]
 297        public string ServiceName { get; set; }
 298
 299        /// <summary>
 300        /// Gets or sets the external id.
 301        /// </summary>
 302        /// <remarks>
 303        /// If this content came from an external service, the id of the content on that service.
 304        /// </remarks>
 305        [JsonIgnore]
 306        public string ExternalId { get; set; }
 307
 308        [JsonIgnore]
 309        public string ExternalSeriesId { get; set; }
 310
 311        [JsonIgnore]
 0312        public virtual bool IsHidden => false;
 313
 314        /// <summary>
 315        /// Gets the type of the location.
 316        /// </summary>
 317        /// <value>The type of the location.</value>
 318        [JsonIgnore]
 319        public virtual LocationType LocationType
 320        {
 321            get
 322            {
 9323                var path = Path;
 9324                if (string.IsNullOrEmpty(path))
 325                {
 0326                    if (SourceType == SourceType.Channel)
 327                    {
 0328                        return LocationType.Remote;
 329                    }
 330
 0331                    return LocationType.Virtual;
 332                }
 333
 9334                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 335            }
 336        }
 337
 338        [JsonIgnore]
 339        public MediaProtocol? PathProtocol
 340        {
 341            get
 342            {
 2573343                var path = Path;
 344
 2573345                if (string.IsNullOrEmpty(path))
 346                {
 116347                    return null;
 348                }
 349
 2457350                return MediaSourceManager.GetPathProtocol(path);
 351            }
 352        }
 353
 354        [JsonIgnore]
 2552355        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 356
 357        [JsonIgnore]
 0358        public bool HasPathProtocol => PathProtocol.HasValue;
 359
 360        [JsonIgnore]
 361        public virtual bool SupportsLocalMetadata
 362        {
 363            get
 364            {
 1434365                if (SourceType == SourceType.Channel)
 366                {
 0367                    return false;
 368                }
 369
 1434370                return IsFileProtocol;
 371            }
 372        }
 373
 374        [JsonIgnore]
 375        public virtual string FileNameWithoutExtension
 376        {
 377            get
 378            {
 0379                if (IsFileProtocol)
 380                {
 0381                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 382                }
 383
 0384                return null;
 385            }
 386        }
 387
 388        [JsonIgnore]
 100389        public virtual bool EnableAlphaNumericSorting => true;
 390
 139391        public virtual bool IsHD => Height >= 720;
 392
 393        public bool IsShortcut { get; set; }
 394
 395        public string ShortcutPath { get; set; }
 396
 397        public int Width { get; set; }
 398
 399        public int Height { get; set; }
 400
 401        public Guid[] ExtraIds { get; set; }
 402
 403        /// <summary>
 404        /// Gets the primary image path.
 405        /// </summary>
 406        /// <remarks>
 407        /// This is just a helper for convenience.
 408        /// </remarks>
 409        /// <value>The primary image path.</value>
 410        [JsonIgnore]
 0411        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 412
 413        /// <summary>
 414        /// Gets or sets the date created.
 415        /// </summary>
 416        /// <value>The date created.</value>
 417        [JsonIgnore]
 418        public DateTime DateCreated { get; set; }
 419
 420        /// <summary>
 421        /// Gets or sets the date modified.
 422        /// </summary>
 423        /// <value>The date modified.</value>
 424        [JsonIgnore]
 425        public DateTime DateModified { get; set; }
 426
 427        public DateTime DateLastSaved { get; set; }
 428
 429        [JsonIgnore]
 430        public DateTime DateLastRefreshed { get; set; }
 431
 432        [JsonIgnore]
 433        public bool IsLocked { get; set; }
 434
 435        /// <summary>
 436        /// Gets or sets the locked fields.
 437        /// </summary>
 438        /// <value>The locked fields.</value>
 439        [JsonIgnore]
 440        public MetadataField[] LockedFields { get; set; }
 441
 442        /// <summary>
 443        /// Gets the type of the media.
 444        /// </summary>
 445        /// <value>The type of the media.</value>
 446        [JsonIgnore]
 130447        public virtual MediaType MediaType => MediaType.Unknown;
 448
 449        [JsonIgnore]
 450        public virtual string[] PhysicalLocations
 451        {
 452            get
 453            {
 0454                if (!IsFileProtocol)
 455                {
 0456                    return Array.Empty<string>();
 457                }
 458
 0459                return [Path];
 460            }
 461        }
 462
 463        [JsonIgnore]
 464        public bool EnableMediaSourceDisplay
 465        {
 466            get
 467            {
 6468                if (SourceType == SourceType.Channel)
 469                {
 0470                    return ChannelManager.EnableMediaSourceDisplay(this);
 471                }
 472
 6473                return true;
 474            }
 475        }
 476
 477        [JsonIgnore]
 478        public Guid ParentId { get; set; }
 479
 480        /// <summary>
 481        /// Gets or sets the logger.
 482        /// </summary>
 483        public static ILogger<BaseItem> Logger { get; set; }
 484
 485        public static ILibraryManager LibraryManager { get; set; }
 486
 487        public static IServerConfigurationManager ConfigurationManager { get; set; }
 488
 489        public static IProviderManager ProviderManager { get; set; }
 490
 491        public static ILocalizationManager LocalizationManager { get; set; }
 492
 493        public static IItemRepository ItemRepository { get; set; }
 494
 495        public static IChapterManager ChapterManager { get; set; }
 496
 497        public static IFileSystem FileSystem { get; set; }
 498
 499        public static IUserDataManager UserDataManager { get; set; }
 500
 501        public static IChannelManager ChannelManager { get; set; }
 502
 503        public static IMediaSourceManager MediaSourceManager { get; set; }
 504
 505        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 506
 507        /// <summary>
 508        /// Gets or sets the name of the forced sort.
 509        /// </summary>
 510        /// <value>The name of the forced sort.</value>
 511        [JsonIgnore]
 512        public string ForcedSortName
 513        {
 472514            get => _forcedSortName;
 515            set
 516            {
 87517                _forcedSortName = value;
 87518                _sortName = null;
 87519            }
 520        }
 521
 522        /// <summary>
 523        /// Gets or sets the name of the sort.
 524        /// </summary>
 525        /// <value>The name of the sort.</value>
 526        [JsonIgnore]
 527        public string SortName
 528        {
 529            get
 530            {
 170531                if (_sortName is null)
 532                {
 100533                    if (!string.IsNullOrEmpty(ForcedSortName))
 534                    {
 535                        // Need the ToLower because that's what CreateSortName does
 0536                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 537                    }
 538                    else
 539                    {
 100540                        _sortName = CreateSortName();
 541                    }
 542                }
 543
 170544                return _sortName;
 545            }
 546
 126547            set => _sortName = value;
 548        }
 549
 550        [JsonIgnore]
 318551        public virtual Guid DisplayParentId => ParentId;
 552
 553        [JsonIgnore]
 554        public BaseItem DisplayParent
 555        {
 556            get
 557            {
 312558                var id = DisplayParentId;
 312559                if (id.IsEmpty())
 560                {
 242561                    return null;
 562                }
 563
 70564                return LibraryManager.GetItemById(id);
 565            }
 566        }
 567
 568        /// <summary>
 569        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 570        /// </summary>
 571        /// <value>The premiere date.</value>
 572        [JsonIgnore]
 573        public DateTime? PremiereDate { get; set; }
 574
 575        /// <summary>
 576        /// Gets or sets the end date.
 577        /// </summary>
 578        /// <value>The end date.</value>
 579        [JsonIgnore]
 580        public DateTime? EndDate { get; set; }
 581
 582        /// <summary>
 583        /// Gets or sets the official rating.
 584        /// </summary>
 585        /// <value>The official rating.</value>
 586        [JsonIgnore]
 587        public string OfficialRating { get; set; }
 588
 589        [JsonIgnore]
 590        public int? InheritedParentalRatingValue { get; set; }
 591
 592        [JsonIgnore]
 593        public int? InheritedParentalRatingSubValue { get; set; }
 594
 595        /// <summary>
 596        /// Gets or sets the critic rating.
 597        /// </summary>
 598        /// <value>The critic rating.</value>
 599        [JsonIgnore]
 600        public float? CriticRating { get; set; }
 601
 602        /// <summary>
 603        /// Gets or sets the custom rating.
 604        /// </summary>
 605        /// <value>The custom rating.</value>
 606        [JsonIgnore]
 607        public string CustomRating { get; set; }
 608
 609        /// <summary>
 610        /// Gets or sets the overview.
 611        /// </summary>
 612        /// <value>The overview.</value>
 613        [JsonIgnore]
 614        public string Overview { get; set; }
 615
 616        /// <summary>
 617        /// Gets or sets the studios.
 618        /// </summary>
 619        /// <value>The studios.</value>
 620        [JsonIgnore]
 621        public string[] Studios { get; set; }
 622
 623        /// <summary>
 624        /// Gets or sets the genres.
 625        /// </summary>
 626        /// <value>The genres.</value>
 627        [JsonIgnore]
 628        public string[] Genres { get; set; }
 629
 630        /// <summary>
 631        /// Gets or sets the tags.
 632        /// </summary>
 633        /// <value>The tags.</value>
 634        [JsonIgnore]
 635        public string[] Tags { get; set; }
 636
 637        [JsonIgnore]
 638        public string[] ProductionLocations { get; set; }
 639
 640        /// <summary>
 641        /// Gets or sets the home page URL.
 642        /// </summary>
 643        /// <value>The home page URL.</value>
 644        [JsonIgnore]
 645        public string HomePageUrl { get; set; }
 646
 647        /// <summary>
 648        /// Gets or sets the community rating.
 649        /// </summary>
 650        /// <value>The community rating.</value>
 651        [JsonIgnore]
 652        public float? CommunityRating { get; set; }
 653
 654        /// <summary>
 655        /// Gets or sets the run time ticks.
 656        /// </summary>
 657        /// <value>The run time ticks.</value>
 658        [JsonIgnore]
 659        public long? RunTimeTicks { get; set; }
 660
 661        /// <summary>
 662        /// Gets or sets the production year.
 663        /// </summary>
 664        /// <value>The production year.</value>
 665        [JsonIgnore]
 666        public int? ProductionYear { get; set; }
 667
 668        /// <summary>
 669        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 670        /// This could be episode number, album track number, etc.
 671        /// </summary>
 672        /// <value>The index number.</value>
 673        [JsonIgnore]
 674        public int? IndexNumber { get; set; }
 675
 676        /// <summary>
 677        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 678        /// </summary>
 679        /// <value>The parent index number.</value>
 680        [JsonIgnore]
 681        public int? ParentIndexNumber { get; set; }
 682
 683        [JsonIgnore]
 0684        public virtual bool HasLocalAlternateVersions => false;
 685
 686        [JsonIgnore]
 687        public string OfficialRatingForComparison
 688        {
 689            get
 690            {
 156691                var officialRating = OfficialRating;
 156692                if (!string.IsNullOrEmpty(officialRating))
 693                {
 0694                    return officialRating;
 695                }
 696
 156697                var parent = DisplayParent;
 156698                if (parent is not null)
 699                {
 35700                    return parent.OfficialRatingForComparison;
 701                }
 702
 121703                return null;
 704            }
 705        }
 706
 707        [JsonIgnore]
 708        public string CustomRatingForComparison
 709        {
 710            get
 711            {
 121712                return GetCustomRatingForComparision();
 713            }
 714        }
 715
 716        /// <summary>
 717        /// Gets or sets the provider ids.
 718        /// </summary>
 719        /// <value>The provider ids.</value>
 720        [JsonIgnore]
 721        public Dictionary<string, string> ProviderIds { get; set; }
 722
 723        [JsonIgnore]
 0724        public virtual Folder LatestItemsIndexContainer => null;
 725
 726        [JsonIgnore]
 727        public string PresentationUniqueKey { get; set; }
 728
 729        [JsonIgnore]
 0730        public virtual bool EnableRememberingTrackSelections => true;
 731
 732        [JsonIgnore]
 733        public virtual bool IsTopParent
 734        {
 735            get
 736            {
 276737                if (this is BasePluginFolder || this is Channel)
 738                {
 59739                    return true;
 740                }
 741
 217742                if (this is IHasCollectionType view)
 743                {
 13744                    if (view.CollectionType == CollectionType.livetv)
 745                    {
 0746                        return true;
 747                    }
 748                }
 749
 217750                if (GetParent() is AggregateFolder)
 751                {
 0752                    return true;
 753                }
 754
 217755                return false;
 756            }
 757        }
 758
 759        [JsonIgnore]
 222760        public virtual bool SupportsAncestors => true;
 761
 762        [JsonIgnore]
 88763        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 764
 765        [JsonIgnore]
 69766        public virtual bool SupportsPeople => false;
 767
 768        [JsonIgnore]
 0769        public virtual bool SupportsThemeMedia => false;
 770
 771        [JsonIgnore]
 0772        public virtual bool SupportsInheritedParentImages => false;
 773
 774        /// <summary>
 775        /// Gets a value indicating whether this instance is folder.
 776        /// </summary>
 777        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 778        [JsonIgnore]
 79779        public virtual bool IsFolder => false;
 780
 781        [JsonIgnore]
 0782        public virtual bool IsDisplayedAsFolder => false;
 783
 784        /// <summary>
 785        /// Gets or sets the remote trailers.
 786        /// </summary>
 787        /// <value>The remote trailers.</value>
 788        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 789
 790        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 791        {
 156792            callstack ??= new();
 156793            var customRating = CustomRating;
 156794            if (!string.IsNullOrEmpty(customRating))
 795            {
 0796                return customRating;
 797            }
 798
 156799            callstack.Add(Id);
 800
 156801            var parent = DisplayParent;
 156802            if (parent is not null && !callstack.Contains(parent.Id))
 803            {
 35804                return parent.GetCustomRatingForComparision(callstack);
 805            }
 806
 121807            return null;
 808        }
 809
 810        public virtual double GetDefaultPrimaryImageAspectRatio()
 811        {
 0812            return 0;
 813        }
 814
 815        public virtual string CreatePresentationUniqueKey()
 816        {
 54817            return Id.ToString("N", CultureInfo.InvariantCulture);
 818        }
 819
 820        public virtual bool CanDelete()
 821        {
 0822            if (SourceType == SourceType.Channel)
 823            {
 0824                return ChannelManager.CanDelete(this);
 825            }
 826
 0827            return IsFileProtocol;
 828        }
 829
 830        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 831        {
 0832            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 833            {
 0834                return true;
 835            }
 836
 0837            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 838
 0839            if (SourceType == SourceType.Channel)
 840            {
 0841                return allowed.Contains(ChannelId);
 842            }
 843
 0844            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 845
 0846            foreach (var folder in collectionFolders)
 847            {
 0848                if (allowed.Contains(folder.Id))
 849                {
 0850                    return true;
 851                }
 852            }
 853
 0854            return false;
 0855        }
 856
 857        public BaseItem GetOwner()
 858        {
 690859            var ownerId = OwnerId;
 690860            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 861        }
 862
 863        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 864        {
 6865            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 866        }
 867
 868        public virtual bool CanDelete(User user)
 869        {
 6870            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 871
 6872            return CanDelete(user, allCollectionFolders);
 873        }
 874
 875        public virtual bool CanDownload()
 876        {
 6877            return false;
 878        }
 879
 880        public virtual bool IsAuthorizedToDownload(User user)
 881        {
 0882            return user.HasPermission(PermissionKind.EnableContentDownloading);
 883        }
 884
 885        public bool CanDownload(User user)
 886        {
 6887            return CanDownload() && IsAuthorizedToDownload(user);
 888        }
 889
 890        /// <inheritdoc />
 891        public override string ToString()
 892        {
 69893            return Name;
 894        }
 895
 896        public virtual string GetInternalMetadataPath()
 897        {
 71898            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 899
 71900            return GetInternalMetadataPath(basePath);
 901        }
 902
 903        protected virtual string GetInternalMetadataPath(string basePath)
 904        {
 71905            if (SourceType == SourceType.Channel)
 906            {
 0907                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 908            }
 909
 71910            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 911
 71912            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 913        }
 914
 915        /// <summary>
 916        /// Creates the name of the sort.
 917        /// </summary>
 918        /// <returns>System.String.</returns>
 919        protected virtual string CreateSortName()
 920        {
 100921            if (Name is null)
 922            {
 0923                return null; // some items may not have name filled in properly
 924            }
 925
 100926            if (!EnableAlphaNumericSorting)
 927            {
 0928                return Name.TrimStart();
 929            }
 930
 100931            var sortable = Name.Trim().ToLowerInvariant();
 932
 800933            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 934            {
 935                // Remove from beginning if a space follows
 300936                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 937                {
 0938                    sortable = sortable.Remove(0, search.Length + 1);
 939                }
 940
 941                // Remove from middle if surrounded by spaces
 300942                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 943
 944                // Remove from end if preceeded by a space
 300945                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 946                {
 0947                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 948                }
 949            }
 950
 1400951            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 952            {
 600953                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 954            }
 955
 800956            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 957            {
 300958                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 959            }
 960
 100961            return ModifySortChunks(sortable);
 962        }
 963
 964        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 965        {
 966            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 967            {
 968                if (isDigitChunk && chunk.Length < 10)
 969                {
 970                    builder.Append('0', 10 - chunk.Length);
 971                }
 972
 973                builder.Append(chunk);
 974            }
 975
 106976            if (name.IsEmpty)
 977            {
 1978                return string.Empty;
 979            }
 980
 105981            var builder = new StringBuilder(name.Length);
 982
 105983            int chunkStart = 0;
 105984            bool isDigitChunk = char.IsDigit(name[0]);
 1674985            for (int i = 0; i < name.Length; i++)
 986            {
 732987                var isDigit = char.IsDigit(name[i]);
 732988                if (isDigit != isDigitChunk)
 989                {
 5990                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5991                    chunkStart = i;
 5992                    isDigitChunk = isDigit;
 993                }
 994            }
 995
 105996            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 997
 998            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 105999            var result = builder.ToString().RemoveDiacritics();
 1051000            if (!result.All(char.IsAscii))
 1001            {
 01002                result = result.Transliterated();
 1003            }
 1004
 1051005            return result;
 1006        }
 1007
 1008        public BaseItem GetParent()
 1009        {
 19231010            var parentId = ParentId;
 19231011            if (parentId.IsEmpty())
 1012            {
 16191013                return null;
 1014            }
 1015
 3041016            return LibraryManager.GetItemById(parentId);
 1017        }
 1018
 1019        public IEnumerable<BaseItem> GetParents()
 1020        {
 1021            var parent = GetParent();
 1022
 1023            while (parent is not null)
 1024            {
 1025                yield return parent;
 1026
 1027                parent = parent.GetParent();
 1028            }
 1029        }
 1030
 1031        /// <summary>
 1032        /// Finds a parent of a given type.
 1033        /// </summary>
 1034        /// <typeparam name="T">Type of parent.</typeparam>
 1035        /// <returns>``0.</returns>
 1036        public T FindParent<T>()
 1037            where T : Folder
 1038        {
 01039            foreach (var parent in GetParents())
 1040            {
 01041                if (parent is T item)
 1042                {
 01043                    return item;
 1044                }
 1045            }
 1046
 01047            return null;
 01048        }
 1049
 1050        /// <summary>
 1051        /// Gets the play access.
 1052        /// </summary>
 1053        /// <param name="user">The user.</param>
 1054        /// <returns>PlayAccess.</returns>
 1055        public PlayAccess GetPlayAccess(User user)
 1056        {
 61057            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1058            {
 01059                return PlayAccess.None;
 1060            }
 1061
 1062            // if (!user.IsParentalScheduleAllowed())
 1063            // {
 1064            //    return PlayAccess.None;
 1065            // }
 1066
 61067            return PlayAccess.Full;
 1068        }
 1069
 1070        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1071        {
 01072            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01073            {
 01074                ItemId = Id
 01075            });
 1076        }
 1077
 1078        protected virtual bool IsActiveRecording()
 1079        {
 01080            return false;
 1081        }
 1082
 1083        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1084        {
 01085            if (SourceType == SourceType.Channel)
 1086            {
 01087                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01088                           .ToList();
 1089
 01090                if (sources.Count > 0)
 1091                {
 01092                    return sources;
 1093                }
 1094            }
 1095
 01096            var list = GetAllItemsForMediaSources();
 01097            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
 1098
 01099            if (IsActiveRecording())
 1100            {
 01101                foreach (var mediaSource in result)
 1102                {
 01103                    mediaSource.Type = MediaSourceType.Placeholder;
 1104                }
 1105            }
 1106
 01107            return result.OrderBy(i =>
 01108            {
 01109                if (i.VideoType == VideoType.VideoFile)
 01110                {
 01111                    return 0;
 01112                }
 01113
 01114                return 1;
 01115            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 01116            .ThenByDescending(i => i, new MediaSourceWidthComparator())
 01117            .ToArray();
 1118        }
 1119
 1120        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1121        {
 01122            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1123        }
 1124
 1125        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
 1126        {
 01127            ArgumentNullException.ThrowIfNull(item);
 1128
 01129            var protocol = item.PathProtocol;
 1130
 1131            // Resolve the item path so everywhere we use the media source it will always point to
 1132            // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
 1133            // path will return null, so it's safe to check for all paths.
 01134            var itemPath = item.Path;
 01135            if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) 
 1136            {
 01137                itemPath = linkInfo.FullName;
 1138            }
 1139
 01140            var info = new MediaSourceInfo
 01141            {
 01142                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 01143                Protocol = protocol ?? MediaProtocol.File,
 01144                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 01145                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 01146                Name = GetMediaSourceName(item),
 01147                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 01148                RunTimeTicks = item.RunTimeTicks,
 01149                Container = item.Container,
 01150                Size = item.Size,
 01151                Type = type,
 01152                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 01153                    && (protocol is null or MediaProtocol.File)
 01154                    && MediaSegmentManager.HasSegments(item.Id)
 01155            };
 1156
 01157            if (string.IsNullOrEmpty(info.Path))
 1158            {
 01159                info.Type = MediaSourceType.Placeholder;
 1160            }
 1161
 01162            if (info.Protocol == MediaProtocol.File)
 1163            {
 01164                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1165            }
 1166
 01167            var video = item as Video;
 01168            if (video is not null)
 1169            {
 01170                info.IsoType = video.IsoType;
 01171                info.VideoType = video.VideoType;
 01172                info.Video3DFormat = video.Video3DFormat;
 01173                info.Timestamp = video.Timestamp;
 1174
 01175                if (video.IsShortcut)
 1176                {
 01177                    info.IsRemote = true;
 01178                    info.Path = video.ShortcutPath;
 01179                    info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
 1180                }
 1181
 01182                if (string.IsNullOrEmpty(info.Container))
 1183                {
 01184                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1185                    {
 01186                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1187                        {
 01188                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1189                        }
 1190                    }
 1191                }
 1192            }
 1193
 01194            if (string.IsNullOrEmpty(info.Container))
 1195            {
 01196                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1197                {
 01198                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1199                }
 1200            }
 1201
 01202            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1203            {
 01204                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1205            }
 1206
 01207            if (video is not null && video.VideoType != VideoType.VideoFile)
 1208            {
 01209                info.SupportsDirectStream = false;
 1210            }
 1211
 01212            info.Bitrate = item.TotalBitrate;
 01213            info.InferTotalBitrate();
 1214
 01215            return info;
 1216        }
 1217
 1218        internal string GetMediaSourceName(BaseItem item)
 1219        {
 41220            var terms = new List<string>();
 1221
 41222            var path = item.Path;
 41223            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1224            {
 41225                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 41226                if (HasLocalAlternateVersions)
 1227                {
 41228                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 41229                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1230                    {
 21231                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
 21232                        if (!name.IsWhiteSpace())
 1233                        {
 21234                            terms.Add(name.ToString());
 1235                        }
 1236                    }
 1237                }
 1238
 41239                if (terms.Count == 0)
 1240                {
 21241                    terms.Add(displayName);
 1242                }
 1243            }
 1244
 41245            if (terms.Count == 0)
 1246            {
 01247                terms.Add(item.Name);
 1248            }
 1249
 41250            if (item is Video video)
 1251            {
 41252                if (video.Video3DFormat.HasValue)
 1253                {
 01254                    terms.Add("3D");
 1255                }
 1256
 41257                if (video.VideoType == VideoType.BluRay)
 1258                {
 01259                    terms.Add("Bluray");
 1260                }
 41261                else if (video.VideoType == VideoType.Dvd)
 1262                {
 01263                    terms.Add("DVD");
 1264                }
 41265                else if (video.VideoType == VideoType.Iso)
 1266                {
 01267                    if (video.IsoType.HasValue)
 1268                    {
 01269                        if (video.IsoType.Value == IsoType.BluRay)
 1270                        {
 01271                            terms.Add("Bluray");
 1272                        }
 01273                        else if (video.IsoType.Value == IsoType.Dvd)
 1274                        {
 01275                            terms.Add("DVD");
 1276                        }
 1277                    }
 1278                    else
 1279                    {
 01280                        terms.Add("ISO");
 1281                    }
 1282                }
 1283            }
 1284
 41285            return string.Join('/', terms);
 1286        }
 1287
 1288        public Task RefreshMetadata(CancellationToken cancellationToken)
 1289        {
 481290            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1291        }
 1292
 1293        /// <summary>
 1294        /// The base implementation to refresh metadata.
 1295        /// </summary>
 1296        /// <param name="options">The options.</param>
 1297        /// <param name="cancellationToken">The cancellation token.</param>
 1298        /// <returns>true if a provider reports we changed.</returns>
 1299        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1300        {
 1301            var requiresSave = false;
 1302
 1303            if (SupportsOwnedItems)
 1304            {
 1305                try
 1306                {
 1307                    if (IsFileProtocol)
 1308                    {
 1309                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1310                    }
 1311
 1312                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 1313                }
 1314                catch (Exception ex)
 1315                {
 1316                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 1317                }
 1318            }
 1319
 1320            var refreshOptions = requiresSave
 1321                ? new MetadataRefreshOptions(options)
 1322                {
 1323                    ForceSave = true
 1324                }
 1325                : options;
 1326
 1327            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 1328        }
 1329
 1330        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1331        {
 01332            if (!IsVisible(user))
 1333            {
 01334                return false;
 1335            }
 1336
 01337            if (GetParents().Any(i => !i.IsVisible(user, true)))
 1338            {
 01339                return false;
 1340            }
 1341
 01342            if (checkFolders)
 1343            {
 01344                var topParent = GetParents().LastOrDefault() ?? this;
 1345
 01346                if (string.IsNullOrEmpty(topParent.Path))
 1347                {
 01348                    return true;
 1349                }
 1350
 01351                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1352
 01353                if (itemCollectionFolders.Count > 0)
 1354                {
 01355                    var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i
 01356                    if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
 1357                    {
 01358                        return false;
 1359                    }
 1360                }
 1361            }
 1362
 01363            return true;
 1364        }
 1365
 1366        public void SetParent(Folder parent)
 1367        {
 91368            ParentId = parent is null ? Guid.Empty : parent.Id;
 91369        }
 1370
 1371        /// <summary>
 1372        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1373        /// Returns true or false indicating if changes were found.
 1374        /// </summary>
 1375        /// <param name="options">The metadata refresh options.</param>
 1376        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1377        /// <param name="cancellationToken">The cancellation token.</param>
 1378        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1379        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1380        {
 1381            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1382            {
 1383                return false;
 1384            }
 1385
 1386            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 1387        }
 1388
 1389        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1390        {
 421391            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1392        }
 1393
 1394        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1395        {
 1396            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 1397            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1398            var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
 1399
 1400            if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.Full
 1401            {
 1402                return false;
 1403            }
 1404
 1405            var ownerId = item.Id;
 1406
 1407            var tasks = extras.Select(i =>
 1408            {
 1409                var subOptions = new MetadataRefreshOptions(options);
 1410                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
 1411                {
 1412                    i.OwnerId = ownerId;
 1413                    i.ParentId = Guid.Empty;
 1414                    subOptions.ForceSave = true;
 1415                }
 1416
 1417                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 1418            });
 1419
 1420            // Cleanup removed extras
 1421            var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 1422            if (removedExtraIds.Length > 0)
 1423            {
 1424                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 1425                {
 1426                    ItemIds = removedExtraIds
 1427                });
 1428                foreach (var removedExtra in removedExtras)
 1429                {
 1430                    LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 1431                    {
 1432                        DeleteFileLocation = false
 1433                    });
 1434                }
 1435            }
 1436
 1437            await Task.WhenAll(tasks).ConfigureAwait(false);
 1438
 1439            item.ExtraIds = newExtraIds;
 1440
 1441            return true;
 1442        }
 1443
 1444        public string GetPresentationUniqueKey()
 1445        {
 01446            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1447        }
 1448
 1449        public virtual bool RequiresRefresh()
 1450        {
 551451            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1452            {
 551453                return false;
 1454            }
 1455
 01456            var info = FileSystem.GetFileSystemInfo(Path);
 1457
 01458            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1459        }
 1460
 1461        public virtual List<string> GetUserDataKeys()
 1462        {
 1201463            var list = new List<string>();
 1464
 1201465            if (SourceType == SourceType.Channel)
 1466            {
 01467                if (!string.IsNullOrEmpty(ExternalId))
 1468                {
 01469                    list.Add(ExternalId);
 1470                }
 1471            }
 1472
 1201473            list.Add(Id.ToString());
 1201474            return list;
 1475        }
 1476
 1477        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1478        {
 51479            var updateType = ItemUpdateType.None;
 1480
 51481            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1482            {
 01483                IsInMixedFolder = newItem.IsInMixedFolder;
 01484                updateType |= ItemUpdateType.MetadataImport;
 1485            }
 1486
 51487            return updateType;
 1488        }
 1489
 1490        public void AfterMetadataRefresh()
 1491        {
 541492            _sortName = null;
 541493        }
 1494
 1495        /// <summary>
 1496        /// Gets the preferred metadata language.
 1497        /// </summary>
 1498        /// <returns>System.String.</returns>
 1499        public string GetPreferredMetadataLanguage()
 1500        {
 541501            string lang = PreferredMetadataLanguage;
 1502
 541503            if (string.IsNullOrEmpty(lang))
 1504            {
 541505                lang = GetParents()
 541506                    .Select(i => i.PreferredMetadataLanguage)
 541507                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1508            }
 1509
 541510            if (string.IsNullOrEmpty(lang))
 1511            {
 541512                lang = LibraryManager.GetCollectionFolders(this)
 541513                    .Select(i => i.PreferredMetadataLanguage)
 541514                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1515            }
 1516
 541517            if (string.IsNullOrEmpty(lang))
 1518            {
 541519                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1520            }
 1521
 541522            if (string.IsNullOrEmpty(lang))
 1523            {
 541524                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1525            }
 1526
 541527            return lang;
 1528        }
 1529
 1530        /// <summary>
 1531        /// Gets the preferred metadata language.
 1532        /// </summary>
 1533        /// <returns>System.String.</returns>
 1534        public string GetPreferredMetadataCountryCode()
 1535        {
 541536            string lang = PreferredMetadataCountryCode;
 1537
 541538            if (string.IsNullOrEmpty(lang))
 1539            {
 541540                lang = GetParents()
 541541                    .Select(i => i.PreferredMetadataCountryCode)
 541542                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1543            }
 1544
 541545            if (string.IsNullOrEmpty(lang))
 1546            {
 541547                lang = LibraryManager.GetCollectionFolders(this)
 541548                    .Select(i => i.PreferredMetadataCountryCode)
 541549                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1550            }
 1551
 541552            if (string.IsNullOrEmpty(lang))
 1553            {
 541554                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1555            }
 1556
 541557            if (string.IsNullOrEmpty(lang))
 1558            {
 541559                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1560            }
 1561
 541562            return lang;
 1563        }
 1564
 1565        public virtual bool IsSaveLocalMetadataEnabled()
 1566        {
 861567            if (SourceType == SourceType.Channel)
 1568            {
 01569                return false;
 1570            }
 1571
 861572            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1573
 861574            return libraryOptions.SaveLocalMetadata;
 1575        }
 1576
 1577        /// <summary>
 1578        /// Determines if a given user has access to this item.
 1579        /// </summary>
 1580        /// <param name="user">The user.</param>
 1581        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1582        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1583        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1584        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1585        {
 131586            ArgumentNullException.ThrowIfNull(user);
 1587
 131588            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1589            {
 01590                return false;
 1591            }
 1592
 131593            var maxAllowedRating = user.MaxParentalRatingScore;
 131594            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 131595            var rating = CustomRatingForComparison;
 1596
 131597            if (string.IsNullOrEmpty(rating))
 1598            {
 131599                rating = OfficialRatingForComparison;
 1600            }
 1601
 131602            if (string.IsNullOrEmpty(rating))
 1603            {
 131604                Logger.LogDebug("{0} has no parental rating set.", Name);
 131605                return !GetBlockUnratedValue(user);
 1606            }
 1607
 01608            var ratingScore = LocalizationManager.GetRatingScore(rating);
 1609
 1610            // Could not determine rating level
 01611            if (ratingScore is null)
 1612            {
 01613                var isAllowed = !GetBlockUnratedValue(user);
 1614
 01615                if (!isAllowed)
 1616                {
 01617                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1618                }
 1619
 01620                return isAllowed;
 1621            }
 1622
 01623            if (!maxAllowedRating.HasValue)
 1624            {
 01625                return true;
 1626            }
 1627
 01628            if (ratingScore.Score != maxAllowedRating.Value)
 1629            {
 01630                return ratingScore.Score < maxAllowedRating.Value;
 1631            }
 1632
 01633            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1634        }
 1635
 1636        public ParentalRatingScore GetParentalRatingScore()
 1637        {
 1081638            var rating = CustomRatingForComparison;
 1639
 1081640            if (string.IsNullOrEmpty(rating))
 1641            {
 1081642                rating = OfficialRatingForComparison;
 1643            }
 1644
 1081645            if (string.IsNullOrEmpty(rating))
 1646            {
 1081647                return null;
 1648            }
 1649
 01650            return LocalizationManager.GetRatingScore(rating);
 1651        }
 1652
 1653        public List<string> GetInheritedTags()
 1654        {
 1241655            var list = new List<string>();
 1241656            list.AddRange(Tags);
 1657
 3281658            foreach (var parent in GetParents())
 1659            {
 401660                list.AddRange(parent.Tags);
 1661            }
 1662
 2481663            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1664            {
 01665                list.AddRange(folder.Tags);
 1666            }
 1667
 1241668            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1669        }
 1670
 1671        private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1672        {
 131673            var allTags = GetInheritedTags();
 131674            if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgno
 1675            {
 01676                return false;
 1677            }
 1678
 131679            var parent = GetParents().FirstOrDefault() ?? this;
 131680            if (parent is UserRootFolder or AggregateFolder or UserView)
 1681            {
 131682                return true;
 1683            }
 1684
 01685            var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
 01686            if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Co
 1687            {
 01688                return false;
 1689            }
 1690
 01691            return true;
 1692        }
 1693
 1694        public virtual UnratedItem GetBlockUnratedType()
 1695        {
 1111696            if (SourceType == SourceType.Channel)
 1697            {
 01698                return UnratedItem.ChannelContent;
 1699            }
 1700
 1111701            return UnratedItem.Other;
 1702        }
 1703
 1704        /// <summary>
 1705        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1706        /// </summary>
 1707        /// <param name="user">The configuration.</param>
 1708        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1709        protected virtual bool GetBlockUnratedValue(User user)
 1710        {
 1711            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1712            // Special folders like series and albums will override this method.
 131713            if (IsFolder || this is IItemByName)
 1714            {
 131715                return false;
 1716            }
 1717
 01718            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1719        }
 1720
 1721        /// <summary>
 1722        /// Determines if this folder should be visible to a given user.
 1723        /// Default is just parental allowed. Can be overridden for more functionality.
 1724        /// </summary>
 1725        /// <param name="user">The user.</param>
 1726        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1727        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1728        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1729        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1730        {
 131731            ArgumentNullException.ThrowIfNull(user);
 1732
 131733            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1734        }
 1735
 1736        public virtual bool IsVisibleStandalone(User user)
 1737        {
 01738            if (SourceType == SourceType.Channel)
 1739            {
 01740                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1741            }
 1742
 01743            return IsVisibleStandaloneInternal(user, true);
 1744        }
 1745
 1746        public virtual string GetClientTypeName()
 1747        {
 1081748            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1749            {
 01750                return "ChannelFolderItem";
 1751            }
 1752
 1081753            return GetType().Name;
 1754        }
 1755
 1756        public BaseItemKind GetBaseItemKind()
 1757        {
 2381758            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1759        }
 1760
 1761        /// <summary>
 1762        /// Gets the linked child.
 1763        /// </summary>
 1764        /// <param name="info">The info.</param>
 1765        /// <returns>BaseItem.</returns>
 1766        protected BaseItem GetLinkedChild(LinkedChild info)
 1767        {
 1768            // First get using the cached Id
 01769            if (info.ItemId.HasValue)
 1770            {
 01771                if (info.ItemId.Value.IsEmpty())
 1772                {
 01773                    return null;
 1774                }
 1775
 01776                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1777
 01778                if (itemById is not null)
 1779                {
 01780                    return itemById;
 1781                }
 1782            }
 1783
 01784            var item = FindLinkedChild(info);
 1785
 1786            // If still null, log
 01787            if (item is null)
 1788            {
 1789                // Don't keep searching over and over
 01790                info.ItemId = Guid.Empty;
 1791            }
 1792            else
 1793            {
 1794                // Cache the id for next time
 01795                info.ItemId = item.Id;
 1796            }
 1797
 01798            return item;
 1799        }
 1800
 1801        private BaseItem FindLinkedChild(LinkedChild info)
 1802        {
 01803            var path = info.Path;
 1804
 01805            if (!string.IsNullOrEmpty(path))
 1806            {
 01807                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 1808
 01809                var itemByPath = LibraryManager.FindByPath(path, null);
 1810
 01811                if (itemByPath is null)
 1812                {
 01813                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1814                }
 1815
 01816                return itemByPath;
 1817            }
 1818
 01819            if (!string.IsNullOrEmpty(info.LibraryItemId))
 1820            {
 01821                var item = LibraryManager.GetItemById(info.LibraryItemId);
 1822
 01823                if (item is null)
 1824                {
 01825                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1826                }
 1827
 01828                return item;
 1829            }
 1830
 01831            return null;
 1832        }
 1833
 1834        /// <summary>
 1835        /// Adds a studio to the item.
 1836        /// </summary>
 1837        /// <param name="name">The name.</param>
 1838        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1839        public void AddStudio(string name)
 1840        {
 41841            ArgumentException.ThrowIfNullOrEmpty(name);
 41842            var current = Studios;
 1843
 41844            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 1845            {
 41846                int curLen = current.Length;
 41847                if (curLen == 0)
 1848                {
 41849                    Studios = [name];
 1850                }
 1851                else
 1852                {
 01853                    Studios = [.. current, name];
 1854                }
 1855            }
 01856        }
 1857
 1858        public void SetStudios(IEnumerable<string> names)
 1859        {
 01860            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 01861        }
 1862
 1863        /// <summary>
 1864        /// Adds a genre to the item.
 1865        /// </summary>
 1866        /// <param name="name">The name.</param>
 1867        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1868        public void AddGenre(string name)
 1869        {
 131870            ArgumentException.ThrowIfNullOrEmpty(name);
 1871
 131872            var genres = Genres;
 131873            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 1874            {
 131875                Genres = [.. genres, name];
 1876            }
 131877        }
 1878
 1879        /// <summary>
 1880        /// Marks the played.
 1881        /// </summary>
 1882        /// <param name="user">The user.</param>
 1883        /// <param name="datePlayed">The date played.</param>
 1884        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1885        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1886        public virtual void MarkPlayed(
 1887            User user,
 1888            DateTime? datePlayed,
 1889            bool resetPosition)
 1890        {
 01891            ArgumentNullException.ThrowIfNull(user);
 1892
 01893            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 01894            {
 01895                Key = GetUserDataKeys().First(),
 01896            };
 1897
 01898            if (datePlayed.HasValue)
 1899            {
 1900                // Increment
 01901                data.PlayCount++;
 1902            }
 1903
 1904            // Ensure it's at least one
 01905            data.PlayCount = Math.Max(data.PlayCount, 1);
 1906
 01907            if (resetPosition)
 1908            {
 01909                data.PlaybackPositionTicks = 0;
 1910            }
 1911
 01912            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 01913            data.Played = true;
 1914
 01915            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01916        }
 1917
 1918        /// <summary>
 1919        /// Marks the unplayed.
 1920        /// </summary>
 1921        /// <param name="user">The user.</param>
 1922        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1923        public virtual void MarkUnplayed(User user)
 1924        {
 01925            ArgumentNullException.ThrowIfNull(user);
 1926
 01927            var data = UserDataManager.GetUserData(user, this);
 1928
 1929            // I think it is okay to do this here.
 1930            // if this is only called when a user is manually forcing something to un-played
 1931            // then it probably is what we want to do...
 01932            data.PlayCount = 0;
 01933            data.PlaybackPositionTicks = 0;
 01934            data.LastPlayedDate = null;
 01935            data.Played = false;
 1936
 01937            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01938        }
 1939
 1940        /// <summary>
 1941        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 1942        /// </summary>
 1943        public virtual void ChangedExternally()
 1944        {
 01945            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 01946        }
 1947
 1948        /// <summary>
 1949        /// Gets an image.
 1950        /// </summary>
 1951        /// <param name="type">The type.</param>
 1952        /// <param name="imageIndex">Index of the image.</param>
 1953        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 1954        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 1955        public bool HasImage(ImageType type, int imageIndex)
 1956        {
 1431957            return GetImageInfo(type, imageIndex) is not null;
 1958        }
 1959
 1960        public void SetImage(ItemImageInfo image, int index)
 1961        {
 131962            if (image.Type == ImageType.Chapter)
 1963            {
 01964                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 1965            }
 1966
 131967            var existingImage = GetImageInfo(image.Type, index);
 1968
 131969            if (existingImage is null)
 1970            {
 111971                AddImage(image);
 1972            }
 1973            else
 1974            {
 21975                existingImage.Path = image.Path;
 21976                existingImage.DateModified = image.DateModified;
 21977                existingImage.Width = image.Width;
 21978                existingImage.Height = image.Height;
 21979                existingImage.BlurHash = image.BlurHash;
 1980            }
 21981        }
 1982
 1983        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 1984        {
 461985            if (type == ImageType.Chapter)
 1986            {
 01987                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 1988            }
 1989
 461990            var image = GetImageInfo(type, index);
 1991
 461992            if (image is null)
 1993            {
 451994                AddImage(GetImageInfo(file, type));
 1995            }
 1996            else
 1997            {
 11998                var imageInfo = GetImageInfo(file, type);
 1999
 12000                image.Path = file.FullName;
 12001                image.DateModified = imageInfo.DateModified;
 2002
 2003                // reset these values
 12004                image.Width = 0;
 12005                image.Height = 0;
 2006            }
 12007        }
 2008
 2009        /// <summary>
 2010        /// Deletes the image.
 2011        /// </summary>
 2012        /// <param name="type">The type.</param>
 2013        /// <param name="index">The index.</param>
 2014        /// <returns>A task.</returns>
 2015        public async Task DeleteImageAsync(ImageType type, int index)
 2016        {
 2017            var info = GetImageInfo(type, index);
 2018
 2019            if (info is null)
 2020            {
 2021                // Nothing to do
 2022                return;
 2023            }
 2024
 2025            // Remove from file system
 2026            var path = info.Path;
 2027            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2028            {
 2029                FileSystem.DeleteFile(path);
 2030            }
 2031
 2032            // Remove from item
 2033            RemoveImage(info);
 2034
 2035            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 2036        }
 2037
 2038        public void RemoveImage(ItemImageInfo image)
 2039        {
 02040            RemoveImages([image]);
 02041        }
 2042
 2043        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2044        {
 92045            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92046        }
 2047
 2048        public void AddImage(ItemImageInfo image)
 2049        {
 562050            ImageInfos = [.. ImageInfos, image];
 562051        }
 2052
 2053        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 2054         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2055
 2056        /// <summary>
 2057        /// Validates that images within the item are still on the filesystem.
 2058        /// </summary>
 2059        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2060        public bool ValidateImages()
 2061        {
 682062            List<ItemImageInfo> deletedImages = null;
 1662063            foreach (var imageInfo in ImageInfos)
 2064            {
 152065                if (!imageInfo.IsLocalFile)
 2066                {
 2067                    continue;
 2068                }
 2069
 152070                if (File.Exists(imageInfo.Path))
 2071                {
 2072                    continue;
 2073                }
 2074
 32075                (deletedImages ??= []).Add(imageInfo);
 2076            }
 2077
 682078            var anyImagesRemoved = deletedImages?.Count > 0;
 682079            if (anyImagesRemoved)
 2080            {
 22081                RemoveImages(deletedImages);
 2082            }
 2083
 682084            return anyImagesRemoved;
 2085        }
 2086
 2087        /// <summary>
 2088        /// Gets the image path.
 2089        /// </summary>
 2090        /// <param name="imageType">Type of the image.</param>
 2091        /// <param name="imageIndex">Index of the image.</param>
 2092        /// <returns>System.String.</returns>
 2093        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2094        public string GetImagePath(ImageType imageType, int imageIndex)
 22095            => GetImageInfo(imageType, imageIndex)?.Path;
 2096
 2097        /// <summary>
 2098        /// Gets the image information.
 2099        /// </summary>
 2100        /// <param name="imageType">Type of the image.</param>
 2101        /// <param name="imageIndex">Index of the image.</param>
 2102        /// <returns>ItemImageInfo.</returns>
 2103        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2104        {
 3152105            if (imageType == ImageType.Chapter)
 2106            {
 02107                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2108
 02109                if (chapter is null)
 2110                {
 02111                    return null;
 2112                }
 2113
 02114                var path = chapter.ImagePath;
 2115
 02116                if (string.IsNullOrEmpty(path))
 2117                {
 02118                    return null;
 2119                }
 2120
 02121                return new ItemImageInfo
 02122                {
 02123                    Path = path,
 02124                    DateModified = chapter.ImageDateModified,
 02125                    Type = imageType
 02126                };
 2127            }
 2128
 2129            // Music albums usually don't have dedicated backdrops, so return one from the artist instead
 3152130            if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
 2131            {
 02132                var artist = FindParent<MusicArtist>();
 2133
 02134                if (artist is not null)
 2135                {
 02136                    return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
 2137                }
 2138            }
 2139
 3152140            return GetImages(imageType)
 3152141                .ElementAtOrDefault(imageIndex);
 2142        }
 2143
 2144        /// <summary>
 2145        /// Computes image index for given image or raises if no matching image found.
 2146        /// </summary>
 2147        /// <param name="image">Image to compute index for.</param>
 2148        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2149        /// </exception>
 2150        /// <returns>Image index.</returns>
 2151        public int GetImageIndex(ItemImageInfo image)
 2152        {
 02153            ArgumentNullException.ThrowIfNull(image);
 2154
 02155            if (image.Type == ImageType.Chapter)
 2156            {
 02157                var chapters = ChapterManager.GetChapters(Id);
 02158                for (var i = 0; i < chapters.Count; i++)
 2159                {
 02160                    if (chapters[i].ImagePath == image.Path)
 2161                    {
 02162                        return i;
 2163                    }
 2164                }
 2165
 02166                throw new ArgumentException("No chapter index found for image path", image.Path);
 2167            }
 2168
 02169            var images = GetImages(image.Type).ToArray();
 02170            for (var i = 0; i < images.Length; i++)
 2171            {
 02172                if (images[i].Path == image.Path)
 2173                {
 02174                    return i;
 2175                }
 2176            }
 2177
 02178            throw new ArgumentException("No image index found for image path", image.Path);
 2179        }
 2180
 2181        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2182        {
 2183            if (imageType == ImageType.Chapter)
 2184            {
 2185                throw new ArgumentException("No image info for chapter images");
 2186            }
 2187
 2188            // Yield return is more performant than LINQ Where on an Array
 2189            for (var i = 0; i < ImageInfos.Length; i++)
 2190            {
 2191                var imageInfo = ImageInfos[i];
 2192                if (imageInfo.Type == imageType)
 2193                {
 2194                    yield return imageInfo;
 2195                }
 2196            }
 2197        }
 2198
 2199        /// <summary>
 2200        /// Adds the images, updating metadata if they already are part of this item.
 2201        /// </summary>
 2202        /// <param name="imageType">Type of the image.</param>
 2203        /// <param name="images">The images.</param>
 2204        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2205        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2206        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2207        {
 42208            if (imageType == ImageType.Chapter)
 2209            {
 02210                throw new ArgumentException("Cannot call AddImages with chapter images");
 2211            }
 2212
 42213            var existingImages = GetImages(imageType)
 42214                .ToList();
 2215
 42216            var newImageList = new List<FileSystemMetadata>();
 42217            var imageUpdated = false;
 2218
 242219            foreach (var newImage in images)
 2220            {
 82221                if (newImage is null)
 2222                {
 02223                    throw new ArgumentException("null image found in list");
 2224                }
 2225
 82226                var existing = existingImages
 82227                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2228
 82229                if (existing is null)
 2230                {
 42231                    newImageList.Add(newImage);
 2232                }
 2233                else
 2234                {
 42235                    if (existing.IsLocalFile)
 2236                    {
 42237                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2238
 2239                        // If date changed then we need to reset saved image dimensions
 42240                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2241                        {
 22242                            existing.Width = 0;
 22243                            existing.Height = 0;
 22244                            imageUpdated = true;
 2245                        }
 2246
 42247                        existing.DateModified = newDateModified;
 2248                    }
 2249                }
 2250            }
 2251
 42252            if (newImageList.Count > 0)
 2253            {
 22254                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2255            }
 2256
 42257            return imageUpdated || newImageList.Count > 0;
 2258        }
 2259
 2260        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2261        {
 502262            return new ItemImageInfo
 502263            {
 502264                Path = file.FullName,
 502265                Type = type,
 502266                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502267            };
 2268        }
 2269
 2270        /// <summary>
 2271        /// Gets the file system path to delete when the item is to be deleted.
 2272        /// </summary>
 2273        /// <returns>The metadata for the deleted paths.</returns>
 2274        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2275        {
 02276            return new[]
 02277            {
 02278                FileSystem.GetFileSystemInfo(Path)
 02279            }.Concat(GetLocalMetadataFilesToDelete());
 2280        }
 2281
 2282        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2283        {
 02284            if (IsFolder || !IsInMixedFolder)
 2285            {
 02286                return [];
 2287            }
 2288
 02289            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2290
 02291            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02292                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02293                .ToList();
 2294        }
 2295
 2296        public bool AllowsMultipleImages(ImageType type)
 2297        {
 182298            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2299        }
 2300
 2301        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2302        {
 02303            if (!AllowsMultipleImages(type))
 2304            {
 02305                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2306            }
 2307
 02308            var info1 = GetImageInfo(type, index1);
 02309            var info2 = GetImageInfo(type, index2);
 2310
 02311            if (info1 is null || info2 is null)
 2312            {
 2313                // Nothing to do
 02314                return Task.CompletedTask;
 2315            }
 2316
 02317            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2318            {
 2319                // TODO: Not supported  yet
 02320                return Task.CompletedTask;
 2321            }
 2322
 02323            var path1 = info1.Path;
 02324            var path2 = info2.Path;
 2325
 02326            FileSystem.SwapFiles(path1, path2);
 2327
 2328            // Refresh these values
 02329            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02330            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2331
 02332            info1.Width = 0;
 02333            info1.Height = 0;
 02334            info2.Width = 0;
 02335            info2.Height = 0;
 2336
 02337            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2338        }
 2339
 2340        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2341        {
 02342            userItemData ??= UserDataManager.GetUserData(user, this);
 2343
 02344            return userItemData is not null && userItemData.Played;
 2345        }
 2346
 2347        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2348        {
 02349            userItemData ??= UserDataManager.GetUserData(user, this);
 2350
 02351            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2352        }
 2353
 2354        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2355        {
 02356            ArgumentNullException.ThrowIfNull(user);
 2357
 02358            userItemData ??= UserDataManager.GetUserData(user, this);
 2359
 02360            return userItemData is null || !userItemData.Played;
 2361        }
 2362
 2363        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2364        {
 542365            return GetItemLookupInfo<ItemLookupInfo>();
 2366        }
 2367
 2368        protected T GetItemLookupInfo<T>()
 2369            where T : ItemLookupInfo, new()
 2370        {
 542371            return new T
 542372            {
 542373                Path = Path,
 542374                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 542375                MetadataLanguage = GetPreferredMetadataLanguage(),
 542376                Name = GetNameForMetadataLookup(),
 542377                OriginalTitle = OriginalTitle,
 542378                ProviderIds = ProviderIds,
 542379                IndexNumber = IndexNumber,
 542380                ParentIndexNumber = ParentIndexNumber,
 542381                Year = ProductionYear,
 542382                PremiereDate = PremiereDate
 542383            };
 2384        }
 2385
 2386        protected virtual string GetNameForMetadataLookup()
 2387        {
 542388            return Name;
 2389        }
 2390
 2391        /// <summary>
 2392        /// This is called before any metadata refresh and returns true if changes were made.
 2393        /// </summary>
 2394        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2395        /// <returns>true if the item has change, else false.</returns>
 2396        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2397        {
 542398            _sortName = null;
 2399
 542400            var hasChanges = false;
 2401
 542402            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2403            {
 02404                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02405                hasChanges = true;
 2406            }
 2407
 542408            return hasChanges;
 2409        }
 2410
 2411        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2412        {
 02413            if (protocol == MediaProtocol.File)
 2414            {
 02415                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2416            }
 2417
 02418            return path;
 2419        }
 2420
 2421        public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User 
 2422        {
 02423            if (RunTimeTicks.HasValue)
 2424            {
 02425                double pct = RunTimeTicks.Value;
 2426
 02427                if (pct > 0)
 2428                {
 02429                    pct = userData.PlaybackPositionTicks / pct;
 2430
 02431                    if (pct > 0)
 2432                    {
 02433                        dto.PlayedPercentage = 100 * pct;
 2434                    }
 2435                }
 2436            }
 02437        }
 2438
 2439        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2440        {
 2441            var newOptions = new MetadataRefreshOptions(options)
 2442            {
 2443                SearchResult = null
 2444            };
 2445
 2446            var item = this;
 2447
 2448            if (copyTitleMetadata)
 2449            {
 2450                // Take some data from the main item, for querying purposes
 2451                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2452                {
 2453                    newOptions.ForceSave = true;
 2454                    ownedItem.Genres = item.Genres;
 2455                }
 2456
 2457                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2458                {
 2459                    newOptions.ForceSave = true;
 2460                    ownedItem.Studios = item.Studios;
 2461                }
 2462
 2463                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2464                {
 2465                    newOptions.ForceSave = true;
 2466                    ownedItem.ProductionLocations = item.ProductionLocations;
 2467                }
 2468
 2469                if (item.CommunityRating != ownedItem.CommunityRating)
 2470                {
 2471                    ownedItem.CommunityRating = item.CommunityRating;
 2472                    newOptions.ForceSave = true;
 2473                }
 2474
 2475                if (item.CriticRating != ownedItem.CriticRating)
 2476                {
 2477                    ownedItem.CriticRating = item.CriticRating;
 2478                    newOptions.ForceSave = true;
 2479                }
 2480
 2481                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2482                {
 2483                    ownedItem.Overview = item.Overview;
 2484                    newOptions.ForceSave = true;
 2485                }
 2486
 2487                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2488                {
 2489                    ownedItem.OfficialRating = item.OfficialRating;
 2490                    newOptions.ForceSave = true;
 2491                }
 2492
 2493                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2494                {
 2495                    ownedItem.CustomRating = item.CustomRating;
 2496                    newOptions.ForceSave = true;
 2497                }
 2498            }
 2499
 2500            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 2501        }
 2502
 2503        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2504        {
 2505            var newOptions = new MetadataRefreshOptions(options)
 2506            {
 2507                SearchResult = null
 2508            };
 2509
 2510            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2511
 2512            // Try to retrieve it from the db. If we don't find it, use the resolved version
 2513            if (LibraryManager.GetItemById(id) is not Video video)
 2514            {
 2515                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2516
 2517                newOptions.ForceSave = true;
 2518            }
 2519
 2520            if (video is null)
 2521            {
 2522                return;
 2523            }
 2524
 2525            if (video.OwnerId.IsEmpty())
 2526            {
 2527                video.OwnerId = Id;
 2528            }
 2529
 2530            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 2531        }
 2532
 2533        public string GetEtag(User user)
 2534        {
 62535            var list = GetEtagValues(user);
 2536
 62537            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2538        }
 2539
 2540        protected virtual List<string> GetEtagValues(User user)
 2541        {
 62542            return
 62543            [
 62544                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62545            ];
 2546        }
 2547
 2548        public virtual IEnumerable<Guid> GetAncestorIds()
 2549        {
 1112550            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2551        }
 2552
 2553        public BaseItem GetTopParent()
 2554        {
 1112555            if (IsTopParent)
 2556            {
 212557                return this;
 2558            }
 2559
 902560            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2561        }
 2562
 2563        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2564        {
 442565            return [Id];
 2566        }
 2567
 2568        public virtual double? GetRefreshProgress()
 2569        {
 02570            return null;
 2571        }
 2572
 2573        public virtual ItemUpdateType OnMetadataChanged()
 2574        {
 1082575            var updateType = ItemUpdateType.None;
 2576
 1082577            var item = this;
 2578
 1082579            var rating = item.GetParentalRatingScore();
 1082580            if (rating is not null)
 2581            {
 02582                if (rating.Score != item.InheritedParentalRatingValue)
 2583                {
 02584                    item.InheritedParentalRatingValue = rating.Score;
 02585                    updateType |= ItemUpdateType.MetadataImport;
 2586                }
 2587
 02588                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2589                {
 02590                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02591                    updateType |= ItemUpdateType.MetadataImport;
 2592                }
 2593            }
 2594            else
 2595            {
 1082596                if (item.InheritedParentalRatingValue is not null)
 2597                {
 02598                    item.InheritedParentalRatingValue = null;
 02599                    item.InheritedParentalRatingSubValue = null;
 02600                    updateType |= ItemUpdateType.MetadataImport;
 2601                }
 2602            }
 2603
 1082604            return updateType;
 2605        }
 2606
 2607        /// <summary>
 2608        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2609        /// </summary>
 2610        /// <param name="children">Media children.</param>
 2611        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2612        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2613        {
 02614            var currentOfficialRating = OfficialRating;
 2615
 2616            // Gather all possible ratings
 02617            var ratings = children
 02618                .Select(i => i.OfficialRating)
 02619                .Where(i => !string.IsNullOrEmpty(i))
 02620                .Distinct(StringComparer.OrdinalIgnoreCase)
 02621                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
 02622                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02623                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02624                .Select(i => i.rating);
 2625
 02626            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2627
 02628            return !string.Equals(
 02629                currentOfficialRating ?? string.Empty,
 02630                OfficialRating ?? string.Empty,
 02631                StringComparison.OrdinalIgnoreCase);
 2632        }
 2633
 2634        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2635        {
 02636            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2637        }
 2638
 2639        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2640        {
 02641            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, 
 2642        }
 2643
 2644        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2645        {
 02646            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2647        }
 2648
 2649        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2650        {
 02651            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user,
 2652        }
 2653
 2654        /// <summary>
 2655        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2656        /// </summary>
 2657        /// <returns>An enumerable containing the items.</returns>
 2658        public IEnumerable<BaseItem> GetExtras()
 2659        {
 62660            return ExtraIds
 62661                .Select(LibraryManager.GetItemById)
 62662                .Where(i => i is not null)
 62663                .OrderBy(i => i.SortName);
 2664        }
 2665
 2666        /// <summary>
 2667        /// Get all extras with specific types that are associated with this item.
 2668        /// </summary>
 2669        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2670        /// <returns>An enumerable containing the extras.</returns>
 2671        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
 2672        {
 02673            return ExtraIds
 02674                .Select(LibraryManager.GetItemById)
 02675                .Where(i => i is not null)
 02676                .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value))
 02677                .OrderBy(i => i.SortName);
 2678        }
 2679
 2680        public virtual long GetRunTimeTicksForPlayState()
 2681        {
 02682            return RunTimeTicks ?? 0;
 2683        }
 2684
 2685        /// <inheritdoc />
 2686        public override bool Equals(object obj)
 2687        {
 02688            return obj is BaseItem baseItem && this.Equals(baseItem);
 2689        }
 2690
 2691        /// <inheritdoc />
 52692        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2693
 2694        /// <inheritdoc />
 612695        public override int GetHashCode() => HashCode.Combine(Id);
 2696    }
 2697}

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()
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)
IsVisibleStandaloneInternal(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
SetParent(MediaBrowser.Controller.Entities.Folder)
GetFileSystemChildren(MediaBrowser.Controller.Providers.IDirectoryService)
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)
RemoveImage(MediaBrowser.Controller.Entities.ItemImageInfo)
RemoveImages(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.ItemImageInfo>)
AddImage(MediaBrowser.Controller.Entities.ItemImageInfo)
ValidateImages()
GetImagePath(MediaBrowser.Model.Entities.ImageType,System.Int32)
GetImageInfo(MediaBrowser.Model.Entities.ImageType,System.Int32)
GetImageIndex(MediaBrowser.Controller.Entities.ItemImageInfo)
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)
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()