< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.BaseItem
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs
Line coverage
45%
Covered lines: 404
Uncovered lines: 483
Coverable lines: 887
Total lines: 2773
Line coverage: 45.5%
Branch coverage
36%
Covered branches: 202
Total branches: 552
Branch coverage: 36.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

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

Coverage delta

Coverage delta 4 -4

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
get_SupportsAddingToPlaylist()100%210%
get_AlwaysScanInternalMetadataPath()100%11100%
get_SupportsPlayedStatus()100%210%
get_SupportsPositionTicksResume()100%210%
get_SupportsRemoteImageDownloading()100%11100%
get_Name()100%11100%
set_Name(...)100%11100%
get_IsUnaired()0%620%
get_IsThemeMedia()0%2040%
get_DisplayPreferencesId()50%22100%
get_SourceType()50%2266.66%
get_ContainingFolderPath()100%22100%
get_IsHidden()100%210%
get_LocationType()33.33%11650%
get_PathProtocol()100%22100%
get_IsFileProtocol()100%11100%
get_HasPathProtocol()100%210%
get_SupportsLocalMetadata()50%2266.66%
get_FileNameWithoutExtension()0%620%
get_EnableAlphaNumericSorting()100%11100%
get_IsHD()100%11100%
get_PrimaryImagePath()100%210%
get_MediaType()100%11100%
get_PhysicalLocations()0%620%
get_EnableMediaSourceDisplay()50%2266.66%
get_ForcedSortName()100%11100%
set_ForcedSortName(...)100%11100%
get_SortName()75%4480%
set_SortName(...)100%11100%
get_DisplayParentId()100%11100%
get_DisplayParent()100%22100%
get_HasLocalAlternateVersions()100%210%
get_OfficialRatingForComparison()75%4485.71%
get_CustomRatingForComparison()100%11100%
get_LatestItemsIndexContainer()100%210%
get_EnableRememberingTrackSelections()100%11100%
get_IsTopParent()80%121075%
get_SupportsAncestors()100%11100%
get_SupportsOwnedItems()100%22100%
get_SupportsPeople()100%11100%
get_SupportsThemeMedia()100%210%
get_SupportsInheritedParentImages()100%210%
get_IsFolder()100%11100%
get_IsDisplayedAsFolder()100%210%
GetCustomRatingForComparision(...)87.5%8888.88%
GetDefaultPrimaryImageAspectRatio()100%210%
CreatePresentationUniqueKey()100%11100%
CanDelete()0%620%
IsAuthorizedToDelete(...)0%7280%
GetOwner()50%22100%
CanDelete(...)50%22100%
CanDelete(...)100%11100%
CanDownload()100%11100%
IsAuthorizedToDownload(...)100%210%
CanDownload(...)50%22100%
ToString()100%11100%
GetInternalMetadataPath()100%11100%
GetInternalMetadataPath(...)50%2275%
CreateSortName()71.42%171475%
ModifySortChunks(...)87.5%8893.75%
GetParent()100%22100%
GetParents()100%22100%
FindParent()0%2040%
GetPlayAccess(...)50%2266.66%
GetMediaStreams()100%210%
IsActiveRecording()100%210%
GetMediaSources(...)0%7280%
GetAllItemsForMediaSources()100%210%
GetVersionInfo(...)0%2970540%
GetMediaSourceName(...)68.75%743265.51%
RefreshMetadata(...)100%11100%
RefreshMetadata()62.5%8882.35%
IsVisibleStandaloneInternal(...)0%342180%
SetParent(...)50%22100%
RefreshedOwnedItems()62.5%201675%
GetFileSystemChildren(...)100%11100%
RefreshExtras()0%156120%
GetPresentationUniqueKey()0%620%
RequiresRefresh()50%11650%
GetUserDataKeys()50%5466.66%
UpdateFromResolvedItem(...)50%2260%
AfterMetadataRefresh()100%11100%
GetPreferredMetadataLanguage()100%88100%
GetPreferredMetadataCountryCode()100%88100%
IsSaveLocalMetadataEnabled()50%2275%
IsParentalAllowed(...)25%641642.85%
GetParentalRatingScore()75%4483.33%
GetInheritedTags()75%4485.71%
IsVisibleViaTags(...)9.09%3022216.66%
GetBlockUnratedType()50%2266.66%
GetBlockUnratedValue(...)50%5466.66%
IsVisible(...)100%11100%
IsVisibleStandalone(...)0%2040%
GetClientTypeName()70%141066.66%
GetBaseItemKind()100%22100%
GetLinkedChild(...)0%7280%
FindLinkedChild(...)0%210140%
AddStudio(...)50%4475%
SetStudios(...)100%210%
AddGenre(...)100%22100%
MarkPlayed(...)0%110100%
MarkUnplayed(...)100%210%
ChangedExternally()100%210%
HasImage(...)100%11100%
SetImage(...)75%4490.9%
SetImagePath(...)75%4490.9%
DeleteImageAsync()0%4260%
RemoveImage(...)100%210%
RemoveImages(...)100%11100%
AddImage(...)100%11100%
UpdateToRepositoryAsync()100%11100%
ReattachUserDataAsync()100%11100%
ValidateImages()100%1212100%
GetImagePath(...)50%22100%
GetImageInfo(...)16.66%24620%
GetImageIndex(...)0%110100%
GetImages()83.33%6685.71%
AddImages(...)85%202091.3%
GetImageInfo(...)100%11100%
GetDeletePaths()100%210%
GetLocalMetadataFilesToDelete()0%2040%
AllowsMultipleImages(...)100%22100%
SwapImagesAsync(...)0%110100%
IsPlayed(...)0%2040%
IsFavoriteOrLiked(...)0%4260%
IsUnplayed(...)0%2040%
MediaBrowser.Controller.Providers.IHasLookupInfo<MediaBrowser.Controller.Providers.ItemLookupInfo>.GetLookupInfo()100%11100%
GetItemLookupInfo()100%11100%
GetNameForMetadataLookup()100%11100%
BeforeMetadataRefresh(...)50%5466.66%
GetMappedPath(...)0%620%
FillUserDataDtoValues(...)0%4260%
RefreshMetadataForOwnedItem()0%342180%
RefreshMetadataForOwnedVideo()0%4260%
GetEtag(...)100%11100%
GetEtagValues(...)100%11100%
GetAncestorIds()100%11100%
GetTopParent()100%22100%
GetIdsForAncestorQuery()100%11100%
GetRefreshProgress()100%210%
OnMetadataChanged()37.5%22840%
UpdateRatingToItems(...)0%4260%
GetThemeSongs(...)100%210%
GetThemeSongs(...)100%210%
GetThemeVideos(...)100%210%
GetThemeVideos(...)100%210%
GetExtras()100%11100%
GetExtras(...)100%210%
GetRunTimeTicksForPlayState()100%210%
Equals(...)0%620%
Equals(...)50%22100%
GetHashCode()100%11100%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591, SA1401
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Text;
 12using System.Text.Json.Serialization;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Common.Extensions;
 21using MediaBrowser.Controller.Channels;
 22using MediaBrowser.Controller.Chapters;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Dto;
 25using MediaBrowser.Controller.Entities.TV;
 26using MediaBrowser.Controller.IO;
 27using MediaBrowser.Controller.Library;
 28using MediaBrowser.Controller.MediaSegments;
 29using MediaBrowser.Controller.Persistence;
 30using MediaBrowser.Controller.Providers;
 31using MediaBrowser.Model.Dto;
 32using MediaBrowser.Model.Entities;
 33using MediaBrowser.Model.Globalization;
 34using MediaBrowser.Model.IO;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.MediaInfo;
 38using Microsoft.Extensions.Logging;
 39
 40namespace MediaBrowser.Controller.Entities
 41{
 42    /// <summary>
 43    /// Class BaseItem.
 44    /// </summary>
 45    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 46    {
 47        private BaseItemKind? _baseItemKind;
 48
 49        public const string ThemeSongFileName = "theme";
 50
 51        /// <summary>
 52        /// The supported image extensions.
 53        /// </summary>
 454        public static readonly string[] SupportedImageExtensions
 455            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 56
 457        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 458        {
 459            ".nfo",
 460            ".xml",
 461            ".srt",
 462            ".vtt",
 463            ".sub",
 464            ".sup",
 465            ".idx",
 466            ".txt",
 467            ".edl",
 468            ".bif",
 469            ".smi",
 470            ".ttml",
 471            ".lrc",
 472            ".elrc"
 473        };
 74
 75        /// <summary>
 76        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 77        /// </summary>
 478        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 479        {
 480            Model.Entities.ExtraType.Unknown,
 481            Model.Entities.ExtraType.BehindTheScenes,
 482            Model.Entities.ExtraType.Clip,
 483            Model.Entities.ExtraType.DeletedScene,
 484            Model.Entities.ExtraType.Interview,
 485            Model.Entities.ExtraType.Sample,
 486            Model.Entities.ExtraType.Scene,
 487            Model.Entities.ExtraType.Featurette,
 488            Model.Entities.ExtraType.Short
 489        };
 90
 91        private string _sortName;
 92
 93        private string _forcedSortName;
 94
 95        private string _name;
 96
 97        public const char SlugChar = '-';
 98
 99        protected BaseItem()
 100        {
 1114101            Tags = Array.Empty<string>();
 1114102            Genres = Array.Empty<string>();
 1114103            Studios = Array.Empty<string>();
 1114104            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 1114105            LockedFields = Array.Empty<MetadataField>();
 1114106            ImageInfos = Array.Empty<ItemImageInfo>();
 1114107            ProductionLocations = Array.Empty<string>();
 1114108            RemoteTrailers = Array.Empty<MediaUrl>();
 1114109            UserData = [];
 1114110        }
 111
 112        /// <summary>
 113        /// Gets or Sets the user data collection as cached from the last Db query.
 114        /// </summary>
 115        [JsonIgnore]
 116        public ICollection<UserData> UserData { get; set; }
 117
 118        [JsonIgnore]
 119        public string PreferredMetadataCountryCode { get; set; }
 120
 121        [JsonIgnore]
 122        public string PreferredMetadataLanguage { get; set; }
 123
 124        public long? Size { get; set; }
 125
 126        public string Container { get; set; }
 127
 128        [JsonIgnore]
 129        public string Tagline { get; set; }
 130
 131        [JsonIgnore]
 132        public virtual ItemImageInfo[] ImageInfos { get; set; }
 133
 134        [JsonIgnore]
 135        public bool IsVirtualItem { get; set; }
 136
 137        /// <summary>
 138        /// Gets or sets the album.
 139        /// </summary>
 140        /// <value>The album.</value>
 141        [JsonIgnore]
 142        public string Album { get; set; }
 143
 144        /// <summary>
 145        /// Gets or sets the LUFS value.
 146        /// </summary>
 147        /// <value>The LUFS Value.</value>
 148        [JsonIgnore]
 149        public float? LUFS { get; set; }
 150
 151        /// <summary>
 152        /// Gets or sets the gain required for audio normalization.
 153        /// </summary>
 154        /// <value>The gain required for audio normalization.</value>
 155        [JsonIgnore]
 156        public float? NormalizationGain { get; set; }
 157
 158        /// <summary>
 159        /// Gets or sets the channel identifier.
 160        /// </summary>
 161        /// <value>The channel identifier.</value>
 162        [JsonIgnore]
 163        public Guid ChannelId { get; set; }
 164
 165        [JsonIgnore]
 0166        public virtual bool SupportsAddingToPlaylist => false;
 167
 168        [JsonIgnore]
 14169        public virtual bool AlwaysScanInternalMetadataPath => false;
 170
 171        /// <summary>
 172        /// Gets or sets a value indicating whether this instance is in mixed folder.
 173        /// </summary>
 174        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 175        [JsonIgnore]
 176        public bool IsInMixedFolder { get; set; }
 177
 178        [JsonIgnore]
 0179        public virtual bool SupportsPlayedStatus => false;
 180
 181        [JsonIgnore]
 0182        public virtual bool SupportsPositionTicksResume => false;
 183
 184        [JsonIgnore]
 17185        public virtual bool SupportsRemoteImageDownloading => true;
 186
 187        /// <summary>
 188        /// Gets or sets the name.
 189        /// </summary>
 190        /// <value>The name.</value>
 191        [JsonIgnore]
 192        public virtual string Name
 193        {
 998194            get => _name;
 195            set
 196            {
 388197                _name = value;
 198
 199                // lazy load this again
 388200                _sortName = null;
 388201            }
 202        }
 203
 204        [JsonIgnore]
 0205        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 206
 207        [JsonIgnore]
 208        public int? TotalBitrate { get; set; }
 209
 210        [JsonIgnore]
 211        public ExtraType? ExtraType { get; set; }
 212
 213        [JsonIgnore]
 0214        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 215
 216        [JsonIgnore]
 217        public string OriginalTitle { get; set; }
 218
 219        [JsonIgnore]
 220        public string OriginalLanguage { get; set; }
 221
 222        /// <summary>
 223        /// Gets or sets the id.
 224        /// </summary>
 225        /// <value>The id.</value>
 226        [JsonIgnore]
 227        public Guid Id { get; set; }
 228
 229        [JsonIgnore]
 230        public Guid OwnerId { get; set; }
 231
 232        /// <summary>
 233        /// Gets or sets the audio.
 234        /// </summary>
 235        /// <value>The audio.</value>
 236        [JsonIgnore]
 237        public ProgramAudio? Audio { get; set; }
 238
 239        /// <summary>
 240        /// Gets the id that should be used to key display prefs for this item.
 241        /// Default is based on the type for everything except actual generic folders.
 242        /// </summary>
 243        /// <value>The display prefs id.</value>
 244        [JsonIgnore]
 245        public virtual Guid DisplayPreferencesId
 246        {
 247            get
 248            {
 6249                var thisType = GetType();
 6250                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 251            }
 252        }
 253
 254        /// <summary>
 255        /// Gets or sets the path.
 256        /// </summary>
 257        /// <value>The path.</value>
 258        [JsonIgnore]
 259        public virtual string Path { get; set; }
 260
 261        [JsonIgnore]
 262        public virtual SourceType SourceType
 263        {
 264            get
 265            {
 2073266                if (!ChannelId.IsEmpty())
 267                {
 0268                    return SourceType.Channel;
 269                }
 270
 2073271                return SourceType.Library;
 272            }
 273        }
 274
 275        /// <summary>
 276        /// Gets the folder containing the item.
 277        /// If the item is a folder, it returns the folder itself.
 278        /// </summary>
 279        [JsonIgnore]
 280        public virtual string ContainingFolderPath
 281        {
 282            get
 283            {
 447284                if (IsFolder)
 285                {
 388286                    return Path;
 287                }
 288
 59289                return System.IO.Path.GetDirectoryName(Path);
 290            }
 291        }
 292
 293        /// <summary>
 294        /// Gets or sets the name of the service.
 295        /// </summary>
 296        /// <value>The name of the service.</value>
 297        [JsonIgnore]
 298        public string ServiceName { get; set; }
 299
 300        /// <summary>
 301        /// Gets or sets the external id.
 302        /// </summary>
 303        /// <remarks>
 304        /// If this content came from an external service, the id of the content on that service.
 305        /// </remarks>
 306        [JsonIgnore]
 307        public string ExternalId { get; set; }
 308
 309        [JsonIgnore]
 310        public string ExternalSeriesId { get; set; }
 311
 312        [JsonIgnore]
 0313        public virtual bool IsHidden => false;
 314
 315        /// <summary>
 316        /// Gets the type of the location.
 317        /// </summary>
 318        /// <value>The type of the location.</value>
 319        [JsonIgnore]
 320        public virtual LocationType LocationType
 321        {
 322            get
 323            {
 9324                var path = Path;
 9325                if (string.IsNullOrEmpty(path))
 326                {
 0327                    if (SourceType == SourceType.Channel)
 328                    {
 0329                        return LocationType.Remote;
 330                    }
 331
 0332                    return LocationType.Virtual;
 333                }
 334
 9335                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 336            }
 337        }
 338
 339        [JsonIgnore]
 340        public MediaProtocol? PathProtocol
 341        {
 342            get
 343            {
 2442344                var path = Path;
 345
 2442346                if (string.IsNullOrEmpty(path))
 347                {
 116348                    return null;
 349                }
 350
 2326351                return MediaSourceManager.GetPathProtocol(path);
 352            }
 353        }
 354
 355        [JsonIgnore]
 2421356        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 357
 358        [JsonIgnore]
 0359        public bool HasPathProtocol => PathProtocol.HasValue;
 360
 361        [JsonIgnore]
 362        public virtual bool SupportsLocalMetadata
 363        {
 364            get
 365            {
 1286366                if (SourceType == SourceType.Channel)
 367                {
 0368                    return false;
 369                }
 370
 1286371                return IsFileProtocol;
 372            }
 373        }
 374
 375        [JsonIgnore]
 376        public virtual string FileNameWithoutExtension
 377        {
 378            get
 379            {
 0380                if (IsFileProtocol)
 381                {
 0382                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 383                }
 384
 0385                return null;
 386            }
 387        }
 388
 389        [JsonIgnore]
 100390        public virtual bool EnableAlphaNumericSorting => true;
 391
 139392        public virtual bool IsHD => Height >= 720;
 393
 394        public bool IsShortcut { get; set; }
 395
 396        public string ShortcutPath { get; set; }
 397
 398        public int Width { get; set; }
 399
 400        public int Height { get; set; }
 401
 402        /// <summary>
 403        /// Gets the primary image path.
 404        /// </summary>
 405        /// <remarks>
 406        /// This is just a helper for convenience.
 407        /// </remarks>
 408        /// <value>The primary image path.</value>
 409        [JsonIgnore]
 0410        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 411
 412        /// <summary>
 413        /// Gets or sets the date created.
 414        /// </summary>
 415        /// <value>The date created.</value>
 416        [JsonIgnore]
 417        public DateTime DateCreated { get; set; }
 418
 419        /// <summary>
 420        /// Gets or sets the date modified.
 421        /// </summary>
 422        /// <value>The date modified.</value>
 423        [JsonIgnore]
 424        public DateTime DateModified { get; set; }
 425
 426        public DateTime DateLastSaved { get; set; }
 427
 428        [JsonIgnore]
 429        public DateTime DateLastRefreshed { get; set; }
 430
 431        [JsonIgnore]
 432        public bool IsLocked { get; set; }
 433
 434        /// <summary>
 435        /// Gets or sets the locked fields.
 436        /// </summary>
 437        /// <value>The locked fields.</value>
 438        [JsonIgnore]
 439        public MetadataField[] LockedFields { get; set; }
 440
 441        /// <summary>
 442        /// Gets the type of the media.
 443        /// </summary>
 444        /// <value>The type of the media.</value>
 445        [JsonIgnore]
 130446        public virtual MediaType MediaType => MediaType.Unknown;
 447
 448        [JsonIgnore]
 449        public virtual string[] PhysicalLocations
 450        {
 451            get
 452            {
 0453                if (!IsFileProtocol)
 454                {
 0455                    return Array.Empty<string>();
 456                }
 457
 0458                return [Path];
 459            }
 460        }
 461
 462        [JsonIgnore]
 463        public bool EnableMediaSourceDisplay
 464        {
 465            get
 466            {
 6467                if (SourceType == SourceType.Channel)
 468                {
 0469                    return ChannelManager.EnableMediaSourceDisplay(this);
 470                }
 471
 6472                return true;
 473            }
 474        }
 475
 476        [JsonIgnore]
 477        public Guid ParentId { get; set; }
 478
 479        /// <summary>
 480        /// Gets or sets the logger.
 481        /// </summary>
 482        public static ILogger<BaseItem> Logger { get; set; }
 483
 484        public static ILibraryManager LibraryManager { get; set; }
 485
 486        public static IServerConfigurationManager ConfigurationManager { get; set; }
 487
 488        public static IProviderManager ProviderManager { get; set; }
 489
 490        public static ILocalizationManager LocalizationManager { get; set; }
 491
 492        public static IItemRepository ItemRepository { get; set; }
 493
 494        public static IItemCountService ItemCountService { get; set; }
 495
 496        public static IChapterManager ChapterManager { get; set; }
 497
 498        public static IFileSystem FileSystem { get; set; }
 499
 500        public static IUserDataManager UserDataManager { get; set; }
 501
 502        public static IChannelManager ChannelManager { get; set; }
 503
 504        public static IMediaSourceManager MediaSourceManager { get; set; }
 505
 506        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 507
 508        /// <summary>
 509        /// Gets or sets the name of the forced sort.
 510        /// </summary>
 511        /// <value>The name of the forced sort.</value>
 512        [JsonIgnore]
 513        public string ForcedSortName
 514        {
 478515            get => _forcedSortName;
 516            set
 517            {
 88518                _forcedSortName = value;
 88519                _sortName = null;
 88520            }
 521        }
 522
 523        /// <summary>
 524        /// Gets or sets the name of the sort.
 525        /// </summary>
 526        /// <value>The name of the sort.</value>
 527        [JsonIgnore]
 528        public string SortName
 529        {
 530            get
 531            {
 170532                if (_sortName is null)
 533                {
 100534                    if (!string.IsNullOrEmpty(ForcedSortName))
 535                    {
 536                        // Need the ToLower because that's what CreateSortName does
 0537                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 538                    }
 539                    else
 540                    {
 100541                        _sortName = CreateSortName();
 542                    }
 543                }
 544
 170545                return _sortName;
 546            }
 547
 127548            set => _sortName = value;
 549        }
 550
 551        [JsonIgnore]
 330552        public virtual Guid DisplayParentId => ParentId;
 553
 554        [JsonIgnore]
 555        public BaseItem DisplayParent
 556        {
 557            get
 558            {
 324559                var id = DisplayParentId;
 324560                if (id.IsEmpty())
 561                {
 248562                    return null;
 563                }
 564
 76565                return LibraryManager.GetItemById(id);
 566            }
 567        }
 568
 569        /// <summary>
 570        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 571        /// </summary>
 572        /// <value>The premiere date.</value>
 573        [JsonIgnore]
 574        public DateTime? PremiereDate { get; set; }
 575
 576        /// <summary>
 577        /// Gets or sets the end date.
 578        /// </summary>
 579        /// <value>The end date.</value>
 580        [JsonIgnore]
 581        public DateTime? EndDate { get; set; }
 582
 583        /// <summary>
 584        /// Gets or sets the official rating.
 585        /// </summary>
 586        /// <value>The official rating.</value>
 587        [JsonIgnore]
 588        public string OfficialRating { get; set; }
 589
 590        [JsonIgnore]
 591        public int? InheritedParentalRatingValue { get; set; }
 592
 593        [JsonIgnore]
 594        public int? InheritedParentalRatingSubValue { get; set; }
 595
 596        /// <summary>
 597        /// Gets or sets the critic rating.
 598        /// </summary>
 599        /// <value>The critic rating.</value>
 600        [JsonIgnore]
 601        public float? CriticRating { get; set; }
 602
 603        /// <summary>
 604        /// Gets or sets the custom rating.
 605        /// </summary>
 606        /// <value>The custom rating.</value>
 607        [JsonIgnore]
 608        public string CustomRating { get; set; }
 609
 610        /// <summary>
 611        /// Gets or sets the overview.
 612        /// </summary>
 613        /// <value>The overview.</value>
 614        [JsonIgnore]
 615        public string Overview { get; set; }
 616
 617        /// <summary>
 618        /// Gets or sets the studios.
 619        /// </summary>
 620        /// <value>The studios.</value>
 621        [JsonIgnore]
 622        public string[] Studios { get; set; }
 623
 624        /// <summary>
 625        /// Gets or sets the genres.
 626        /// </summary>
 627        /// <value>The genres.</value>
 628        [JsonIgnore]
 629        public string[] Genres { get; set; }
 630
 631        /// <summary>
 632        /// Gets or sets the tags.
 633        /// </summary>
 634        /// <value>The tags.</value>
 635        [JsonIgnore]
 636        public string[] Tags { get; set; }
 637
 638        [JsonIgnore]
 639        public string[] ProductionLocations { get; set; }
 640
 641        /// <summary>
 642        /// Gets or sets the home page URL.
 643        /// </summary>
 644        /// <value>The home page URL.</value>
 645        [JsonIgnore]
 646        public string HomePageUrl { get; set; }
 647
 648        /// <summary>
 649        /// Gets or sets the community rating.
 650        /// </summary>
 651        /// <value>The community rating.</value>
 652        [JsonIgnore]
 653        public float? CommunityRating { get; set; }
 654
 655        /// <summary>
 656        /// Gets or sets the run time ticks.
 657        /// </summary>
 658        /// <value>The run time ticks.</value>
 659        [JsonIgnore]
 660        public long? RunTimeTicks { get; set; }
 661
 662        /// <summary>
 663        /// Gets or sets the production year.
 664        /// </summary>
 665        /// <value>The production year.</value>
 666        [JsonIgnore]
 667        public int? ProductionYear { get; set; }
 668
 669        /// <summary>
 670        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 671        /// This could be episode number, album track number, etc.
 672        /// </summary>
 673        /// <value>The index number.</value>
 674        [JsonIgnore]
 675        public int? IndexNumber { get; set; }
 676
 677        /// <summary>
 678        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 679        /// </summary>
 680        /// <value>The parent index number.</value>
 681        [JsonIgnore]
 682        public int? ParentIndexNumber { get; set; }
 683
 684        [JsonIgnore]
 0685        public virtual bool HasLocalAlternateVersions => false;
 686
 687        [JsonIgnore]
 688        public string OfficialRatingForComparison
 689        {
 690            get
 691            {
 162692                var officialRating = OfficialRating;
 162693                if (!string.IsNullOrEmpty(officialRating))
 694                {
 0695                    return officialRating;
 696                }
 697
 162698                var parent = DisplayParent;
 162699                if (parent is not null)
 700                {
 38701                    return parent.OfficialRatingForComparison;
 702                }
 703
 124704                return null;
 705            }
 706        }
 707
 708        [JsonIgnore]
 709        public string CustomRatingForComparison
 710        {
 711            get
 712            {
 124713                return GetCustomRatingForComparision();
 714            }
 715        }
 716
 717        /// <summary>
 718        /// Gets or sets the provider ids.
 719        /// </summary>
 720        /// <value>The provider ids.</value>
 721        [JsonIgnore]
 722        public Dictionary<string, string> ProviderIds { get; set; }
 723
 724        [JsonIgnore]
 0725        public virtual Folder LatestItemsIndexContainer => null;
 726
 727        [JsonIgnore]
 728        public string PresentationUniqueKey { get; set; }
 729
 730        [JsonIgnore]
 22731        public virtual bool EnableRememberingTrackSelections => true;
 732
 733        [JsonIgnore]
 734        public virtual bool IsTopParent
 735        {
 736            get
 737            {
 347738                if (this is BasePluginFolder || this is Channel)
 739                {
 59740                    return true;
 741                }
 742
 288743                if (this is IHasCollectionType view)
 744                {
 13745                    if (view.CollectionType == CollectionType.livetv)
 746                    {
 0747                        return true;
 748                    }
 749                }
 750
 288751                if (GetParent() is AggregateFolder)
 752                {
 0753                    return true;
 754                }
 755
 288756                return false;
 757            }
 758        }
 759
 760        [JsonIgnore]
 444761        public virtual bool SupportsAncestors => true;
 762
 763        [JsonIgnore]
 92764        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 765
 766        [JsonIgnore]
 68767        public virtual bool SupportsPeople => false;
 768
 769        [JsonIgnore]
 0770        public virtual bool SupportsThemeMedia => false;
 771
 772        [JsonIgnore]
 0773        public virtual bool SupportsInheritedParentImages => false;
 774
 775        /// <summary>
 776        /// Gets a value indicating whether this instance is folder.
 777        /// </summary>
 778        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 779        [JsonIgnore]
 79780        public virtual bool IsFolder => false;
 781
 782        [JsonIgnore]
 0783        public virtual bool IsDisplayedAsFolder => false;
 784
 785        /// <summary>
 786        /// Gets or sets the remote trailers.
 787        /// </summary>
 788        /// <value>The remote trailers.</value>
 789        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 790
 791        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 792        {
 162793            callstack ??= new();
 162794            var customRating = CustomRating;
 162795            if (!string.IsNullOrEmpty(customRating))
 796            {
 0797                return customRating;
 798            }
 799
 162800            callstack.Add(Id);
 801
 162802            var parent = DisplayParent;
 162803            if (parent is not null && !callstack.Contains(parent.Id))
 804            {
 38805                return parent.GetCustomRatingForComparision(callstack);
 806            }
 807
 124808            return null;
 809        }
 810
 811        public virtual double GetDefaultPrimaryImageAspectRatio()
 812        {
 0813            return 0;
 814        }
 815
 816        public virtual string CreatePresentationUniqueKey()
 817        {
 57818            return Id.ToString("N", CultureInfo.InvariantCulture);
 819        }
 820
 821        public virtual bool CanDelete()
 822        {
 0823            if (SourceType == SourceType.Channel)
 824            {
 0825                return ChannelManager.CanDelete(this);
 826            }
 827
 0828            return IsFileProtocol;
 829        }
 830
 831        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 832        {
 0833            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 834            {
 0835                return true;
 836            }
 837
 0838            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 839
 0840            if (SourceType == SourceType.Channel)
 841            {
 0842                return allowed.Contains(ChannelId);
 843            }
 844
 0845            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 846
 0847            foreach (var folder in collectionFolders)
 848            {
 0849                if (allowed.Contains(folder.Id))
 850                {
 0851                    return true;
 852                }
 853            }
 854
 0855            return false;
 0856        }
 857
 858        public BaseItem GetOwner()
 859        {
 628860            var ownerId = OwnerId;
 628861            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 862        }
 863
 864        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 865        {
 6866            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 867        }
 868
 869        public virtual bool CanDelete(User user)
 870        {
 6871            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 872
 6873            return CanDelete(user, allCollectionFolders);
 874        }
 875
 876        public virtual bool CanDownload()
 877        {
 6878            return false;
 879        }
 880
 881        public virtual bool IsAuthorizedToDownload(User user)
 882        {
 0883            return user.HasPermission(PermissionKind.EnableContentDownloading);
 884        }
 885
 886        public bool CanDownload(User user)
 887        {
 6888            return CanDownload() && IsAuthorizedToDownload(user);
 889        }
 890
 891        /// <inheritdoc />
 892        public override string ToString()
 893        {
 69894            return Name;
 895        }
 896
 897        public virtual string GetInternalMetadataPath()
 898        {
 71899            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 900
 71901            return GetInternalMetadataPath(basePath);
 902        }
 903
 904        protected virtual string GetInternalMetadataPath(string basePath)
 905        {
 71906            if (SourceType == SourceType.Channel)
 907            {
 0908                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 909            }
 910
 71911            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 912
 71913            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 914        }
 915
 916        /// <summary>
 917        /// Creates the name of the sort.
 918        /// </summary>
 919        /// <returns>System.String.</returns>
 920        protected virtual string CreateSortName()
 921        {
 100922            if (Name is null)
 923            {
 0924                return null; // some items may not have name filled in properly
 925            }
 926
 100927            if (!EnableAlphaNumericSorting)
 928            {
 0929                return Name.TrimStart();
 930            }
 931
 100932            var sortable = Name.Trim().ToLowerInvariant();
 933
 800934            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 935            {
 936                // Remove from beginning if a space follows
 300937                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 938                {
 0939                    sortable = sortable.Remove(0, search.Length + 1);
 940                }
 941
 942                // Remove from middle if surrounded by spaces
 300943                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 944
 945                // Remove from end if preceeded by a space
 300946                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 947                {
 0948                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 949                }
 950            }
 951
 1400952            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 953            {
 600954                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 955            }
 956
 800957            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 958            {
 300959                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 960            }
 961
 100962            return ModifySortChunks(sortable);
 963        }
 964
 965        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 966        {
 967            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 968            {
 969                if (isDigitChunk && chunk.Length < 10)
 970                {
 971                    builder.Append('0', 10 - chunk.Length);
 972                }
 973
 974                builder.Append(chunk);
 975            }
 976
 106977            if (name.IsEmpty)
 978            {
 1979                return string.Empty;
 980            }
 981
 105982            var builder = new StringBuilder(name.Length);
 983
 105984            int chunkStart = 0;
 105985            bool isDigitChunk = char.IsDigit(name[0]);
 1674986            for (int i = 0; i < name.Length; i++)
 987            {
 732988                var isDigit = char.IsDigit(name[i]);
 732989                if (isDigit != isDigitChunk)
 990                {
 5991                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5992                    chunkStart = i;
 5993                    isDigitChunk = isDigit;
 994                }
 995            }
 996
 105997            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 998
 999            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 1051000            var result = builder.ToString().RemoveDiacritics();
 1051001            if (!result.All(char.IsAscii))
 1002            {
 01003                result = result.Transliterated();
 1004            }
 1005
 1051006            return result;
 1007        }
 1008
 1009        public BaseItem GetParent()
 1010        {
 18991011            var parentId = ParentId;
 18991012            if (parentId.IsEmpty())
 1013            {
 16661014                return null;
 1015            }
 1016
 2331017            return LibraryManager.GetItemById(parentId);
 1018        }
 1019
 1020        public IEnumerable<BaseItem> GetParents()
 1021        {
 6811022            var parent = GetParent();
 1023
 7521024            while (parent is not null)
 1025            {
 711026                yield return parent;
 1027
 711028                parent = parent.GetParent();
 1029            }
 6811030        }
 1031
 1032        /// <summary>
 1033        /// Finds a parent of a given type.
 1034        /// </summary>
 1035        /// <typeparam name="T">Type of parent.</typeparam>
 1036        /// <returns>``0.</returns>
 1037        public T FindParent<T>()
 1038            where T : Folder
 1039        {
 01040            foreach (var parent in GetParents())
 1041            {
 01042                if (parent is T item)
 1043                {
 01044                    return item;
 1045                }
 1046            }
 1047
 01048            return null;
 01049        }
 1050
 1051        /// <summary>
 1052        /// Gets the play access.
 1053        /// </summary>
 1054        /// <param name="user">The user.</param>
 1055        /// <returns>PlayAccess.</returns>
 1056        public PlayAccess GetPlayAccess(User user)
 1057        {
 61058            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1059            {
 01060                return PlayAccess.None;
 1061            }
 1062
 1063            // if (!user.IsParentalScheduleAllowed())
 1064            // {
 1065            //    return PlayAccess.None;
 1066            // }
 1067
 61068            return PlayAccess.Full;
 1069        }
 1070
 1071        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1072        {
 01073            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01074            {
 01075                ItemId = Id
 01076            });
 1077        }
 1078
 1079        protected virtual bool IsActiveRecording()
 1080        {
 01081            return false;
 1082        }
 1083
 1084        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1085        {
 01086            if (SourceType == SourceType.Channel)
 1087            {
 01088                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01089                           .ToList();
 1090
 01091                if (sources.Count > 0)
 1092                {
 01093                    return sources;
 1094                }
 1095            }
 1096
 01097            var list = GetAllItemsForMediaSources();
 01098            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
 1099
 01100            if (IsActiveRecording())
 1101            {
 01102                foreach (var mediaSource in result)
 1103                {
 01104                    mediaSource.Type = MediaSourceType.Placeholder;
 1105                }
 1106            }
 1107
 01108            return result.OrderBy(i =>
 01109            {
 01110                if (i.VideoType == VideoType.VideoFile)
 01111                {
 01112                    return 0;
 01113                }
 01114
 01115                return 1;
 01116            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 01117            .ThenByDescending(i => i, new MediaSourceWidthComparator())
 01118            .ToArray();
 1119        }
 1120
 1121        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1122        {
 01123            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1124        }
 1125
 1126        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
 1127        {
 01128            ArgumentNullException.ThrowIfNull(item);
 1129
 01130            var protocol = item.PathProtocol;
 1131
 1132            // Resolve the item path so everywhere we use the media source it will always point to
 1133            // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
 1134            // path will return null, so it's safe to check for all paths.
 01135            var itemPath = item.Path;
 01136            if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) 
 1137            {
 01138                itemPath = linkInfo.FullName;
 1139            }
 1140
 01141            var info = new MediaSourceInfo
 01142            {
 01143                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 01144                Protocol = protocol ?? MediaProtocol.File,
 01145                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 01146                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 01147                Name = GetMediaSourceName(item),
 01148                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 01149                RunTimeTicks = item.RunTimeTicks,
 01150                Container = item.Container,
 01151                Size = item.Size,
 01152                Type = type,
 01153                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 01154                    && (protocol is null or MediaProtocol.File)
 01155                    && MediaSegmentManager.HasSegments(item.Id)
 01156            };
 1157
 01158            if (string.IsNullOrEmpty(info.Path))
 1159            {
 01160                info.Type = MediaSourceType.Placeholder;
 1161            }
 1162
 01163            if (info.Protocol == MediaProtocol.File)
 1164            {
 01165                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1166            }
 1167
 01168            var video = item as Video;
 01169            if (video is not null)
 1170            {
 01171                info.IsoType = video.IsoType;
 01172                info.VideoType = video.VideoType;
 01173                info.Video3DFormat = video.Video3DFormat;
 01174                info.Timestamp = video.Timestamp;
 1175
 01176                if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
 1177                {
 01178                    var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
 1179
 1180                    // Only allow remote shortcut paths — local file paths in .strm files
 1181                    // could be used to read arbitrary files from the server.
 01182                    if (shortcutProtocol != MediaProtocol.File)
 1183                    {
 01184                        info.IsRemote = true;
 01185                        info.Path = video.ShortcutPath;
 01186                        info.Protocol = shortcutProtocol;
 1187                    }
 1188                }
 1189
 01190                if (string.IsNullOrEmpty(info.Container))
 1191                {
 01192                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1193                    {
 01194                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1195                        {
 01196                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1197                        }
 1198                    }
 1199                }
 1200            }
 1201
 01202            if (string.IsNullOrEmpty(info.Container))
 1203            {
 01204                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1205                {
 01206                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1207                }
 1208            }
 1209
 01210            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1211            {
 01212                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1213            }
 1214
 01215            if (video is not null && video.VideoType != VideoType.VideoFile)
 1216            {
 01217                info.SupportsDirectStream = false;
 1218            }
 1219
 01220            info.Bitrate = item.TotalBitrate;
 01221            info.InferTotalBitrate();
 1222
 01223            return info;
 1224        }
 1225
 1226        internal string GetMediaSourceName(BaseItem item)
 1227        {
 41228            var terms = new List<string>();
 1229
 41230            var path = item.Path;
 41231            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1232            {
 41233                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 41234                if (HasLocalAlternateVersions)
 1235                {
 41236                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 41237                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1238                    {
 21239                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
 21240                        if (!name.IsWhiteSpace())
 1241                        {
 21242                            terms.Add(name.ToString());
 1243                        }
 1244                    }
 1245                }
 1246
 41247                if (terms.Count == 0)
 1248                {
 21249                    terms.Add(displayName);
 1250                }
 1251            }
 1252
 41253            if (terms.Count == 0)
 1254            {
 01255                terms.Add(item.Name);
 1256            }
 1257
 41258            if (item is Video video)
 1259            {
 41260                if (video.Video3DFormat.HasValue)
 1261                {
 01262                    terms.Add("3D");
 1263                }
 1264
 41265                if (video.VideoType == VideoType.BluRay)
 1266                {
 01267                    terms.Add("Bluray");
 1268                }
 41269                else if (video.VideoType == VideoType.Dvd)
 1270                {
 01271                    terms.Add("DVD");
 1272                }
 41273                else if (video.VideoType == VideoType.Iso)
 1274                {
 01275                    if (video.IsoType.HasValue)
 1276                    {
 01277                        if (video.IsoType.Value == IsoType.BluRay)
 1278                        {
 01279                            terms.Add("Bluray");
 1280                        }
 01281                        else if (video.IsoType.Value == IsoType.Dvd)
 1282                        {
 01283                            terms.Add("DVD");
 1284                        }
 1285                    }
 1286                    else
 1287                    {
 01288                        terms.Add("ISO");
 1289                    }
 1290                }
 1291            }
 1292
 41293            return string.Join('/', terms);
 1294        }
 1295
 1296        public Task RefreshMetadata(CancellationToken cancellationToken)
 1297        {
 501298            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1299        }
 1300
 1301        /// <summary>
 1302        /// The base implementation to refresh metadata.
 1303        /// </summary>
 1304        /// <param name="options">The options.</param>
 1305        /// <param name="cancellationToken">The cancellation token.</param>
 1306        /// <returns>true if a provider reports we changed.</returns>
 1307        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1308        {
 571309            var requiresSave = false;
 1310
 571311            if (SupportsOwnedItems)
 1312            {
 1313                try
 1314                {
 351315                    if (IsFileProtocol)
 1316                    {
 351317                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1318                    }
 1319
 351320                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 351321                }
 01322                catch (Exception ex)
 1323                {
 01324                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01325                }
 1326            }
 1327
 571328            var refreshOptions = requiresSave
 571329                ? new MetadataRefreshOptions(options)
 571330                {
 571331                    ForceSave = true
 571332                }
 571333                : options;
 1334
 571335            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 561336        }
 1337
 1338        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1339        {
 01340            if (!IsVisible(user))
 1341            {
 01342                return false;
 1343            }
 1344
 01345            var parents = GetParents().ToList();
 01346            if (parents.Any(i => !i.IsVisible(user, true)))
 1347            {
 01348                return false;
 1349            }
 1350
 01351            if (checkFolders)
 1352            {
 01353                var topParent = parents.Count > 0 ? parents[^1] : this;
 1354
 01355                if (string.IsNullOrEmpty(topParent.Path))
 1356                {
 01357                    return true;
 1358                }
 1359
 01360                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1361
 01362                if (itemCollectionFolders.Count > 0)
 1363                {
 01364                    var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 1365                    IEnumerable<Guid> userCollectionFolderIds;
 01366                    if (blockedMediaFolders.Length > 0)
 1367                    {
 1368                        // User has blocked folders - get all library folders and exclude blocked ones
 01369                        userCollectionFolderIds = LibraryManager.GetUserRootFolder().Children
 01370                            .Select(i => i.Id)
 01371                            .Where(id => !blockedMediaFolders.Contains(id));
 1372                    }
 01373                    else if (user.HasPermission(PermissionKind.EnableAllFolders))
 1374                    {
 1375                        // User can access all folders - no need to filter
 01376                        return true;
 1377                    }
 1378                    else
 1379                    {
 1380                        // User has specific enabled folders
 01381                        userCollectionFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders);
 1382                    }
 1383
 01384                    if (!itemCollectionFolders.Any(userCollectionFolderIds.Contains))
 1385                    {
 01386                        return false;
 1387                    }
 1388                }
 1389            }
 1390
 01391            return true;
 1392        }
 1393
 1394        public void SetParent(Folder parent)
 1395        {
 91396            ParentId = parent is null ? Guid.Empty : parent.Id;
 91397        }
 1398
 1399        /// <summary>
 1400        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1401        /// Returns true or false indicating if changes were found.
 1402        /// </summary>
 1403        /// <param name="options">The metadata refresh options.</param>
 1404        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1405        /// <param name="cancellationToken">The cancellation token.</param>
 1406        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1407        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1408        {
 351409            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1410            {
 351411                return false;
 1412            }
 1413
 01414            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 351415        }
 1416
 1417        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1418        {
 411419            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1420        }
 1421
 1422        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1423        {
 01424            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 01425            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1426
 01427            var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
 01428            {
 01429                OwnerIds = [item.Id]
 01430            }).Select(e => e.Id).ToArray();
 1431
 01432            var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
 1433
 01434            if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.Full
 1435            {
 01436                return false;
 1437            }
 1438
 01439            var ownerId = item.Id;
 1440
 01441            var tasks = extras.Select(i =>
 01442            {
 01443                var subOptions = new MetadataRefreshOptions(options);
 01444                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
 01445                {
 01446                    subOptions.ForceSave = true;
 01447                }
 01448
 01449                i.OwnerId = ownerId;
 01450                i.ParentId = Guid.Empty;
 01451                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 01452            });
 1453
 01454            var removedExtraIds = currentExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 01455            if (removedExtraIds.Length > 0)
 1456            {
 01457                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 01458                {
 01459                    ItemIds = removedExtraIds
 01460                });
 01461                foreach (var removedExtra in removedExtras)
 1462                {
 1463                    // Only delete items that are actual extras (have ExtraType set)
 1464                    // Items with OwnerId but no ExtraType might be alternate versions, not extras
 01465                    if (removedExtra.ExtraType.HasValue)
 1466                    {
 01467                        LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 01468                        {
 01469                            DeleteFileLocation = false
 01470                        });
 1471                    }
 1472                }
 1473            }
 1474
 01475            await Task.WhenAll(tasks).ConfigureAwait(false);
 1476
 01477            return true;
 01478        }
 1479
 1480        public string GetPresentationUniqueKey()
 1481        {
 01482            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1483        }
 1484
 1485        public virtual bool RequiresRefresh()
 1486        {
 571487            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1488            {
 571489                return false;
 1490            }
 1491
 01492            var info = FileSystem.GetFileSystemInfo(Path);
 1493
 01494            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1495        }
 1496
 1497        public virtual List<string> GetUserDataKeys()
 1498        {
 1501499            var list = new List<string>();
 1500
 1501501            if (SourceType == SourceType.Channel)
 1502            {
 01503                if (!string.IsNullOrEmpty(ExternalId))
 1504                {
 01505                    list.Add(ExternalId);
 1506                }
 1507            }
 1508
 1501509            list.Add(Id.ToString());
 1501510            return list;
 1511        }
 1512
 1513        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1514        {
 51515            var updateType = ItemUpdateType.None;
 1516
 51517            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1518            {
 01519                IsInMixedFolder = newItem.IsInMixedFolder;
 01520                updateType |= ItemUpdateType.MetadataImport;
 1521            }
 1522
 51523            return updateType;
 1524        }
 1525
 1526        public void AfterMetadataRefresh()
 1527        {
 561528            _sortName = null;
 561529        }
 1530
 1531        /// <summary>
 1532        /// Gets the preferred metadata language.
 1533        /// </summary>
 1534        /// <returns>System.String.</returns>
 1535        public string GetPreferredMetadataLanguage()
 1536        {
 341537            string lang = PreferredMetadataLanguage;
 1538
 341539            if (string.IsNullOrEmpty(lang))
 1540            {
 341541                lang = GetParents()
 341542                    .Select(i => i.PreferredMetadataLanguage)
 341543                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1544            }
 1545
 341546            if (string.IsNullOrEmpty(lang))
 1547            {
 341548                lang = LibraryManager.GetCollectionFolders(this)
 341549                    .Select(i => i.PreferredMetadataLanguage)
 341550                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1551            }
 1552
 341553            if (string.IsNullOrEmpty(lang))
 1554            {
 341555                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1556            }
 1557
 341558            if (string.IsNullOrEmpty(lang))
 1559            {
 341560                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1561            }
 1562
 341563            return lang;
 1564        }
 1565
 1566        /// <summary>
 1567        /// Gets the preferred metadata language.
 1568        /// </summary>
 1569        /// <returns>System.String.</returns>
 1570        public string GetPreferredMetadataCountryCode()
 1571        {
 341572            string lang = PreferredMetadataCountryCode;
 1573
 341574            if (string.IsNullOrEmpty(lang))
 1575            {
 341576                lang = GetParents()
 341577                    .Select(i => i.PreferredMetadataCountryCode)
 341578                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1579            }
 1580
 341581            if (string.IsNullOrEmpty(lang))
 1582            {
 341583                lang = LibraryManager.GetCollectionFolders(this)
 341584                    .Select(i => i.PreferredMetadataCountryCode)
 341585                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1586            }
 1587
 341588            if (string.IsNullOrEmpty(lang))
 1589            {
 341590                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1591            }
 1592
 341593            if (string.IsNullOrEmpty(lang))
 1594            {
 341595                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1596            }
 1597
 341598            return lang;
 1599        }
 1600
 1601        public virtual bool IsSaveLocalMetadataEnabled()
 1602        {
 861603            if (SourceType == SourceType.Channel)
 1604            {
 01605                return false;
 1606            }
 1607
 861608            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1609
 861610            return libraryOptions.SaveLocalMetadata;
 1611        }
 1612
 1613        /// <summary>
 1614        /// Determines if a given user has access to this item.
 1615        /// </summary>
 1616        /// <param name="user">The user.</param>
 1617        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1618        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1619        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1620        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1621        {
 101622            ArgumentNullException.ThrowIfNull(user);
 1623
 101624            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1625            {
 01626                return false;
 1627            }
 1628
 101629            var maxAllowedRating = user.MaxParentalRatingScore;
 101630            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 101631            var rating = CustomRatingForComparison;
 1632
 101633            if (string.IsNullOrEmpty(rating))
 1634            {
 101635                rating = OfficialRatingForComparison;
 1636            }
 1637
 101638            if (string.IsNullOrEmpty(rating))
 1639            {
 101640                return !GetBlockUnratedValue(user);
 1641            }
 1642
 01643            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1644
 1645            // Could not determine rating level
 01646            if (ratingScore is null)
 1647            {
 01648                var isAllowed = !GetBlockUnratedValue(user);
 1649
 01650                if (!isAllowed)
 1651                {
 01652                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1653                }
 1654
 01655                return isAllowed;
 1656            }
 1657
 01658            if (!maxAllowedRating.HasValue)
 1659            {
 01660                return true;
 1661            }
 1662
 01663            if (ratingScore.Score != maxAllowedRating.Value)
 1664            {
 01665                return ratingScore.Score < maxAllowedRating.Value;
 1666            }
 1667
 01668            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1669        }
 1670
 1671        public ParentalRatingScore GetParentalRatingScore()
 1672        {
 1141673            var rating = CustomRatingForComparison;
 1674
 1141675            if (string.IsNullOrEmpty(rating))
 1676            {
 1141677                rating = OfficialRatingForComparison;
 1678            }
 1679
 1141680            if (string.IsNullOrEmpty(rating))
 1681            {
 1141682                return null;
 1683            }
 1684
 01685            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1686        }
 1687
 1688        public List<string> GetInheritedTags()
 1689        {
 1111690            var list = new List<string>();
 1111691            list.AddRange(Tags);
 1692
 2761693            foreach (var parent in GetParents())
 1694            {
 271695                list.AddRange(parent.Tags);
 1696            }
 1697
 2221698            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1699            {
 01700                list.AddRange(folder.Tags);
 1701            }
 1702
 1111703            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1704        }
 1705
 1706        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1707        {
 101708            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 101709            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 1710
 101711            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 1712            {
 101713                return true;
 1714            }
 1715
 1716            // Normalize tags using the same logic as database queries
 01717            var normalizedBlockedTags = blockedTags
 01718                .Where(t => !string.IsNullOrWhiteSpace(t))
 01719                .Select(t => t.GetCleanValue())
 01720                .ToHashSet(StringComparer.Ordinal);
 1721
 01722            var normalizedItemTags = GetInheritedTags()
 01723                .Select(t => t.GetCleanValue())
 01724                .ToHashSet(StringComparer.Ordinal);
 1725
 1726            // Check blocked tags - item is hidden if it has any blocked tag
 01727            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 1728            {
 01729                return false;
 1730            }
 1731
 01732            var parent = GetParents().FirstOrDefault() ?? this;
 01733            if (parent is UserRootFolder or AggregateFolder or UserView)
 1734            {
 01735                return true;
 1736            }
 1737
 1738            // Check allowed tags - item must have at least one allowed tag
 01739            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 1740            {
 01741                var normalizedAllowedTags = allowedTags
 01742                    .Where(t => !string.IsNullOrWhiteSpace(t))
 01743                    .Select(t => t.GetCleanValue())
 01744                    .ToHashSet(StringComparer.Ordinal);
 1745
 01746                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 1747                {
 01748                    return false;
 1749                }
 1750            }
 1751
 01752            return true;
 1753        }
 1754
 1755        public virtual UnratedItem GetBlockUnratedType()
 1756        {
 1111757            if (SourceType == SourceType.Channel)
 1758            {
 01759                return UnratedItem.ChannelContent;
 1760            }
 1761
 1111762            return UnratedItem.Other;
 1763        }
 1764
 1765        /// <summary>
 1766        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1767        /// </summary>
 1768        /// <param name="user">The configuration.</param>
 1769        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1770        protected virtual bool GetBlockUnratedValue(User user)
 1771        {
 1772            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1773            // Special folders like series and albums will override this method.
 101774            if (IsFolder || this is IItemByName)
 1775            {
 101776                return false;
 1777            }
 1778
 01779            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1780        }
 1781
 1782        /// <summary>
 1783        /// Determines if this folder should be visible to a given user.
 1784        /// Default is just parental allowed. Can be overridden for more functionality.
 1785        /// </summary>
 1786        /// <param name="user">The user.</param>
 1787        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1788        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1789        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1790        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1791        {
 101792            ArgumentNullException.ThrowIfNull(user);
 1793
 101794            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1795        }
 1796
 1797        public virtual bool IsVisibleStandalone(User user)
 1798        {
 01799            if (SourceType == SourceType.Channel)
 1800            {
 01801                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1802            }
 1803
 01804            return IsVisibleStandaloneInternal(user, true);
 1805        }
 1806
 1807        public virtual string GetClientTypeName()
 1808        {
 1101809            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1810            {
 01811                return "ChannelFolderItem";
 1812            }
 1813
 1101814            return GetType().Name;
 1815        }
 1816
 1817        public BaseItemKind GetBaseItemKind()
 1818        {
 2391819            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1820        }
 1821
 1822        /// <summary>
 1823        /// Gets the linked child.
 1824        /// </summary>
 1825        /// <param name="info">The info.</param>
 1826        /// <returns>BaseItem.</returns>
 1827        protected BaseItem GetLinkedChild(LinkedChild info)
 1828        {
 1829            // First get using the cached Id
 01830            if (info.ItemId.HasValue)
 1831            {
 01832                if (info.ItemId.Value.IsEmpty())
 1833                {
 01834                    return null;
 1835                }
 1836
 01837                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1838
 01839                if (itemById is not null)
 1840                {
 01841                    return itemById;
 1842                }
 1843            }
 1844
 01845            var item = FindLinkedChild(info);
 1846
 1847            // If still null, log
 01848            if (item is null)
 1849            {
 1850                // Don't keep searching over and over
 01851                info.ItemId = Guid.Empty;
 1852            }
 1853            else
 1854            {
 1855                // Cache the id for next time
 01856                info.ItemId = item.Id;
 1857            }
 1858
 01859            return item;
 1860        }
 1861
 1862#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 1863        private BaseItem FindLinkedChild(LinkedChild info)
 1864        {
 1865            // First try to find by ItemId (new preferred method)
 01866            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 1867            {
 01868                var item = LibraryManager.GetItemById(info.ItemId.Value);
 01869                if (item is not null)
 1870                {
 01871                    return item;
 1872                }
 1873
 01874                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 1875            }
 1876
 1877            // Fall back to Path (legacy method)
 01878            var path = info.Path;
 01879            if (!string.IsNullOrEmpty(path))
 1880            {
 01881                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 1882
 01883                var itemByPath = LibraryManager.FindByPath(path, null);
 1884
 01885                if (itemByPath is null)
 1886                {
 01887                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1888                }
 1889
 01890                return itemByPath;
 1891            }
 1892
 1893            // Fall back to LibraryItemId (legacy method)
 01894            if (!string.IsNullOrEmpty(info.LibraryItemId))
 1895            {
 01896                var item = LibraryManager.GetItemById(info.LibraryItemId);
 1897
 01898                if (item is null)
 1899                {
 01900                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 1901                }
 1902
 01903                return item;
 1904            }
 1905
 01906            return null;
 1907        }
 1908#pragma warning restore CS0618
 1909
 1910        /// <summary>
 1911        /// Adds a studio to the item.
 1912        /// </summary>
 1913        /// <param name="name">The name.</param>
 1914        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1915        public void AddStudio(string name)
 1916        {
 41917            ArgumentException.ThrowIfNullOrEmpty(name);
 41918            var current = Studios;
 1919
 41920            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 1921            {
 41922                int curLen = current.Length;
 41923                if (curLen == 0)
 1924                {
 41925                    Studios = [name];
 1926                }
 1927                else
 1928                {
 01929                    Studios = [.. current, name];
 1930                }
 1931            }
 01932        }
 1933
 1934        public void SetStudios(IEnumerable<string> names)
 1935        {
 01936            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 01937        }
 1938
 1939        /// <summary>
 1940        /// Adds a genre to the item.
 1941        /// </summary>
 1942        /// <param name="name">The name.</param>
 1943        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1944        public void AddGenre(string name)
 1945        {
 131946            ArgumentException.ThrowIfNullOrEmpty(name);
 1947
 131948            var genres = Genres;
 131949            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 1950            {
 131951                Genres = [.. genres, name];
 1952            }
 131953        }
 1954
 1955        /// <summary>
 1956        /// Marks the played.
 1957        /// </summary>
 1958        /// <param name="user">The user.</param>
 1959        /// <param name="datePlayed">The date played.</param>
 1960        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1961        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1962        public virtual void MarkPlayed(
 1963            User user,
 1964            DateTime? datePlayed,
 1965            bool resetPosition)
 1966        {
 01967            ArgumentNullException.ThrowIfNull(user);
 1968
 01969            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 01970            {
 01971                Key = GetUserDataKeys().First(),
 01972            };
 1973
 01974            if (datePlayed.HasValue)
 1975            {
 1976                // Increment
 01977                data.PlayCount++;
 1978            }
 1979
 1980            // Ensure it's at least one
 01981            data.PlayCount = Math.Max(data.PlayCount, 1);
 1982
 01983            if (resetPosition)
 1984            {
 01985                data.PlaybackPositionTicks = 0;
 1986            }
 1987
 01988            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 01989            data.Played = true;
 1990
 01991            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01992        }
 1993
 1994        /// <summary>
 1995        /// Marks the unplayed.
 1996        /// </summary>
 1997        /// <param name="user">The user.</param>
 1998        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1999        public virtual void MarkUnplayed(User user)
 2000        {
 02001            ArgumentNullException.ThrowIfNull(user);
 2002
 02003            var data = UserDataManager.GetUserData(user, this);
 2004
 2005            // I think it is okay to do this here.
 2006            // if this is only called when a user is manually forcing something to un-played
 2007            // then it probably is what we want to do...
 02008            data.PlayCount = 0;
 02009            data.PlaybackPositionTicks = 0;
 02010            data.LastPlayedDate = null;
 02011            data.Played = false;
 2012
 02013            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02014        }
 2015
 2016        /// <summary>
 2017        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2018        /// </summary>
 2019        public virtual void ChangedExternally()
 2020        {
 02021            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02022        }
 2023
 2024        /// <summary>
 2025        /// Gets an image.
 2026        /// </summary>
 2027        /// <param name="type">The type.</param>
 2028        /// <param name="imageIndex">Index of the image.</param>
 2029        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2030        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2031        public bool HasImage(ImageType type, int imageIndex)
 2032        {
 1432033            return GetImageInfo(type, imageIndex) is not null;
 2034        }
 2035
 2036        public void SetImage(ItemImageInfo image, int index)
 2037        {
 132038            if (image.Type == ImageType.Chapter)
 2039            {
 02040                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2041            }
 2042
 132043            var existingImage = GetImageInfo(image.Type, index);
 2044
 132045            if (existingImage is null)
 2046            {
 112047                AddImage(image);
 2048            }
 2049            else
 2050            {
 22051                existingImage.Path = image.Path;
 22052                existingImage.DateModified = image.DateModified;
 22053                existingImage.Width = image.Width;
 22054                existingImage.Height = image.Height;
 22055                existingImage.BlurHash = image.BlurHash;
 2056            }
 22057        }
 2058
 2059        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2060        {
 462061            if (type == ImageType.Chapter)
 2062            {
 02063                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2064            }
 2065
 462066            var image = GetImageInfo(type, index);
 2067
 462068            if (image is null)
 2069            {
 452070                AddImage(GetImageInfo(file, type));
 2071            }
 2072            else
 2073            {
 12074                var imageInfo = GetImageInfo(file, type);
 2075
 12076                image.Path = file.FullName;
 12077                image.DateModified = imageInfo.DateModified;
 2078
 2079                // reset these values
 12080                image.Width = 0;
 12081                image.Height = 0;
 2082            }
 12083        }
 2084
 2085        /// <summary>
 2086        /// Deletes the image.
 2087        /// </summary>
 2088        /// <param name="type">The type.</param>
 2089        /// <param name="index">The index.</param>
 2090        /// <returns>A task.</returns>
 2091        public async Task DeleteImageAsync(ImageType type, int index)
 2092        {
 02093            var info = GetImageInfo(type, index);
 2094
 02095            if (info is null)
 2096            {
 2097                // Nothing to do
 02098                return;
 2099            }
 2100
 2101            // Remove from file system
 02102            var path = info.Path;
 02103            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2104            {
 02105                FileSystem.DeleteFile(path);
 2106            }
 2107
 2108            // Remove from item
 02109            RemoveImage(info);
 2110
 02111            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02112        }
 2113
 2114        public void RemoveImage(ItemImageInfo image)
 2115        {
 02116            RemoveImages([image]);
 02117        }
 2118
 2119        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2120        {
 92121            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92122        }
 2123
 2124        public void AddImage(ItemImageInfo image)
 2125        {
 562126            ImageInfos = [.. ImageInfos, image];
 562127        }
 2128
 2129        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1092130         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2131
 2132        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 332133            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2134
 2135        /// <summary>
 2136        /// Validates that images within the item are still on the filesystem.
 2137        /// </summary>
 2138        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2139        public bool ValidateImages()
 2140        {
 712141            List<ItemImageInfo> deletedImages = null;
 1722142            foreach (var imageInfo in ImageInfos)
 2143            {
 152144                if (!imageInfo.IsLocalFile)
 2145                {
 2146                    continue;
 2147                }
 2148
 152149                if (File.Exists(imageInfo.Path))
 2150                {
 2151                    continue;
 2152                }
 2153
 32154                (deletedImages ??= []).Add(imageInfo);
 2155            }
 2156
 712157            var anyImagesRemoved = deletedImages?.Count > 0;
 712158            if (anyImagesRemoved)
 2159            {
 22160                RemoveImages(deletedImages);
 2161            }
 2162
 712163            return anyImagesRemoved;
 2164        }
 2165
 2166        /// <summary>
 2167        /// Gets the image path.
 2168        /// </summary>
 2169        /// <param name="imageType">Type of the image.</param>
 2170        /// <param name="imageIndex">Index of the image.</param>
 2171        /// <returns>System.String.</returns>
 2172        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2173        public string GetImagePath(ImageType imageType, int imageIndex)
 22174            => GetImageInfo(imageType, imageIndex)?.Path;
 2175
 2176        /// <summary>
 2177        /// Gets the image information.
 2178        /// </summary>
 2179        /// <param name="imageType">Type of the image.</param>
 2180        /// <param name="imageIndex">Index of the image.</param>
 2181        /// <returns>ItemImageInfo.</returns>
 2182        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2183        {
 2982184            if (imageType == ImageType.Chapter)
 2185            {
 02186                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2187
 02188                if (chapter is null)
 2189                {
 02190                    return null;
 2191                }
 2192
 02193                var path = chapter.ImagePath;
 2194
 02195                if (string.IsNullOrEmpty(path))
 2196                {
 02197                    return null;
 2198                }
 2199
 02200                return new ItemImageInfo
 02201                {
 02202                    Path = path,
 02203                    DateModified = chapter.ImageDateModified,
 02204                    Type = imageType
 02205                };
 2206            }
 2207
 2982208            return GetImages(imageType)
 2982209                .ElementAtOrDefault(imageIndex);
 2210        }
 2211
 2212        /// <summary>
 2213        /// Computes image index for given image or raises if no matching image found.
 2214        /// </summary>
 2215        /// <param name="image">Image to compute index for.</param>
 2216        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2217        /// </exception>
 2218        /// <returns>Image index.</returns>
 2219        public int GetImageIndex(ItemImageInfo image)
 2220        {
 02221            ArgumentNullException.ThrowIfNull(image);
 2222
 02223            if (image.Type == ImageType.Chapter)
 2224            {
 02225                var chapters = ChapterManager.GetChapters(Id);
 02226                for (var i = 0; i < chapters.Count; i++)
 2227                {
 02228                    if (chapters[i].ImagePath == image.Path)
 2229                    {
 02230                        return i;
 2231                    }
 2232                }
 2233
 02234                throw new ArgumentException("No chapter index found for image path", image.Path);
 2235            }
 2236
 02237            var images = GetImages(image.Type).ToArray();
 02238            for (var i = 0; i < images.Length; i++)
 2239            {
 02240                if (images[i].Path == image.Path)
 2241                {
 02242                    return i;
 2243                }
 2244            }
 2245
 02246            throw new ArgumentException("No image index found for image path", image.Path);
 2247        }
 2248
 2249        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2250        {
 4012251            if (imageType == ImageType.Chapter)
 2252            {
 02253                throw new ArgumentException("No image info for chapter images");
 2254            }
 2255
 2256            // Yield return is more performant than LINQ Where on an Array
 13482257            for (var i = 0; i < ImageInfos.Length; i++)
 2258            {
 2912259                var imageInfo = ImageInfos[i];
 2912260                if (imageInfo.Type == imageType)
 2261                {
 1502262                    yield return imageInfo;
 2263                }
 2264            }
 3832265        }
 2266
 2267        /// <summary>
 2268        /// Adds the images, updating metadata if they already are part of this item.
 2269        /// </summary>
 2270        /// <param name="imageType">Type of the image.</param>
 2271        /// <param name="images">The images.</param>
 2272        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2273        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2274        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2275        {
 42276            if (imageType == ImageType.Chapter)
 2277            {
 02278                throw new ArgumentException("Cannot call AddImages with chapter images");
 2279            }
 2280
 42281            var existingImages = GetImages(imageType)
 42282                .ToList();
 2283
 42284            var newImageList = new List<FileSystemMetadata>();
 42285            var imageUpdated = false;
 2286
 242287            foreach (var newImage in images)
 2288            {
 82289                if (newImage is null)
 2290                {
 02291                    throw new ArgumentException("null image found in list");
 2292                }
 2293
 82294                var existing = existingImages
 82295                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2296
 82297                if (existing is null)
 2298                {
 42299                    newImageList.Add(newImage);
 2300                }
 2301                else
 2302                {
 42303                    if (existing.IsLocalFile)
 2304                    {
 42305                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2306
 2307                        // If date changed then we need to reset saved image dimensions
 42308                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2309                        {
 22310                            existing.Width = 0;
 22311                            existing.Height = 0;
 22312                            imageUpdated = true;
 2313                        }
 2314
 42315                        existing.DateModified = newDateModified;
 2316                    }
 2317                }
 2318            }
 2319
 42320            if (newImageList.Count > 0)
 2321            {
 22322                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2323            }
 2324
 42325            return imageUpdated || newImageList.Count > 0;
 2326        }
 2327
 2328        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2329        {
 502330            return new ItemImageInfo
 502331            {
 502332                Path = file.FullName,
 502333                Type = type,
 502334                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502335            };
 2336        }
 2337
 2338        /// <summary>
 2339        /// Gets the file system path to delete when the item is to be deleted.
 2340        /// </summary>
 2341        /// <returns>The metadata for the deleted paths.</returns>
 2342        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2343        {
 02344            return new[]
 02345            {
 02346                FileSystem.GetFileSystemInfo(Path)
 02347            }.Concat(GetLocalMetadataFilesToDelete());
 2348        }
 2349
 2350        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2351        {
 02352            if (IsFolder || !IsInMixedFolder)
 2353            {
 02354                return [];
 2355            }
 2356
 02357            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2358
 02359            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02360                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02361                .ToList();
 2362        }
 2363
 2364        public bool AllowsMultipleImages(ImageType type)
 2365        {
 182366            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2367        }
 2368
 2369        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2370        {
 02371            if (!AllowsMultipleImages(type))
 2372            {
 02373                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2374            }
 2375
 02376            var info1 = GetImageInfo(type, index1);
 02377            var info2 = GetImageInfo(type, index2);
 2378
 02379            if (info1 is null || info2 is null)
 2380            {
 2381                // Nothing to do
 02382                return Task.CompletedTask;
 2383            }
 2384
 02385            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2386            {
 2387                // TODO: Not supported  yet
 02388                return Task.CompletedTask;
 2389            }
 2390
 02391            var path1 = info1.Path;
 02392            var path2 = info2.Path;
 2393
 02394            FileSystem.SwapFiles(path1, path2);
 2395
 2396            // Refresh these values
 02397            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02398            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2399
 02400            info1.Width = 0;
 02401            info1.Height = 0;
 02402            info2.Width = 0;
 02403            info2.Height = 0;
 2404
 02405            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2406        }
 2407
 2408        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2409        {
 02410            userItemData ??= UserDataManager.GetUserData(user, this);
 2411
 02412            return userItemData is not null && userItemData.Played;
 2413        }
 2414
 2415        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2416        {
 02417            userItemData ??= UserDataManager.GetUserData(user, this);
 2418
 02419            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2420        }
 2421
 2422        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2423        {
 02424            ArgumentNullException.ThrowIfNull(user);
 2425
 02426            userItemData ??= UserDataManager.GetUserData(user, this);
 2427
 02428            return userItemData is null || !userItemData.Played;
 2429        }
 2430
 2431        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2432        {
 342433            return GetItemLookupInfo<ItemLookupInfo>();
 2434        }
 2435
 2436        protected T GetItemLookupInfo<T>()
 2437            where T : ItemLookupInfo, new()
 2438        {
 342439            return new T
 342440            {
 342441                Path = Path,
 342442                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 342443                MetadataLanguage = GetPreferredMetadataLanguage(),
 342444                Name = GetNameForMetadataLookup(),
 342445                OriginalTitle = OriginalTitle,
 342446                ProviderIds = ProviderIds,
 342447                IndexNumber = IndexNumber,
 342448                ParentIndexNumber = ParentIndexNumber,
 342449                Year = ProductionYear,
 342450                PremiereDate = PremiereDate
 342451            };
 2452        }
 2453
 2454        protected virtual string GetNameForMetadataLookup()
 2455        {
 342456            return Name;
 2457        }
 2458
 2459        /// <summary>
 2460        /// This is called before any metadata refresh and returns true if changes were made.
 2461        /// </summary>
 2462        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2463        /// <returns>true if the item has change, else false.</returns>
 2464        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2465        {
 342466            _sortName = null;
 2467
 342468            var hasChanges = false;
 2469
 342470            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2471            {
 02472                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02473                hasChanges = true;
 2474            }
 2475
 342476            return hasChanges;
 2477        }
 2478
 2479        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2480        {
 02481            if (protocol == MediaProtocol.File)
 2482            {
 02483                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2484            }
 2485
 02486            return path;
 2487        }
 2488
 2489        public virtual void FillUserDataDtoValues(
 2490            UserItemDataDto dto,
 2491            UserItemData userData,
 2492            BaseItemDto itemDto,
 2493            User user,
 2494            DtoOptions fields,
 2495            (int Played, int Total)? precomputedCounts = null)
 2496        {
 02497            if (RunTimeTicks.HasValue)
 2498            {
 02499                double pct = RunTimeTicks.Value;
 2500
 02501                if (pct > 0)
 2502                {
 02503                    pct = userData.PlaybackPositionTicks / pct;
 2504
 02505                    if (pct > 0)
 2506                    {
 02507                        dto.PlayedPercentage = 100 * pct;
 2508                    }
 2509                }
 2510            }
 02511        }
 2512
 2513        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2514        {
 02515            var newOptions = new MetadataRefreshOptions(options)
 02516            {
 02517                SearchResult = null
 02518            };
 2519
 02520            var item = this;
 2521
 02522            if (copyTitleMetadata)
 2523            {
 2524                // Take some data from the main item, for querying purposes
 02525                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2526                {
 02527                    newOptions.ForceSave = true;
 02528                    ownedItem.Genres = item.Genres;
 2529                }
 2530
 02531                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2532                {
 02533                    newOptions.ForceSave = true;
 02534                    ownedItem.Studios = item.Studios;
 2535                }
 2536
 02537                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2538                {
 02539                    newOptions.ForceSave = true;
 02540                    ownedItem.ProductionLocations = item.ProductionLocations;
 2541                }
 2542
 02543                if (item.CommunityRating != ownedItem.CommunityRating)
 2544                {
 02545                    ownedItem.CommunityRating = item.CommunityRating;
 02546                    newOptions.ForceSave = true;
 2547                }
 2548
 02549                if (item.CriticRating != ownedItem.CriticRating)
 2550                {
 02551                    ownedItem.CriticRating = item.CriticRating;
 02552                    newOptions.ForceSave = true;
 2553                }
 2554
 02555                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2556                {
 02557                    ownedItem.Overview = item.Overview;
 02558                    newOptions.ForceSave = true;
 2559                }
 2560
 02561                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2562                {
 02563                    ownedItem.OfficialRating = item.OfficialRating;
 02564                    newOptions.ForceSave = true;
 2565                }
 2566
 02567                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2568                {
 02569                    ownedItem.CustomRating = item.CustomRating;
 02570                    newOptions.ForceSave = true;
 2571                }
 2572            }
 2573
 02574            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02575        }
 2576
 2577        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2578        {
 02579            var newOptions = new MetadataRefreshOptions(options)
 02580            {
 02581                SearchResult = null
 02582            };
 2583
 02584            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2585
 2586            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02587            if (LibraryManager.GetItemById(id) is not Video video)
 2588            {
 02589                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2590
 02591                newOptions.ForceSave = true;
 2592            }
 2593
 02594            if (video is null)
 2595            {
 02596                return;
 2597            }
 2598
 02599            if (video.OwnerId.IsEmpty())
 2600            {
 02601                video.OwnerId = Id;
 2602            }
 2603
 02604            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02605        }
 2606
 2607        public string GetEtag(User user)
 2608        {
 62609            var list = GetEtagValues(user);
 2610
 62611            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2612        }
 2613
 2614        protected virtual List<string> GetEtagValues(User user)
 2615        {
 62616            return
 62617            [
 62618                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62619            ];
 2620        }
 2621
 2622        public virtual IEnumerable<Guid> GetAncestorIds()
 2623        {
 1112624            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2625        }
 2626
 2627        public BaseItem GetTopParent()
 2628        {
 1942629            if (IsTopParent)
 2630            {
 212631                return this;
 2632            }
 2633
 1732634            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2635        }
 2636
 2637        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2638        {
 442639            return [Id];
 2640        }
 2641
 2642        public virtual double? GetRefreshProgress()
 2643        {
 02644            return null;
 2645        }
 2646
 2647        public virtual ItemUpdateType OnMetadataChanged()
 2648        {
 1142649            var updateType = ItemUpdateType.None;
 2650
 1142651            var item = this;
 2652
 1142653            var rating = item.GetParentalRatingScore();
 1142654            if (rating is not null)
 2655            {
 02656                if (rating.Score != item.InheritedParentalRatingValue)
 2657                {
 02658                    item.InheritedParentalRatingValue = rating.Score;
 02659                    updateType |= ItemUpdateType.MetadataImport;
 2660                }
 2661
 02662                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2663                {
 02664                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02665                    updateType |= ItemUpdateType.MetadataImport;
 2666                }
 2667            }
 2668            else
 2669            {
 1142670                if (item.InheritedParentalRatingValue is not null)
 2671                {
 02672                    item.InheritedParentalRatingValue = null;
 02673                    item.InheritedParentalRatingSubValue = null;
 02674                    updateType |= ItemUpdateType.MetadataImport;
 2675                }
 2676            }
 2677
 1142678            return updateType;
 2679        }
 2680
 2681        /// <summary>
 2682        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2683        /// </summary>
 2684        /// <param name="children">Media children.</param>
 2685        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2686        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2687        {
 02688            var currentOfficialRating = OfficialRating;
 2689
 2690            // Gather all possible ratings
 02691            var ratings = children
 02692                .Select(i => i.OfficialRating)
 02693                .Where(i => !string.IsNullOrEmpty(i))
 02694                .Distinct(StringComparer.OrdinalIgnoreCase)
 02695                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 02696                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02697                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02698                .Select(i => i.rating);
 2699
 02700            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2701
 02702            return !string.Equals(
 02703                currentOfficialRating ?? string.Empty,
 02704                OfficialRating ?? string.Empty,
 02705                StringComparison.OrdinalIgnoreCase);
 2706        }
 2707
 2708        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2709        {
 02710            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2711        }
 2712
 2713        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2714        {
 02715            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, 
 2716        }
 2717
 2718        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2719        {
 02720            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2721        }
 2722
 2723        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2724        {
 02725            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user,
 2726        }
 2727
 2728        /// <summary>
 2729        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2730        /// </summary>
 2731        /// <returns>An enumerable containing the items.</returns>
 2732        public IEnumerable<BaseItem> GetExtras()
 2733        {
 62734            return LibraryManager.GetItemList(new InternalItemsQuery()
 62735            {
 62736                OwnerIds = [Id],
 62737                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 62738            });
 2739        }
 2740
 2741        /// <summary>
 2742        /// Get all extras with specific types that are associated with this item.
 2743        /// </summary>
 2744        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2745        /// <returns>An enumerable containing the extras.</returns>
 2746        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
 2747        {
 02748            return LibraryManager.GetItemList(new InternalItemsQuery()
 02749            {
 02750                OwnerIds = [Id],
 02751                ExtraTypes = extraTypes.ToArray(),
 02752                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 02753            });
 2754        }
 2755
 2756        public virtual long GetRunTimeTicksForPlayState()
 2757        {
 02758            return RunTimeTicks ?? 0;
 2759        }
 2760
 2761        /// <inheritdoc />
 2762        public override bool Equals(object obj)
 2763        {
 02764            return obj is BaseItem baseItem && this.Equals(baseItem);
 2765        }
 2766
 2767        /// <inheritdoc />
 1142768        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2769
 2770        /// <inheritdoc />
 1002771        public override int GetHashCode() => HashCode.Combine(Id);
 2772    }
 2773}

Methods/Properties

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