< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.BaseItem
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs
Line coverage
54%
Covered lines: 505
Uncovered lines: 422
Coverable lines: 927
Total lines: 2913
Line coverage: 54.4%
Branch coverage
41%
Covered branches: 244
Total branches: 594
Branch coverage: 41%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/28/2026 - 12:14:04 AM Line coverage: 51.2% (375/731) Branch coverage: 40.2% (185/460) Total lines: 26884/7/2026 - 12:14:03 AM Line coverage: 51% (374/732) Branch coverage: 39.8% (185/464) Total lines: 26944/19/2026 - 12:14:27 AM Line coverage: 47.2% (404/855) Branch coverage: 38.6% (207/536) Total lines: 26945/4/2026 - 12:15:16 AM Line coverage: 45.4% (403/887) Branch coverage: 36.5% (202/552) Total lines: 27705/8/2026 - 12:15:13 AM Line coverage: 45.5% (404/887) Branch coverage: 36.5% (202/552) Total lines: 27735/20/2026 - 12:15:44 AM Line coverage: 45.5% (404/887) Branch coverage: 32.4% (179/552) Total lines: 27735/27/2026 - 12:15:38 AM Line coverage: 45.6% (406/890) Branch coverage: 32.6% (182/558) Total lines: 27886/2/2026 - 12:15:49 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27796/6/2026 - 12:15:50 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27817/6/2026 - 12:16:28 AM Line coverage: 54.4% (505/927) Branch coverage: 41% (244/594) Total lines: 2913 3/28/2026 - 12:14:04 AM Line coverage: 51.2% (375/731) Branch coverage: 40.2% (185/460) Total lines: 26884/7/2026 - 12:14:03 AM Line coverage: 51% (374/732) Branch coverage: 39.8% (185/464) Total lines: 26944/19/2026 - 12:14:27 AM Line coverage: 47.2% (404/855) Branch coverage: 38.6% (207/536) Total lines: 26945/4/2026 - 12:15:16 AM Line coverage: 45.4% (403/887) Branch coverage: 36.5% (202/552) Total lines: 27705/8/2026 - 12:15:13 AM Line coverage: 45.5% (404/887) Branch coverage: 36.5% (202/552) Total lines: 27735/20/2026 - 12:15:44 AM Line coverage: 45.5% (404/887) Branch coverage: 32.4% (179/552) Total lines: 27735/27/2026 - 12:15:38 AM Line coverage: 45.6% (406/890) Branch coverage: 32.6% (182/558) Total lines: 27886/2/2026 - 12:15:49 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27796/6/2026 - 12:15:50 AM Line coverage: 45.9% (408/888) Branch coverage: 33.2% (183/550) Total lines: 27817/6/2026 - 12:16:28 AM Line coverage: 54.4% (505/927) Branch coverage: 41% (244/594) Total lines: 2913

Coverage delta

Coverage delta 9 -9

Metrics

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

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591, SA1401
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Text;
 12using System.Text.Json.Serialization;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Common.Extensions;
 21using MediaBrowser.Controller.Channels;
 22using MediaBrowser.Controller.Chapters;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Dto;
 25using MediaBrowser.Controller.Entities.TV;
 26using MediaBrowser.Controller.Library;
 27using MediaBrowser.Controller.MediaSegments;
 28using MediaBrowser.Controller.Persistence;
 29using MediaBrowser.Controller.Providers;
 30using MediaBrowser.Model.Dto;
 31using MediaBrowser.Model.Entities;
 32using MediaBrowser.Model.Globalization;
 33using MediaBrowser.Model.IO;
 34using MediaBrowser.Model.Library;
 35using MediaBrowser.Model.LiveTv;
 36using MediaBrowser.Model.MediaInfo;
 37using Microsoft.Extensions.Logging;
 38
 39namespace MediaBrowser.Controller.Entities
 40{
 41    /// <summary>
 42    /// Class BaseItem.
 43    /// </summary>
 44    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 45    {
 46        private BaseItemKind? _baseItemKind;
 47
 48        public const string ThemeSongFileName = "theme";
 49
 50        /// <summary>
 51        /// The supported image extensions.
 52        /// </summary>
 453        public static readonly string[] SupportedImageExtensions
 454            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 55
 456        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 457        {
 458            ".nfo",
 459            ".xml",
 460            ".srt",
 461            ".vtt",
 462            ".sub",
 463            ".sup",
 464            ".idx",
 465            ".txt",
 466            ".edl",
 467            ".bif",
 468            ".smi",
 469            ".ttml",
 470            ".lrc",
 471            ".elrc"
 472        };
 73
 74        /// <summary>
 75        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 76        /// </summary>
 477        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 478        {
 479            Model.Entities.ExtraType.Unknown,
 480            Model.Entities.ExtraType.BehindTheScenes,
 481            Model.Entities.ExtraType.Clip,
 482            Model.Entities.ExtraType.DeletedScene,
 483            Model.Entities.ExtraType.Interview,
 484            Model.Entities.ExtraType.Sample,
 485            Model.Entities.ExtraType.Scene,
 486            Model.Entities.ExtraType.Featurette,
 487            Model.Entities.ExtraType.Short
 488        };
 89
 490        private static readonly char[] VersionDelimiters = ['-', '_', '.'];
 91
 92        private string _sortName;
 93
 94        private string _forcedSortName;
 95
 96        private string _name;
 97
 98        private string _originalLanguage;
 99
 100        public const char SlugChar = '-';
 101
 102        protected BaseItem()
 103        {
 1208104            Tags = Array.Empty<string>();
 1208105            Genres = Array.Empty<string>();
 1208106            Studios = Array.Empty<string>();
 1208107            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 1208108            LockedFields = Array.Empty<MetadataField>();
 1208109            ImageInfos = Array.Empty<ItemImageInfo>();
 1208110            ProductionLocations = Array.Empty<string>();
 1208111            RemoteTrailers = Array.Empty<MediaUrl>();
 1208112            UserData = [];
 1208113        }
 114
 115        /// <summary>
 116        /// Gets or Sets the user data collection as cached from the last Db query.
 117        /// </summary>
 118        [JsonIgnore]
 119        public ICollection<UserData> UserData { get; set; }
 120
 121        [JsonIgnore]
 122        public string PreferredMetadataCountryCode { get; set; }
 123
 124        [JsonIgnore]
 125        public string PreferredMetadataLanguage { get; set; }
 126
 127        public long? Size { get; set; }
 128
 129        public string Container { get; set; }
 130
 131        [JsonIgnore]
 132        public string Tagline { get; set; }
 133
 134        [JsonIgnore]
 135        public virtual ItemImageInfo[] ImageInfos { get; set; }
 136
 137        [JsonIgnore]
 138        public bool IsVirtualItem { get; set; }
 139
 140        /// <summary>
 141        /// Gets or sets the album.
 142        /// </summary>
 143        /// <value>The album.</value>
 144        [JsonIgnore]
 145        public string Album { get; set; }
 146
 147        /// <summary>
 148        /// Gets or sets the LUFS value.
 149        /// </summary>
 150        /// <value>The LUFS Value.</value>
 151        [JsonIgnore]
 152        public float? LUFS { get; set; }
 153
 154        /// <summary>
 155        /// Gets or sets the gain required for audio normalization.
 156        /// </summary>
 157        /// <value>The gain required for audio normalization.</value>
 158        [JsonIgnore]
 159        public float? NormalizationGain { get; set; }
 160
 161        /// <summary>
 162        /// Gets or sets the channel identifier.
 163        /// </summary>
 164        /// <value>The channel identifier.</value>
 165        [JsonIgnore]
 166        public Guid ChannelId { get; set; }
 167
 168        [JsonIgnore]
 0169        public virtual bool SupportsAddingToPlaylist => false;
 170
 171        [JsonIgnore]
 14172        public virtual bool AlwaysScanInternalMetadataPath => false;
 173
 174        /// <summary>
 175        /// Gets or sets a value indicating whether this instance is in mixed folder.
 176        /// </summary>
 177        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 178        [JsonIgnore]
 179        public bool IsInMixedFolder { get; set; }
 180
 181        [JsonIgnore]
 0182        public virtual bool SupportsPlayedStatus => false;
 183
 184        [JsonIgnore]
 0185        public virtual bool SupportsPositionTicksResume => false;
 186
 187        [JsonIgnore]
 17188        public virtual bool SupportsRemoteImageDownloading => true;
 189
 190        /// <summary>
 191        /// Gets or sets the name.
 192        /// </summary>
 193        /// <value>The name.</value>
 194        [JsonIgnore]
 195        public virtual string Name
 196        {
 1036197            get => _name;
 198            set
 199            {
 419200                _name = value;
 201
 202                // lazy load this again
 419203                _sortName = null;
 419204            }
 205        }
 206
 207        [JsonIgnore]
 0208        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 209
 210        [JsonIgnore]
 211        public int? TotalBitrate { get; set; }
 212
 213        [JsonIgnore]
 214        public ExtraType? ExtraType { get; set; }
 215
 216        [JsonIgnore]
 0217        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 218
 219        [JsonIgnore]
 220        public string OriginalTitle { get; set; }
 221
 222        [JsonIgnore]
 223        public string OriginalLanguage
 224        {
 421225            get => _originalLanguage;
 232226            set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
 227        }
 228
 229        /// <summary>
 230        /// Gets or sets the id.
 231        /// </summary>
 232        /// <value>The id.</value>
 233        [JsonIgnore]
 234        public Guid Id { get; set; }
 235
 236        [JsonIgnore]
 237        public Guid OwnerId { get; set; }
 238
 239        /// <summary>
 240        /// Gets or sets the audio.
 241        /// </summary>
 242        /// <value>The audio.</value>
 243        [JsonIgnore]
 244        public ProgramAudio? Audio { get; set; }
 245
 246        /// <summary>
 247        /// Gets the id that should be used to key display prefs for this item.
 248        /// Default is based on the type for everything except actual generic folders.
 249        /// </summary>
 250        /// <value>The display prefs id.</value>
 251        [JsonIgnore]
 252        public virtual Guid DisplayPreferencesId
 253        {
 254            get
 255            {
 6256                var thisType = GetType();
 6257                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 258            }
 259        }
 260
 261        /// <summary>
 262        /// Gets or sets the path.
 263        /// </summary>
 264        /// <value>The path.</value>
 265        [JsonIgnore]
 266        public virtual string Path { get; set; }
 267
 268        [JsonIgnore]
 269        public virtual SourceType SourceType
 270        {
 271            get
 272            {
 2351273                if (!ChannelId.IsEmpty())
 274                {
 0275                    return SourceType.Channel;
 276                }
 277
 2351278                return SourceType.Library;
 279            }
 280        }
 281
 282        /// <summary>
 283        /// Gets the folder containing the item.
 284        /// If the item is a folder, it returns the folder itself.
 285        /// </summary>
 286        [JsonIgnore]
 287        public virtual string ContainingFolderPath
 288        {
 289            get
 290            {
 475291                if (IsFolder)
 292                {
 411293                    return Path;
 294                }
 295
 64296                return System.IO.Path.GetDirectoryName(Path);
 297            }
 298        }
 299
 300        /// <summary>
 301        /// Gets or sets the name of the service.
 302        /// </summary>
 303        /// <value>The name of the service.</value>
 304        [JsonIgnore]
 305        public string ServiceName { get; set; }
 306
 307        /// <summary>
 308        /// Gets or sets the external id.
 309        /// </summary>
 310        /// <remarks>
 311        /// If this content came from an external service, the id of the content on that service.
 312        /// </remarks>
 313        [JsonIgnore]
 314        public string ExternalId { get; set; }
 315
 316        [JsonIgnore]
 317        public string ExternalSeriesId { get; set; }
 318
 319        [JsonIgnore]
 0320        public virtual bool IsHidden => false;
 321
 322        /// <summary>
 323        /// Gets the type of the location.
 324        /// </summary>
 325        /// <value>The type of the location.</value>
 326        [JsonIgnore]
 327        public virtual LocationType LocationType
 328        {
 329            get
 330            {
 14331                var path = Path;
 14332                if (string.IsNullOrEmpty(path))
 333                {
 5334                    if (SourceType == SourceType.Channel)
 335                    {
 0336                        return LocationType.Remote;
 337                    }
 338
 5339                    return LocationType.Virtual;
 340                }
 341
 9342                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 343            }
 344        }
 345
 346        [JsonIgnore]
 347        public MediaProtocol? PathProtocol
 348        {
 349            get
 350            {
 2603351                var path = Path;
 352
 2603353                if (string.IsNullOrEmpty(path))
 354                {
 116355                    return null;
 356                }
 357
 2487358                return MediaSourceManager.GetPathProtocol(path);
 359            }
 360        }
 361
 362        [JsonIgnore]
 2564363        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 364
 365        [JsonIgnore]
 0366        public bool HasPathProtocol => PathProtocol.HasValue;
 367
 368        [JsonIgnore]
 369        public virtual bool SupportsLocalMetadata
 370        {
 371            get
 372            {
 1352373                if (SourceType == SourceType.Channel)
 374                {
 0375                    return false;
 376                }
 377
 1352378                return IsFileProtocol;
 379            }
 380        }
 381
 382        [JsonIgnore]
 383        public virtual string FileNameWithoutExtension
 384        {
 385            get
 386            {
 0387                if (IsFileProtocol)
 388                {
 0389                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 390                }
 391
 0392                return null;
 393            }
 394        }
 395
 396        [JsonIgnore]
 105397        public virtual bool EnableAlphaNumericSorting => true;
 398
 146399        public virtual bool IsHD => Height >= 720;
 400
 401        public bool IsShortcut { get; set; }
 402
 403        public string ShortcutPath { get; set; }
 404
 405        public int Width { get; set; }
 406
 407        public int Height { get; set; }
 408
 409        /// <summary>
 410        /// Gets the primary image path.
 411        /// </summary>
 412        /// <remarks>
 413        /// This is just a helper for convenience.
 414        /// </remarks>
 415        /// <value>The primary image path.</value>
 416        [JsonIgnore]
 0417        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 418
 419        /// <summary>
 420        /// Gets or sets the date created.
 421        /// </summary>
 422        /// <value>The date created.</value>
 423        [JsonIgnore]
 424        public DateTime DateCreated { get; set; }
 425
 426        /// <summary>
 427        /// Gets or sets the date modified.
 428        /// </summary>
 429        /// <value>The date modified.</value>
 430        [JsonIgnore]
 431        public DateTime DateModified { get; set; }
 432
 433        public DateTime DateLastSaved { get; set; }
 434
 435        [JsonIgnore]
 436        public DateTime DateLastRefreshed { get; set; }
 437
 438        [JsonIgnore]
 439        public bool IsLocked { get; set; }
 440
 441        /// <summary>
 442        /// Gets or sets the locked fields.
 443        /// </summary>
 444        /// <value>The locked fields.</value>
 445        [JsonIgnore]
 446        public MetadataField[] LockedFields { get; set; }
 447
 448        /// <summary>
 449        /// Gets the type of the media.
 450        /// </summary>
 451        /// <value>The type of the media.</value>
 452        [JsonIgnore]
 138453        public virtual MediaType MediaType => MediaType.Unknown;
 454
 455        [JsonIgnore]
 456        public virtual string[] PhysicalLocations
 457        {
 458            get
 459            {
 0460                if (!IsFileProtocol)
 461                {
 0462                    return Array.Empty<string>();
 463                }
 464
 0465                return [Path];
 466            }
 467        }
 468
 469        [JsonIgnore]
 470        public bool EnableMediaSourceDisplay
 471        {
 472            get
 473            {
 6474                if (SourceType == SourceType.Channel)
 475                {
 0476                    return ChannelManager.EnableMediaSourceDisplay(this);
 477                }
 478
 6479                return true;
 480            }
 481        }
 482
 483        [JsonIgnore]
 484        public Guid ParentId { get; set; }
 485
 486        /// <summary>
 487        /// Gets or sets the logger.
 488        /// </summary>
 489        public static ILogger<BaseItem> Logger { get; set; }
 490
 491        public static ILibraryManager LibraryManager { get; set; }
 492
 493        public static IServerConfigurationManager ConfigurationManager { get; set; }
 494
 495        public static IProviderManager ProviderManager { get; set; }
 496
 497        public static ILocalizationManager LocalizationManager { get; set; }
 498
 499        public static IItemRepository ItemRepository { get; set; }
 500
 501        public static IItemCountService ItemCountService { get; set; }
 502
 503        public static IChapterManager ChapterManager { get; set; }
 504
 505        public static IFileSystem FileSystem { get; set; }
 506
 507        public static IUserDataManager UserDataManager { get; set; }
 508
 509        public static IChannelManager ChannelManager { get; set; }
 510
 511        public static IMediaSourceManager MediaSourceManager { get; set; }
 512
 513        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 514
 515        /// <summary>
 516        /// Gets or sets the name of the forced sort.
 517        /// </summary>
 518        /// <value>The name of the forced sort.</value>
 519        [JsonIgnore]
 520        public string ForcedSortName
 521        {
 491522            get => _forcedSortName;
 523            set
 524            {
 92525                _forcedSortName = value;
 92526                _sortName = null;
 92527            }
 528        }
 529
 530        /// <summary>
 531        /// Gets or sets the name of the sort.
 532        /// </summary>
 533        /// <value>The name of the sort.</value>
 534        [JsonIgnore]
 535        public string SortName
 536        {
 537            get
 538            {
 178539                if (_sortName is null)
 540                {
 105541                    if (!string.IsNullOrEmpty(ForcedSortName))
 542                    {
 543                        // Need the ToLower because that's what CreateSortName does
 0544                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 545                    }
 546                    else
 547                    {
 105548                        _sortName = CreateSortName();
 549                    }
 550                }
 551
 178552                return _sortName;
 553            }
 554
 133555            set => _sortName = value;
 556        }
 557
 558        [JsonIgnore]
 334559        public virtual Guid DisplayParentId => ParentId;
 560
 561        [JsonIgnore]
 562        public BaseItem DisplayParent
 563        {
 564            get
 565            {
 334566                var id = DisplayParentId;
 334567                if (id.IsEmpty())
 568                {
 252569                    return null;
 570                }
 571
 82572                return LibraryManager.GetItemById(id);
 573            }
 574        }
 575
 576        /// <summary>
 577        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 578        /// </summary>
 579        /// <value>The premiere date.</value>
 580        [JsonIgnore]
 581        public DateTime? PremiereDate { get; set; }
 582
 583        /// <summary>
 584        /// Gets or sets the end date.
 585        /// </summary>
 586        /// <value>The end date.</value>
 587        [JsonIgnore]
 588        public DateTime? EndDate { get; set; }
 589
 590        /// <summary>
 591        /// Gets or sets the official rating.
 592        /// </summary>
 593        /// <value>The official rating.</value>
 594        [JsonIgnore]
 595        public string OfficialRating { get; set; }
 596
 597        [JsonIgnore]
 598        public int? InheritedParentalRatingValue { get; set; }
 599
 600        [JsonIgnore]
 601        public int? InheritedParentalRatingSubValue { get; set; }
 602
 603        /// <summary>
 604        /// Gets or sets the critic rating.
 605        /// </summary>
 606        /// <value>The critic rating.</value>
 607        [JsonIgnore]
 608        public float? CriticRating { get; set; }
 609
 610        /// <summary>
 611        /// Gets or sets the custom rating.
 612        /// </summary>
 613        /// <value>The custom rating.</value>
 614        [JsonIgnore]
 615        public string CustomRating { get; set; }
 616
 617        /// <summary>
 618        /// Gets or sets the overview.
 619        /// </summary>
 620        /// <value>The overview.</value>
 621        [JsonIgnore]
 622        public string Overview { get; set; }
 623
 624        /// <summary>
 625        /// Gets or sets the studios.
 626        /// </summary>
 627        /// <value>The studios.</value>
 628        [JsonIgnore]
 629        public string[] Studios { get; set; }
 630
 631        /// <summary>
 632        /// Gets or sets the genres.
 633        /// </summary>
 634        /// <value>The genres.</value>
 635        [JsonIgnore]
 636        public string[] Genres { get; set; }
 637
 638        /// <summary>
 639        /// Gets or sets the tags.
 640        /// </summary>
 641        /// <value>The tags.</value>
 642        [JsonIgnore]
 643        public string[] Tags { get; set; }
 644
 645        [JsonIgnore]
 646        public string[] ProductionLocations { get; set; }
 647
 648        /// <summary>
 649        /// Gets or sets the home page URL.
 650        /// </summary>
 651        /// <value>The home page URL.</value>
 652        [JsonIgnore]
 653        public string HomePageUrl { get; set; }
 654
 655        /// <summary>
 656        /// Gets or sets the community rating.
 657        /// </summary>
 658        /// <value>The community rating.</value>
 659        [JsonIgnore]
 660        public float? CommunityRating { get; set; }
 661
 662        /// <summary>
 663        /// Gets or sets the run time ticks.
 664        /// </summary>
 665        /// <value>The run time ticks.</value>
 666        [JsonIgnore]
 667        public long? RunTimeTicks { get; set; }
 668
 669        /// <summary>
 670        /// Gets or sets the production year.
 671        /// </summary>
 672        /// <value>The production year.</value>
 673        [JsonIgnore]
 674        public int? ProductionYear { get; set; }
 675
 676        /// <summary>
 677        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 678        /// This could be episode number, album track number, etc.
 679        /// </summary>
 680        /// <value>The index number.</value>
 681        [JsonIgnore]
 682        public int? IndexNumber { get; set; }
 683
 684        /// <summary>
 685        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 686        /// </summary>
 687        /// <value>The parent index number.</value>
 688        [JsonIgnore]
 689        public int? ParentIndexNumber { get; set; }
 690
 691        [JsonIgnore]
 0692        public virtual bool HasLocalAlternateVersions => false;
 693
 694        [JsonIgnore]
 695        public string OfficialRatingForComparison
 696        {
 697            get
 698            {
 164699                var officialRating = OfficialRating;
 164700                if (!string.IsNullOrEmpty(officialRating))
 701                {
 0702                    return officialRating;
 703                }
 704
 164705                var parent = DisplayParent;
 164706                if (parent is not null)
 707                {
 38708                    return parent.OfficialRatingForComparison;
 709                }
 710
 126711                return null;
 712            }
 713        }
 714
 715        [JsonIgnore]
 716        public string CustomRatingForComparison
 717        {
 718            get
 719            {
 126720                return GetCustomRatingForComparision();
 721            }
 722        }
 723
 724        /// <summary>
 725        /// Gets or sets the provider ids.
 726        /// </summary>
 727        /// <value>The provider ids.</value>
 728        [JsonIgnore]
 729        public Dictionary<string, string> ProviderIds { get; set; }
 730
 731        [JsonIgnore]
 0732        public virtual Folder LatestItemsIndexContainer => null;
 733
 734        [JsonIgnore]
 735        public string PresentationUniqueKey { get; set; }
 736
 737        [JsonIgnore]
 31738        public virtual bool EnableRememberingTrackSelections => true;
 739
 740        [JsonIgnore]
 741        public virtual bool IsTopParent
 742        {
 743            get
 744            {
 360745                if (this is BasePluginFolder || this is Channel)
 746                {
 61747                    return true;
 748                }
 749
 299750                if (this is IHasCollectionType view)
 751                {
 13752                    if (view.CollectionType == CollectionType.livetv)
 753                    {
 0754                        return true;
 755                    }
 756                }
 757
 299758                if (GetParent() is AggregateFolder)
 759                {
 0760                    return true;
 761                }
 762
 299763                return false;
 764            }
 765        }
 766
 767        [JsonIgnore]
 468768        public virtual bool SupportsAncestors => true;
 769
 770        [JsonIgnore]
 93771        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 772
 773        [JsonIgnore]
 73774        public virtual bool SupportsPeople => false;
 775
 776        [JsonIgnore]
 0777        public virtual bool SupportsThemeMedia => false;
 778
 779        [JsonIgnore]
 0780        public virtual bool SupportsInheritedParentImages => false;
 781
 782        /// <summary>
 783        /// Gets a value indicating whether this instance is folder.
 784        /// </summary>
 785        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 786        [JsonIgnore]
 90787        public virtual bool IsFolder => false;
 788
 789        [JsonIgnore]
 0790        public virtual bool IsDisplayedAsFolder => false;
 791
 792        /// <summary>
 793        /// Gets or sets the remote trailers.
 794        /// </summary>
 795        /// <value>The remote trailers.</value>
 796        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 797
 798        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 799        {
 164800            callstack ??= new();
 164801            var customRating = CustomRating;
 164802            if (!string.IsNullOrEmpty(customRating))
 803            {
 0804                return customRating;
 805            }
 806
 164807            callstack.Add(Id);
 808
 164809            var parent = DisplayParent;
 164810            if (parent is not null && !callstack.Contains(parent.Id))
 811            {
 38812                return parent.GetCustomRatingForComparision(callstack);
 813            }
 814
 126815            return null;
 816        }
 817
 818        public virtual double GetDefaultPrimaryImageAspectRatio()
 819        {
 0820            return 0;
 821        }
 822
 823        public virtual string CreatePresentationUniqueKey()
 824        {
 58825            return Id.ToString("N", CultureInfo.InvariantCulture);
 826        }
 827
 828        public virtual bool CanDelete()
 829        {
 0830            if (SourceType == SourceType.Channel)
 831            {
 0832                return ChannelManager.CanDelete(this);
 833            }
 834
 0835            return IsFileProtocol;
 836        }
 837
 838        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 839        {
 0840            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 841            {
 0842                return true;
 843            }
 844
 0845            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 846
 0847            if (SourceType == SourceType.Channel)
 848            {
 0849                return allowed.Contains(ChannelId);
 850            }
 851
 0852            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 853
 0854            foreach (var folder in collectionFolders)
 855            {
 0856                if (allowed.Contains(folder.Id))
 857                {
 0858                    return true;
 859                }
 860            }
 861
 0862            return false;
 0863        }
 864
 865        public BaseItem GetOwner()
 866        {
 668867            var ownerId = OwnerId;
 668868            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 869        }
 870
 871        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 872        {
 6873            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 874        }
 875
 876        public virtual bool CanDelete(User user)
 877        {
 6878            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 879
 6880            return CanDelete(user, allCollectionFolders);
 881        }
 882
 883        public virtual bool CanDownload()
 884        {
 6885            return false;
 886        }
 887
 888        public virtual bool IsAuthorizedToDownload(User user)
 889        {
 0890            return user.HasPermission(PermissionKind.EnableContentDownloading);
 891        }
 892
 893        public bool CanDownload(User user)
 894        {
 6895            return CanDownload() && IsAuthorizedToDownload(user);
 896        }
 897
 898        /// <inheritdoc />
 899        public override string ToString()
 900        {
 69901            return Name;
 902        }
 903
 904        public virtual string GetInternalMetadataPath()
 905        {
 72906            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 907
 72908            return GetInternalMetadataPath(basePath);
 909        }
 910
 911        protected virtual string GetInternalMetadataPath(string basePath)
 912        {
 72913            if (SourceType == SourceType.Channel)
 914            {
 0915                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 916            }
 917
 72918            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 919
 72920            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 921        }
 922
 923        /// <summary>
 924        /// Creates the name of the sort.
 925        /// </summary>
 926        /// <returns>System.String.</returns>
 927        protected virtual string CreateSortName()
 928        {
 105929            if (Name is null)
 930            {
 0931                return null; // some items may not have name filled in properly
 932            }
 933
 105934            if (!EnableAlphaNumericSorting)
 935            {
 0936                return Name.TrimStart();
 937            }
 938
 105939            var sortable = Name.Trim().ToLowerInvariant();
 940
 840941            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 942            {
 943                // Remove from beginning if a space follows
 315944                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 945                {
 0946                    sortable = sortable.Remove(0, search.Length + 1);
 947                }
 948
 949                // Remove from middle if surrounded by spaces
 315950                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 951
 952                // Remove from end if preceeded by a space
 315953                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 954                {
 0955                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 956                }
 957            }
 958
 1470959            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 960            {
 630961                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 962            }
 963
 840964            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 965            {
 315966                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 967            }
 968
 105969            return ModifySortChunks(sortable);
 970        }
 971
 972        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 973        {
 974            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 975            {
 976                if (isDigitChunk && chunk.Length < 10)
 977                {
 978                    builder.Append('0', 10 - chunk.Length);
 979                }
 980
 981                builder.Append(chunk);
 982            }
 983
 111984            if (name.IsEmpty)
 985            {
 1986                return string.Empty;
 987            }
 988
 110989            var builder = new StringBuilder(name.Length);
 990
 110991            int chunkStart = 0;
 110992            bool isDigitChunk = char.IsDigit(name[0]);
 1758993            for (int i = 0; i < name.Length; i++)
 994            {
 769995                var isDigit = char.IsDigit(name[i]);
 769996                if (isDigit != isDigitChunk)
 997                {
 5998                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5999                    chunkStart = i;
 51000                    isDigitChunk = isDigit;
 1001                }
 1002            }
 1003
 1101004            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 1005
 1006            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 1101007            var result = builder.ToString().RemoveDiacritics();
 1101008            if (!result.All(char.IsAscii))
 1009            {
 01010                result = result.Transliterated();
 1011            }
 1012
 1101013            return result;
 1014        }
 1015
 1016        public BaseItem GetParent()
 1017        {
 21371018            var parentId = ParentId;
 21371019            if (parentId.IsEmpty())
 1020            {
 18521021                return null;
 1022            }
 1023
 2851024            return LibraryManager.GetItemById(parentId);
 1025        }
 1026
 1027        public IEnumerable<BaseItem> GetParents()
 1028        {
 7101029            var parent = GetParent();
 1030
 7831031            while (parent is not null)
 1032            {
 731033                yield return parent;
 1034
 731035                parent = parent.GetParent();
 1036            }
 7101037        }
 1038
 1039        /// <summary>
 1040        /// Finds a parent of a given type.
 1041        /// </summary>
 1042        /// <typeparam name="T">Type of parent.</typeparam>
 1043        /// <returns>``0.</returns>
 1044        public T FindParent<T>()
 1045            where T : Folder
 1046        {
 01047            foreach (var parent in GetParents())
 1048            {
 01049                if (parent is T item)
 1050                {
 01051                    return item;
 1052                }
 1053            }
 1054
 01055            return null;
 01056        }
 1057
 1058        /// <summary>
 1059        /// Gets the play access.
 1060        /// </summary>
 1061        /// <param name="user">The user.</param>
 1062        /// <returns>PlayAccess.</returns>
 1063        public PlayAccess GetPlayAccess(User user)
 1064        {
 61065            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1066            {
 01067                return PlayAccess.None;
 1068            }
 1069
 1070            // if (!user.IsParentalScheduleAllowed())
 1071            // {
 1072            //    return PlayAccess.None;
 1073            // }
 1074
 61075            return PlayAccess.Full;
 1076        }
 1077
 1078        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1079        {
 01080            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01081            {
 01082                ItemId = Id
 01083            });
 1084        }
 1085
 1086        protected virtual bool IsActiveRecording()
 1087        {
 01088            return false;
 1089        }
 1090
 1091        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1092        {
 61093            if (SourceType == SourceType.Channel)
 1094            {
 01095                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01096                           .ToList();
 1097
 01098                if (sources.Count > 0)
 1099                {
 01100                    return sources;
 1101                }
 1102            }
 1103
 61104            var list = GetAllItemsForMediaSources().ToList();
 61105            var commonPrefix = GetCommonNamePrefix(list);
 61106            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType, commonPrefix
 1107
 61108            if (IsActiveRecording())
 1109            {
 01110                foreach (var mediaSource in result)
 1111                {
 01112                    mediaSource.Type = MediaSourceType.Placeholder;
 1113                }
 1114            }
 1115
 1116            // The source belonging to the item being queried sorts first so it is the default the client plays.
 61117            var selfId = Id.ToString("N", CultureInfo.InvariantCulture);
 1118
 61119            return result
 61120                .OrderByDescending(i => string.Equals(i.Id, selfId, StringComparison.OrdinalIgnoreCase))
 61121                .ThenBy(i => i.VideoType == VideoType.VideoFile ? 0 : 1)
 61122                .ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 61123                .ThenByDescending(i => i, new MediaSourceWidthComparator())
 61124                .ToArray();
 1125        }
 1126
 1127        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1128        {
 01129            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1130        }
 1131
 1132        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type, string 
 1133        {
 181134            ArgumentNullException.ThrowIfNull(item);
 1135
 181136            var protocol = item.PathProtocol;
 181137            var itemPath = item.Path;
 1138
 181139            var info = new MediaSourceInfo
 181140            {
 181141                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 181142                Protocol = protocol ?? MediaProtocol.File,
 181143                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 181144                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 181145                Name = GetMediaSourceName(item, commonPrefix),
 181146                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 181147                RunTimeTicks = item.RunTimeTicks,
 181148                Container = item.Container,
 181149                Size = item.Size,
 181150                Type = type,
 181151                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 181152                    && (protocol is null or MediaProtocol.File)
 181153                    && MediaSegmentManager.HasSegments(item.Id)
 181154            };
 1155
 181156            if (string.IsNullOrEmpty(info.Path))
 1157            {
 01158                info.Type = MediaSourceType.Placeholder;
 1159            }
 1160
 181161            if (info.Protocol == MediaProtocol.File)
 1162            {
 181163                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1164            }
 1165
 181166            var video = item as Video;
 181167            if (video is not null)
 1168            {
 181169                info.IsoType = video.IsoType;
 181170                info.VideoType = video.VideoType;
 181171                info.Video3DFormat = video.Video3DFormat;
 181172                info.Timestamp = video.Timestamp;
 1173
 181174                if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
 1175                {
 01176                    var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
 1177
 1178                    // Only allow remote shortcut paths — local file paths in .strm files
 1179                    // could be used to read arbitrary files from the server.
 01180                    if (shortcutProtocol != MediaProtocol.File)
 1181                    {
 01182                        info.IsRemote = true;
 01183                        info.Path = video.ShortcutPath;
 01184                        info.Protocol = shortcutProtocol;
 1185                    }
 1186                }
 1187
 181188                if (string.IsNullOrEmpty(info.Container))
 1189                {
 181190                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1191                    {
 181192                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1193                        {
 181194                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1195                        }
 1196                    }
 1197                }
 1198            }
 1199
 181200            if (string.IsNullOrEmpty(info.Container))
 1201            {
 01202                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1203                {
 01204                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1205                }
 1206            }
 1207
 181208            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1209            {
 181210                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1211            }
 1212
 181213            if (video is not null && video.VideoType != VideoType.VideoFile)
 1214            {
 01215                info.SupportsDirectStream = false;
 1216            }
 1217
 181218            info.Bitrate = item.TotalBitrate;
 181219            info.InferTotalBitrate();
 1220
 181221            return info;
 1222        }
 1223
 1224        internal string GetMediaSourceName(BaseItem item, string commonPrefix = null)
 1225        {
 381226            var terms = new List<string>();
 1227
 381228            var path = item.Path;
 381229            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1230            {
 381231                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 1232
 1233                // Prefer the suffix that differs from the other versions: strip the prefix shared by
 1234                // all sibling files. This works regardless of folder layout, so it also labels episode
 1235                // versions that share a season folder (e.g. "Greyscale" instead of the full
 1236                // "Show - S01E02 - Title - Greyscale"). The prefix is already retreated to a delimiter
 1237                // boundary (see GetCommonVersionPrefix).
 381238                if (!string.IsNullOrEmpty(commonPrefix)
 381239                    && displayName.Length > commonPrefix.Length
 381240                    && displayName.StartsWith(commonPrefix, StringComparison.OrdinalIgnoreCase))
 1241                {
 271242                    var name = displayName.AsSpan(commonPrefix.Length).TrimStart([' ', .. VersionDelimiters]);
 271243                    if (!name.IsWhiteSpace())
 1244                    {
 271245                        terms.Add(name.ToString());
 1246                    }
 1247                }
 1248
 1249                // Fall back to the containing folder name (the common layout for movie versions, and
 1250                // the path taken when no common prefix could be derived).
 381251                if (terms.Count == 0 && HasLocalAlternateVersions)
 1252                {
 81253                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 81254                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1255                    {
 21256                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', .. VersionDelimiters]
 21257                        if (!name.IsWhiteSpace())
 1258                        {
 21259                            terms.Add(name.ToString());
 1260                        }
 1261                    }
 1262                }
 1263
 381264                if (terms.Count == 0)
 1265                {
 91266                    terms.Add(displayName);
 1267                }
 1268            }
 1269
 381270            if (terms.Count == 0)
 1271            {
 01272                terms.Add(item.Name);
 1273            }
 1274
 381275            if (item is Video video)
 1276            {
 381277                if (video.Video3DFormat.HasValue)
 1278                {
 01279                    terms.Add("3D");
 1280                }
 1281
 381282                if (video.VideoType == VideoType.BluRay)
 1283                {
 01284                    terms.Add("Bluray");
 1285                }
 381286                else if (video.VideoType == VideoType.Dvd)
 1287                {
 01288                    terms.Add("DVD");
 1289                }
 381290                else if (video.VideoType == VideoType.Iso)
 1291                {
 01292                    if (video.IsoType.HasValue)
 1293                    {
 01294                        if (video.IsoType.Value == IsoType.BluRay)
 1295                        {
 01296                            terms.Add("Bluray");
 1297                        }
 01298                        else if (video.IsoType.Value == IsoType.Dvd)
 1299                        {
 01300                            terms.Add("DVD");
 1301                        }
 1302                    }
 1303                    else
 1304                    {
 01305                        terms.Add("ISO");
 1306                    }
 1307                }
 1308            }
 1309
 381310            return string.Join('/', terms);
 1311        }
 1312
 1313        /// <summary>
 1314        /// Derives the prefix shared by the supplied media source items' file names, used to strip the
 1315        /// common part and surface a short version label per source. Returns null when there are fewer
 1316        /// than two file-based sources, since there is nothing to differentiate.
 1317        /// </summary>
 1318        /// <param name="items">The media source items.</param>
 1319        /// <returns>The shared prefix, or null when no useful prefix exists.</returns>
 1320        private static string GetCommonNamePrefix(IReadOnlyList<(BaseItem Item, MediaSourceType MediaSourceType)> items)
 1321        {
 61322            var fileNames = new List<string>();
 481323            foreach (var (item, _) in items)
 1324            {
 181325                if (item.IsFileProtocol && !string.IsNullOrEmpty(item.Path))
 1326                {
 181327                    fileNames.Add(System.IO.Path.GetFileNameWithoutExtension(item.Path));
 1328                }
 1329            }
 1330
 61331            if (fileNames.Count < 2)
 1332            {
 01333                return null;
 1334            }
 1335
 61336            var prefix = GetCommonVersionPrefix(fileNames);
 61337            return string.IsNullOrEmpty(prefix) ? null : prefix;
 1338        }
 1339
 1340        /// <summary>
 1341        /// Computes the case-insensitive longest common prefix of the supplied version file names,
 1342        /// retreated to the last delimiter boundary. Retreating keeps the differing suffix intact:
 1343        /// it avoids slicing through a word every version shares (e.g. "Grey" in "Greyscale" and
 1344        /// "Greyish") while still trimming the common part when every version is suffixed (e.g.
 1345        /// "- Greyscale" / "- Colorized"). It prefers a structural delimiter ('-', '_', '.') so a
 1346        /// token shared by the descriptors but separated only by spaces (e.g. a common "2160p ") is
 1347        /// kept in the label, falling back to a space only when no structural delimiter is shared. The
 1348        /// separators mirror the version delimiters recognised by the naming layer (Emby.Naming
 1349        /// VideoFlagDelimiters).
 1350        /// </summary>
 1351        /// <param name="fileNames">The version file names without extension; must contain at least one entry.</param>
 1352        /// <returns>The shared prefix retreated to a separator boundary, or an empty string when none is shared.</retur
 1353        internal static string GetCommonVersionPrefix(IReadOnlyList<string> fileNames)
 1354        {
 141355            var prefix = fileNames[0];
 681356            for (var i = 1; i < fileNames.Count && prefix.Length > 0; i++)
 1357            {
 201358                var name = fileNames[i];
 201359                var length = Math.Min(prefix.Length, name.Length);
 201360                var common = 0;
 2531361                while (common < length && char.ToUpperInvariant(prefix[common]) == char.ToUpperInvariant(name[common]))
 1362                {
 2331363                    common++;
 1364                }
 1365
 201366                prefix = prefix[..common];
 1367            }
 1368
 1369            // If the common prefix is itself a whole file name then one version is unlabelled (the
 1370            // base name); the boundary already sits at the end of that name, so don't retreat into it.
 141371            var prefixIsWholeName = false;
 601372            for (var i = 0; i < fileNames.Count; i++)
 1373            {
 231374                if (fileNames[i].Length == prefix.Length)
 1375                {
 71376                    prefixIsWholeName = true;
 71377                    break;
 1378                }
 1379            }
 1380
 141381            if (!prefixIsWholeName)
 1382            {
 1383                // Retreat to the last structural delimiter ('-', '_', '.').
 71384                var cut = prefix.Length;
 461385                while (cut > 0 && Array.IndexOf(VersionDelimiters, prefix[cut - 1]) < 0)
 1386                {
 391387                    cut--;
 1388                }
 1389
 71390                if (cut == 0)
 1391                {
 11392                    cut = prefix.Length;
 21393                    while (cut > 0 && prefix[cut - 1] != ' ')
 1394                    {
 11395                        cut--;
 1396                    }
 1397                }
 1398
 71399                prefix = prefix[..cut];
 1400            }
 1401
 141402            return prefix;
 1403        }
 1404
 1405        public Task RefreshMetadata(CancellationToken cancellationToken)
 1406        {
 511407            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1408        }
 1409
 1410        /// <summary>
 1411        /// The base implementation to refresh metadata.
 1412        /// </summary>
 1413        /// <param name="options">The options.</param>
 1414        /// <param name="cancellationToken">The cancellation token.</param>
 1415        /// <returns>true if a provider reports we changed.</returns>
 1416        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1417        {
 581418            var requiresSave = false;
 1419
 581420            if (SupportsOwnedItems)
 1421            {
 1422                try
 1423                {
 351424                    if (IsFileProtocol)
 1425                    {
 351426                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1427                    }
 1428
 351429                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 351430                }
 01431                catch (Exception ex)
 1432                {
 01433                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01434                }
 1435            }
 1436
 581437            var refreshOptions = requiresSave
 581438                ? new MetadataRefreshOptions(options)
 581439                {
 581440                    ForceSave = true
 581441                }
 581442                : options;
 1443
 581444            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 581445        }
 1446
 1447        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1448        {
 01449            if (!IsVisible(user))
 1450            {
 01451                return false;
 1452            }
 1453
 01454            var parents = GetParents().ToList();
 01455            if (parents.Any(i => !i.IsVisible(user, true)))
 1456            {
 01457                return false;
 1458            }
 1459
 01460            if (checkFolders)
 1461            {
 01462                var topParent = parents.Count > 0 ? parents[^1] : this;
 1463
 01464                if (string.IsNullOrEmpty(topParent.Path))
 1465                {
 01466                    return true;
 1467                }
 1468
 01469                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1470
 01471                if (itemCollectionFolders.Count > 0)
 1472                {
 01473                    var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 1474                    IEnumerable<Guid> userCollectionFolderIds;
 01475                    if (blockedMediaFolders.Length > 0)
 1476                    {
 1477                        // User has blocked folders - get all library folders and exclude blocked ones
 01478                        userCollectionFolderIds = LibraryManager.GetUserRootFolder().Children
 01479                            .Select(i => i.Id)
 01480                            .Where(id => !blockedMediaFolders.Contains(id));
 1481                    }
 01482                    else if (user.HasPermission(PermissionKind.EnableAllFolders))
 1483                    {
 1484                        // User can access all folders - no need to filter
 01485                        return true;
 1486                    }
 1487                    else
 1488                    {
 1489                        // User has specific enabled folders
 01490                        userCollectionFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders);
 1491                    }
 1492
 01493                    if (!itemCollectionFolders.Any(userCollectionFolderIds.Contains))
 1494                    {
 01495                        return false;
 1496                    }
 1497                }
 1498            }
 1499
 01500            return true;
 1501        }
 1502
 1503        public void SetParent(Folder parent)
 1504        {
 91505            ParentId = parent is null ? Guid.Empty : parent.Id;
 91506        }
 1507
 1508        /// <summary>
 1509        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1510        /// Returns true or false indicating if changes were found.
 1511        /// </summary>
 1512        /// <param name="options">The metadata refresh options.</param>
 1513        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1514        /// <param name="cancellationToken">The cancellation token.</param>
 1515        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1516        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1517        {
 351518            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1519            {
 351520                return false;
 1521            }
 1522
 01523            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 351524        }
 1525
 1526        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1527        {
 421528            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1529        }
 1530
 1531        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1532        {
 01533            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 01534            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1535
 01536            var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
 01537            {
 01538                OwnerIds = [item.Id]
 01539            }).Select(e => e.Id).ToArray();
 1540
 01541            var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
 1542
 01543            if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.Full
 1544            {
 01545                return false;
 1546            }
 1547
 01548            var ownerId = item.Id;
 1549
 01550            var tasks = extras.Select(i =>
 01551            {
 01552                var subOptions = new MetadataRefreshOptions(options);
 01553                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
 01554                {
 01555                    subOptions.ForceSave = true;
 01556                }
 01557
 01558                i.OwnerId = ownerId;
 01559                i.ParentId = Guid.Empty;
 01560                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 01561            });
 1562
 01563            var removedExtraIds = currentExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 01564            if (removedExtraIds.Length > 0)
 1565            {
 01566                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 01567                {
 01568                    ItemIds = removedExtraIds
 01569                });
 01570                foreach (var removedExtra in removedExtras)
 1571                {
 1572                    // Only delete items that are actual extras (have ExtraType set)
 1573                    // Items with OwnerId but no ExtraType might be alternate versions, not extras
 01574                    if (removedExtra.ExtraType.HasValue)
 1575                    {
 01576                        LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 01577                        {
 01578                            DeleteFileLocation = false
 01579                        });
 1580                    }
 1581                }
 1582            }
 1583
 01584            await Task.WhenAll(tasks).ConfigureAwait(false);
 1585
 01586            return true;
 01587        }
 1588
 1589        public string GetPresentationUniqueKey()
 1590        {
 01591            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1592        }
 1593
 1594        public virtual bool RequiresRefresh()
 1595        {
 581596            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1597            {
 581598                return false;
 1599            }
 1600
 01601            var info = FileSystem.GetFileSystemInfo(Path);
 1602
 01603            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1604        }
 1605
 1606        public virtual List<string> GetUserDataKeys()
 1607        {
 1591608            var list = new List<string>();
 1609
 1591610            if (SourceType == SourceType.Channel)
 1611            {
 01612                if (!string.IsNullOrEmpty(ExternalId))
 1613                {
 01614                    list.Add(ExternalId);
 1615                }
 1616            }
 1617
 1591618            list.Add(Id.ToString());
 1591619            return list;
 1620        }
 1621
 1622        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1623        {
 51624            var updateType = ItemUpdateType.None;
 1625
 51626            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1627            {
 01628                IsInMixedFolder = newItem.IsInMixedFolder;
 01629                updateType |= ItemUpdateType.MetadataImport;
 1630            }
 1631
 51632            return updateType;
 1633        }
 1634
 1635        public void AfterMetadataRefresh()
 1636        {
 581637            _sortName = null;
 581638        }
 1639
 1640        /// <summary>
 1641        /// Gets the preferred metadata language.
 1642        /// </summary>
 1643        /// <returns>System.String.</returns>
 1644        public string GetPreferredMetadataLanguage()
 1645        {
 361646            string lang = PreferredMetadataLanguage;
 1647
 361648            if (string.IsNullOrEmpty(lang))
 1649            {
 361650                lang = GetParents()
 361651                    .Select(i => i.PreferredMetadataLanguage)
 361652                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1653            }
 1654
 361655            if (string.IsNullOrEmpty(lang))
 1656            {
 361657                lang = LibraryManager.GetCollectionFolders(this)
 361658                    .Select(i => i.PreferredMetadataLanguage)
 361659                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1660            }
 1661
 361662            if (string.IsNullOrEmpty(lang))
 1663            {
 361664                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1665            }
 1666
 361667            if (string.IsNullOrEmpty(lang))
 1668            {
 361669                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1670            }
 1671
 361672            return lang;
 1673        }
 1674
 1675        /// <summary>
 1676        /// Gets the preferred metadata country code.
 1677        /// </summary>
 1678        /// <returns>System.String.</returns>
 1679        public string GetPreferredMetadataCountryCode()
 1680        {
 361681            string lang = PreferredMetadataCountryCode;
 1682
 361683            if (string.IsNullOrEmpty(lang))
 1684            {
 361685                lang = GetParents()
 361686                    .Select(i => i.PreferredMetadataCountryCode)
 361687                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1688            }
 1689
 361690            if (string.IsNullOrEmpty(lang))
 1691            {
 361692                lang = LibraryManager.GetCollectionFolders(this)
 361693                    .Select(i => i.PreferredMetadataCountryCode)
 361694                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1695            }
 1696
 361697            if (string.IsNullOrEmpty(lang))
 1698            {
 361699                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1700            }
 1701
 361702            if (string.IsNullOrEmpty(lang))
 1703            {
 361704                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1705            }
 1706
 361707            return lang;
 1708        }
 1709
 1710        /// <summary>
 1711        /// Gets the original language of the item, inheriting from parent items if necessary.
 1712        /// </summary>
 1713        /// <returns>System.String.</returns>
 1714        public virtual string GetInheritedOriginalLanguage()
 1715        {
 01716            return OriginalLanguage;
 1717        }
 1718
 1719        public virtual bool IsSaveLocalMetadataEnabled()
 1720        {
 881721            if (SourceType == SourceType.Channel)
 1722            {
 01723                return false;
 1724            }
 1725
 881726            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1727
 881728            return libraryOptions.SaveLocalMetadata;
 1729        }
 1730
 1731        /// <summary>
 1732        /// Determines if a given user has access to this item.
 1733        /// </summary>
 1734        /// <param name="user">The user.</param>
 1735        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1736        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1737        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1738        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1739        {
 101740            ArgumentNullException.ThrowIfNull(user);
 1741
 101742            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1743            {
 01744                return false;
 1745            }
 1746
 101747            var maxAllowedRating = user.MaxParentalRatingScore;
 101748            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 101749            var rating = CustomRatingForComparison;
 1750
 101751            if (string.IsNullOrEmpty(rating))
 1752            {
 101753                rating = OfficialRatingForComparison;
 1754            }
 1755
 101756            if (string.IsNullOrEmpty(rating))
 1757            {
 101758                return !GetBlockUnratedValue(user);
 1759            }
 1760
 01761            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1762
 1763            // Could not determine rating level
 01764            if (ratingScore is null)
 1765            {
 01766                var isAllowed = !GetBlockUnratedValue(user);
 1767
 01768                if (!isAllowed)
 1769                {
 01770                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1771                }
 1772
 01773                return isAllowed;
 1774            }
 1775
 01776            if (!maxAllowedRating.HasValue)
 1777            {
 01778                return true;
 1779            }
 1780
 01781            if (ratingScore.Score != maxAllowedRating.Value)
 1782            {
 01783                return ratingScore.Score < maxAllowedRating.Value;
 1784            }
 1785
 01786            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1787        }
 1788
 1789        public ParentalRatingScore GetParentalRatingScore()
 1790        {
 1161791            var rating = CustomRatingForComparison;
 1792
 1161793            if (string.IsNullOrEmpty(rating))
 1794            {
 1161795                rating = OfficialRatingForComparison;
 1796            }
 1797
 1161798            if (string.IsNullOrEmpty(rating))
 1799            {
 1161800                return null;
 1801            }
 1802
 01803            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1804        }
 1805
 1806        public List<string> GetInheritedTags()
 1807        {
 1171808            var list = new List<string>();
 1171809            list.AddRange(Tags);
 1810
 2901811            foreach (var parent in GetParents())
 1812            {
 281813                list.AddRange(parent.Tags);
 1814            }
 1815
 2341816            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1817            {
 01818                list.AddRange(folder.Tags);
 1819            }
 1820
 1171821            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1822        }
 1823
 1824        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1825        {
 101826            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 101827            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 1828
 101829            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 1830            {
 101831                return true;
 1832            }
 1833
 1834            // Normalize tags using the same logic as database queries
 01835            var normalizedBlockedTags = blockedTags
 01836                .Where(t => !string.IsNullOrWhiteSpace(t))
 01837                .Select(t => t.GetCleanValue())
 01838                .ToHashSet(StringComparer.Ordinal);
 1839
 01840            var normalizedItemTags = GetInheritedTags()
 01841                .Select(t => t.GetCleanValue())
 01842                .ToHashSet(StringComparer.Ordinal);
 1843
 1844            // Check blocked tags - item is hidden if it has any blocked tag
 01845            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 1846            {
 01847                return false;
 1848            }
 1849
 01850            var parent = GetParents().FirstOrDefault() ?? this;
 01851            if (parent is UserRootFolder or AggregateFolder or UserView)
 1852            {
 01853                return true;
 1854            }
 1855
 1856            // Check allowed tags - item must have at least one allowed tag
 01857            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 1858            {
 01859                var normalizedAllowedTags = allowedTags
 01860                    .Where(t => !string.IsNullOrWhiteSpace(t))
 01861                    .Select(t => t.GetCleanValue())
 01862                    .ToHashSet(StringComparer.Ordinal);
 1863
 01864                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 1865                {
 01866                    return false;
 1867                }
 1868            }
 1869
 01870            return true;
 1871        }
 1872
 1873        public virtual UnratedItem GetBlockUnratedType()
 1874        {
 1171875            if (SourceType == SourceType.Channel)
 1876            {
 01877                return UnratedItem.ChannelContent;
 1878            }
 1879
 1171880            return UnratedItem.Other;
 1881        }
 1882
 1883        /// <summary>
 1884        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1885        /// </summary>
 1886        /// <param name="user">The configuration.</param>
 1887        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1888        protected virtual bool GetBlockUnratedValue(User user)
 1889        {
 1890            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1891            // Special folders like series and albums will override this method.
 101892            if (IsFolder || this is IItemByName)
 1893            {
 101894                return false;
 1895            }
 1896
 01897            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1898        }
 1899
 1900        /// <summary>
 1901        /// Determines if this folder should be visible to a given user.
 1902        /// Default is just parental allowed. Can be overridden for more functionality.
 1903        /// </summary>
 1904        /// <param name="user">The user.</param>
 1905        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1906        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1907        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1908        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1909        {
 101910            ArgumentNullException.ThrowIfNull(user);
 1911
 101912            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1913        }
 1914
 1915        public virtual bool IsVisibleStandalone(User user)
 1916        {
 01917            if (SourceType == SourceType.Channel)
 1918            {
 01919                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1920            }
 1921
 01922            return IsVisibleStandaloneInternal(user, true);
 1923        }
 1924
 1925        public virtual string GetClientTypeName()
 1926        {
 1191927            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1928            {
 01929                return "ChannelFolderItem";
 1930            }
 1931
 1191932            return GetType().Name;
 1933        }
 1934
 1935        public BaseItemKind GetBaseItemKind()
 1936        {
 2511937            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1938        }
 1939
 1940        /// <summary>
 1941        /// Gets the linked child.
 1942        /// </summary>
 1943        /// <param name="info">The info.</param>
 1944        /// <returns>BaseItem.</returns>
 1945        protected BaseItem GetLinkedChild(LinkedChild info)
 1946        {
 1947            // First get using the cached Id
 01948            if (info.ItemId.HasValue)
 1949            {
 01950                if (info.ItemId.Value.IsEmpty())
 1951                {
 01952                    return null;
 1953                }
 1954
 01955                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1956
 01957                if (itemById is not null)
 1958                {
 01959                    return itemById;
 1960                }
 1961            }
 1962
 01963            var item = FindLinkedChild(info);
 1964
 1965            // If still null, log
 01966            if (item is null)
 1967            {
 1968                // Don't keep searching over and over
 01969                info.ItemId = Guid.Empty;
 1970            }
 1971            else
 1972            {
 1973                // Cache the id for next time
 01974                info.ItemId = item.Id;
 1975            }
 1976
 01977            return item;
 1978        }
 1979
 1980#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 1981        private BaseItem FindLinkedChild(LinkedChild info)
 1982        {
 1983            // First try to find by ItemId (new preferred method)
 01984            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 1985            {
 01986                var item = LibraryManager.GetItemById(info.ItemId.Value);
 01987                if (item is not null)
 1988                {
 01989                    return item;
 1990                }
 1991
 01992                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 1993            }
 1994
 1995            // Fall back to Path (legacy method)
 01996            var path = info.Path;
 01997            if (!string.IsNullOrEmpty(path))
 1998            {
 01999                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 2000
 02001                var itemByPath = LibraryManager.FindByPath(path, null);
 2002
 02003                if (itemByPath is null)
 2004                {
 02005                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 2006                }
 2007
 02008                return itemByPath;
 2009            }
 2010
 2011            // Fall back to LibraryItemId (legacy method)
 02012            if (!string.IsNullOrEmpty(info.LibraryItemId))
 2013            {
 02014                var item = LibraryManager.GetItemById(info.LibraryItemId);
 2015
 02016                if (item is null)
 2017                {
 02018                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 2019                }
 2020
 02021                return item;
 2022            }
 2023
 02024            return null;
 2025        }
 2026#pragma warning restore CS0618
 2027
 2028        /// <summary>
 2029        /// Adds a studio to the item.
 2030        /// </summary>
 2031        /// <param name="name">The name.</param>
 2032        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 2033        public void AddStudio(string name)
 2034        {
 42035            ArgumentException.ThrowIfNullOrEmpty(name);
 42036            var current = Studios;
 2037
 42038            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 2039            {
 42040                int curLen = current.Length;
 42041                if (curLen == 0)
 2042                {
 42043                    Studios = [name];
 2044                }
 2045                else
 2046                {
 02047                    Studios = [.. current, name];
 2048                }
 2049            }
 02050        }
 2051
 2052        public void SetStudios(IEnumerable<string> names)
 2053        {
 02054            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 02055        }
 2056
 2057        /// <summary>
 2058        /// Adds a genre to the item.
 2059        /// </summary>
 2060        /// <param name="name">The name.</param>
 2061        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 2062        public void AddGenre(string name)
 2063        {
 132064            ArgumentException.ThrowIfNullOrEmpty(name);
 2065
 132066            var genres = Genres;
 132067            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 2068            {
 132069                Genres = [.. genres, name];
 2070            }
 132071        }
 2072
 2073        /// <summary>
 2074        /// Marks the played.
 2075        /// </summary>
 2076        /// <param name="user">The user.</param>
 2077        /// <param name="datePlayed">The date played.</param>
 2078        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 2079        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2080        public virtual void MarkPlayed(
 2081            User user,
 2082            DateTime? datePlayed,
 2083            bool resetPosition)
 2084        {
 02085            ArgumentNullException.ThrowIfNull(user);
 2086
 02087            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 02088            {
 02089                Key = GetUserDataKeys().First(),
 02090            };
 2091
 02092            if (datePlayed.HasValue)
 2093            {
 2094                // Increment
 02095                data.PlayCount++;
 2096            }
 2097
 2098            // Ensure it's at least one
 02099            data.PlayCount = Math.Max(data.PlayCount, 1);
 2100
 02101            if (resetPosition)
 2102            {
 02103                data.PlaybackPositionTicks = 0;
 2104            }
 2105
 02106            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 02107            data.Played = true;
 2108
 02109            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02110        }
 2111
 2112        /// <summary>
 2113        /// Marks the unplayed.
 2114        /// </summary>
 2115        /// <param name="user">The user.</param>
 2116        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2117        public virtual void MarkUnplayed(User user)
 2118        {
 02119            ArgumentNullException.ThrowIfNull(user);
 2120
 02121            var data = UserDataManager.GetUserData(user, this);
 2122
 2123            // I think it is okay to do this here.
 2124            // if this is only called when a user is manually forcing something to un-played
 2125            // then it probably is what we want to do...
 02126            ResetPlayedState(data);
 2127
 02128            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02129        }
 2130
 2131        /// <summary>
 2132        /// Clears the played state on the supplied user data.
 2133        /// </summary>
 2134        /// <param name="data">The user data to reset.</param>
 2135        protected static void ResetPlayedState(UserItemData data)
 2136        {
 22137            ArgumentNullException.ThrowIfNull(data);
 2138
 22139            data.PlayCount = 0;
 22140            data.PlaybackPositionTicks = 0;
 22141            data.LastPlayedDate = null;
 22142            data.Played = false;
 22143        }
 2144
 2145        /// <summary>
 2146        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2147        /// </summary>
 2148        public virtual void ChangedExternally()
 2149        {
 02150            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02151        }
 2152
 2153        /// <summary>
 2154        /// Gets an image.
 2155        /// </summary>
 2156        /// <param name="type">The type.</param>
 2157        /// <param name="imageIndex">Index of the image.</param>
 2158        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2159        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2160        public bool HasImage(ImageType type, int imageIndex)
 2161        {
 1452162            return GetImageInfo(type, imageIndex) is not null;
 2163        }
 2164
 2165        public void SetImage(ItemImageInfo image, int index)
 2166        {
 242167            if (image.Type == ImageType.Chapter)
 2168            {
 02169                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2170            }
 2171
 242172            var existingImage = GetImageInfo(image.Type, index);
 2173
 242174            if (existingImage is null)
 2175            {
 212176                AddImage(image);
 2177            }
 2178            else
 2179            {
 32180                existingImage.Path = image.Path;
 32181                existingImage.DateModified = image.DateModified;
 32182                existingImage.Width = image.Width;
 32183                existingImage.Height = image.Height;
 32184                existingImage.BlurHash = image.BlurHash;
 2185            }
 32186        }
 2187
 2188        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2189        {
 462190            if (type == ImageType.Chapter)
 2191            {
 02192                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2193            }
 2194
 462195            var image = GetImageInfo(type, index);
 2196
 462197            if (image is null)
 2198            {
 452199                AddImage(GetImageInfo(file, type));
 2200            }
 2201            else
 2202            {
 12203                var imageInfo = GetImageInfo(file, type);
 2204
 12205                image.Path = file.FullName;
 12206                image.DateModified = imageInfo.DateModified;
 2207
 2208                // reset these values
 12209                image.Width = 0;
 12210                image.Height = 0;
 2211            }
 12212        }
 2213
 2214        /// <summary>
 2215        /// Deletes the image.
 2216        /// </summary>
 2217        /// <param name="type">The type.</param>
 2218        /// <param name="index">The index.</param>
 2219        /// <returns>A task.</returns>
 2220        public async Task DeleteImageAsync(ImageType type, int index)
 2221        {
 02222            var info = GetImageInfo(type, index);
 2223
 02224            if (info is null)
 2225            {
 2226                // Nothing to do
 02227                return;
 2228            }
 2229
 2230            // Remove from file system
 02231            var path = info.Path;
 02232            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2233            {
 02234                FileSystem.DeleteFile(path);
 2235            }
 2236
 2237            // Remove from item
 02238            RemoveImage(info);
 2239
 02240            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02241        }
 2242
 2243        public void RemoveImage(ItemImageInfo image)
 2244        {
 02245            RemoveImages([image]);
 02246        }
 2247
 2248        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2249        {
 92250            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92251        }
 2252
 2253        public void AddImage(ItemImageInfo image)
 2254        {
 662255            ImageInfos = [.. ImageInfos, image];
 662256        }
 2257
 2258        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1152259         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2260
 2261        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 352262            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2263
 2264        /// <summary>
 2265        /// Validates that images within the item are still on the filesystem.
 2266        /// </summary>
 2267        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2268        public bool ValidateImages()
 2269        {
 722270            List<ItemImageInfo> deletedImages = null;
 1742271            foreach (var imageInfo in ImageInfos)
 2272            {
 152273                if (!imageInfo.IsLocalFile)
 2274                {
 2275                    continue;
 2276                }
 2277
 152278                if (File.Exists(imageInfo.Path))
 2279                {
 2280                    continue;
 2281                }
 2282
 32283                (deletedImages ??= []).Add(imageInfo);
 2284            }
 2285
 722286            var anyImagesRemoved = deletedImages?.Count > 0;
 722287            if (anyImagesRemoved)
 2288            {
 22289                RemoveImages(deletedImages);
 2290            }
 2291
 722292            return anyImagesRemoved;
 2293        }
 2294
 2295        /// <summary>
 2296        /// Gets the image path.
 2297        /// </summary>
 2298        /// <param name="imageType">Type of the image.</param>
 2299        /// <param name="imageIndex">Index of the image.</param>
 2300        /// <returns>System.String.</returns>
 2301        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2302        public string GetImagePath(ImageType imageType, int imageIndex)
 42303            => GetImageInfo(imageType, imageIndex)?.Path;
 2304
 2305        /// <summary>
 2306        /// Gets the image information.
 2307        /// </summary>
 2308        /// <param name="imageType">Type of the image.</param>
 2309        /// <param name="imageIndex">Index of the image.</param>
 2310        /// <returns>ItemImageInfo.</returns>
 2311        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2312        {
 3252313            if (imageType == ImageType.Chapter)
 2314            {
 02315                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2316
 02317                if (chapter is null)
 2318                {
 02319                    return null;
 2320                }
 2321
 02322                var path = chapter.ImagePath;
 2323
 02324                if (string.IsNullOrEmpty(path))
 2325                {
 02326                    return null;
 2327                }
 2328
 02329                return new ItemImageInfo
 02330                {
 02331                    Path = path,
 02332                    DateModified = chapter.ImageDateModified,
 02333                    Type = imageType
 02334                };
 2335            }
 2336
 3252337            return GetImages(imageType)
 3252338                .ElementAtOrDefault(imageIndex);
 2339        }
 2340
 2341        /// <summary>
 2342        /// Computes image index for given image or raises if no matching image found.
 2343        /// </summary>
 2344        /// <param name="image">Image to compute index for.</param>
 2345        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2346        /// </exception>
 2347        /// <returns>Image index.</returns>
 2348        public int GetImageIndex(ItemImageInfo image)
 2349        {
 02350            ArgumentNullException.ThrowIfNull(image);
 2351
 02352            if (image.Type == ImageType.Chapter)
 2353            {
 02354                var chapters = ChapterManager.GetChapters(Id);
 02355                for (var i = 0; i < chapters.Count; i++)
 2356                {
 02357                    if (chapters[i].ImagePath == image.Path)
 2358                    {
 02359                        return i;
 2360                    }
 2361                }
 2362
 02363                throw new ArgumentException("No chapter index found for image path", image.Path);
 2364            }
 2365
 02366            var images = GetImages(image.Type).ToArray();
 02367            for (var i = 0; i < images.Length; i++)
 2368            {
 02369                if (images[i].Path == image.Path)
 2370                {
 02371                    return i;
 2372                }
 2373            }
 2374
 02375            throw new ArgumentException("No image index found for image path", image.Path);
 2376        }
 2377
 2378        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2379        {
 4332380            if (imageType == ImageType.Chapter)
 2381            {
 02382                throw new ArgumentException("No image info for chapter images");
 2383            }
 2384
 2385            // Yield return is more performant than LINQ Where on an Array
 14222386            for (var i = 0; i < ImageInfos.Length; i++)
 2387            {
 3092388                var imageInfo = ImageInfos[i];
 3092389                if (imageInfo.Type == imageType)
 2390                {
 1632391                    yield return imageInfo;
 2392                }
 2393            }
 4022394        }
 2395
 2396        /// <summary>
 2397        /// Adds the images, updating metadata if they already are part of this item.
 2398        /// </summary>
 2399        /// <param name="imageType">Type of the image.</param>
 2400        /// <param name="images">The images.</param>
 2401        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2402        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2403        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2404        {
 42405            if (imageType == ImageType.Chapter)
 2406            {
 02407                throw new ArgumentException("Cannot call AddImages with chapter images");
 2408            }
 2409
 42410            var existingImages = GetImages(imageType)
 42411                .ToList();
 2412
 42413            var newImageList = new List<FileSystemMetadata>();
 42414            var imageUpdated = false;
 2415
 242416            foreach (var newImage in images)
 2417            {
 82418                if (newImage is null)
 2419                {
 02420                    throw new ArgumentException("null image found in list");
 2421                }
 2422
 82423                var existing = existingImages
 82424                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2425
 82426                if (existing is null)
 2427                {
 42428                    newImageList.Add(newImage);
 2429                }
 2430                else
 2431                {
 42432                    if (existing.IsLocalFile)
 2433                    {
 42434                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2435
 2436                        // If date changed then we need to reset saved image dimensions
 42437                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2438                        {
 22439                            existing.Width = 0;
 22440                            existing.Height = 0;
 22441                            imageUpdated = true;
 2442                        }
 2443
 42444                        existing.DateModified = newDateModified;
 2445                    }
 2446                }
 2447            }
 2448
 42449            if (newImageList.Count > 0)
 2450            {
 22451                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2452            }
 2453
 42454            return imageUpdated || newImageList.Count > 0;
 2455        }
 2456
 2457        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2458        {
 502459            return new ItemImageInfo
 502460            {
 502461                Path = file.FullName,
 502462                Type = type,
 502463                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502464            };
 2465        }
 2466
 2467        /// <summary>
 2468        /// Gets the file system path to delete when the item is to be deleted.
 2469        /// </summary>
 2470        /// <returns>The metadata for the deleted paths.</returns>
 2471        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2472        {
 02473            return new[]
 02474            {
 02475                FileSystem.GetFileSystemInfo(Path)
 02476            }.Concat(GetLocalMetadataFilesToDelete());
 2477        }
 2478
 2479        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2480        {
 02481            if (IsFolder || !IsInMixedFolder)
 2482            {
 02483                return [];
 2484            }
 2485
 02486            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2487
 02488            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02489                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02490                .ToList();
 2491        }
 2492
 2493        public bool AllowsMultipleImages(ImageType type)
 2494        {
 232495            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2496        }
 2497
 2498        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2499        {
 02500            if (!AllowsMultipleImages(type))
 2501            {
 02502                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2503            }
 2504
 02505            var info1 = GetImageInfo(type, index1);
 02506            var info2 = GetImageInfo(type, index2);
 2507
 02508            if (info1 is null || info2 is null)
 2509            {
 2510                // Nothing to do
 02511                return Task.CompletedTask;
 2512            }
 2513
 02514            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2515            {
 2516                // TODO: Not supported  yet
 02517                return Task.CompletedTask;
 2518            }
 2519
 02520            var path1 = info1.Path;
 02521            var path2 = info2.Path;
 2522
 02523            FileSystem.SwapFiles(path1, path2);
 2524
 2525            // Refresh these values
 02526            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02527            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2528
 02529            info1.Width = 0;
 02530            info1.Height = 0;
 02531            info2.Width = 0;
 02532            info2.Height = 0;
 2533
 02534            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2535        }
 2536
 2537        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2538        {
 02539            userItemData ??= UserDataManager.GetUserData(user, this);
 2540
 02541            return userItemData is not null && userItemData.Played;
 2542        }
 2543
 2544        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2545        {
 02546            userItemData ??= UserDataManager.GetUserData(user, this);
 2547
 02548            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2549        }
 2550
 2551        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2552        {
 02553            ArgumentNullException.ThrowIfNull(user);
 2554
 02555            userItemData ??= UserDataManager.GetUserData(user, this);
 2556
 02557            return userItemData is null || !userItemData.Played;
 2558        }
 2559
 2560        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2561        {
 362562            return GetItemLookupInfo<ItemLookupInfo>();
 2563        }
 2564
 2565        protected T GetItemLookupInfo<T>()
 2566            where T : ItemLookupInfo, new()
 2567        {
 362568            return new T
 362569            {
 362570                Path = Path,
 362571                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 362572                MetadataLanguage = GetPreferredMetadataLanguage(),
 362573                Name = GetNameForMetadataLookup(),
 362574                OriginalTitle = OriginalTitle,
 362575                ProviderIds = ProviderIds,
 362576                IndexNumber = IndexNumber,
 362577                ParentIndexNumber = ParentIndexNumber,
 362578                Year = ProductionYear,
 362579                PremiereDate = PremiereDate
 362580            };
 2581        }
 2582
 2583        protected virtual string GetNameForMetadataLookup()
 2584        {
 362585            return Name;
 2586        }
 2587
 2588        /// <summary>
 2589        /// This is called before any metadata refresh and returns true if changes were made.
 2590        /// </summary>
 2591        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2592        /// <returns>true if the item has change, else false.</returns>
 2593        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2594        {
 362595            _sortName = null;
 2596
 362597            var hasChanges = false;
 2598
 362599            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2600            {
 02601                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02602                hasChanges = true;
 2603            }
 2604
 362605            return hasChanges;
 2606        }
 2607
 2608        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2609        {
 02610            if (protocol == MediaProtocol.File)
 2611            {
 02612                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2613            }
 2614
 02615            return path;
 2616        }
 2617
 2618        public virtual void FillUserDataDtoValues(
 2619            UserItemDataDto dto,
 2620            UserItemData userData,
 2621            BaseItemDto itemDto,
 2622            User user,
 2623            DtoOptions fields,
 2624            (int Played, int Total)? precomputedCounts = null)
 2625        {
 02626            if (RunTimeTicks.HasValue)
 2627            {
 02628                double pct = RunTimeTicks.Value;
 2629
 02630                if (pct > 0)
 2631                {
 02632                    pct = userData.PlaybackPositionTicks / pct;
 2633
 02634                    if (pct > 0)
 2635                    {
 02636                        dto.PlayedPercentage = 100 * pct;
 2637                    }
 2638                }
 2639            }
 02640        }
 2641
 2642        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2643        {
 02644            var newOptions = new MetadataRefreshOptions(options)
 02645            {
 02646                SearchResult = null
 02647            };
 2648
 02649            var item = this;
 2650
 02651            if (copyTitleMetadata)
 2652            {
 2653                // Take some data from the main item, for querying purposes
 02654                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2655                {
 02656                    newOptions.ForceSave = true;
 02657                    ownedItem.Genres = item.Genres;
 2658                }
 2659
 02660                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2661                {
 02662                    newOptions.ForceSave = true;
 02663                    ownedItem.Studios = item.Studios;
 2664                }
 2665
 02666                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2667                {
 02668                    newOptions.ForceSave = true;
 02669                    ownedItem.ProductionLocations = item.ProductionLocations;
 2670                }
 2671
 02672                if (item.CommunityRating != ownedItem.CommunityRating)
 2673                {
 02674                    ownedItem.CommunityRating = item.CommunityRating;
 02675                    newOptions.ForceSave = true;
 2676                }
 2677
 02678                if (item.CriticRating != ownedItem.CriticRating)
 2679                {
 02680                    ownedItem.CriticRating = item.CriticRating;
 02681                    newOptions.ForceSave = true;
 2682                }
 2683
 02684                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2685                {
 02686                    ownedItem.Overview = item.Overview;
 02687                    newOptions.ForceSave = true;
 2688                }
 2689
 02690                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2691                {
 02692                    ownedItem.OfficialRating = item.OfficialRating;
 02693                    newOptions.ForceSave = true;
 2694                }
 2695
 02696                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2697                {
 02698                    ownedItem.CustomRating = item.CustomRating;
 02699                    newOptions.ForceSave = true;
 2700                }
 2701            }
 2702
 02703            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02704        }
 2705
 2706        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2707        {
 02708            var newOptions = new MetadataRefreshOptions(options)
 02709            {
 02710                SearchResult = null
 02711            };
 2712
 02713            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2714
 2715            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02716            if (LibraryManager.GetItemById(id) is not Video video)
 2717            {
 02718                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2719
 02720                newOptions.ForceSave = true;
 2721            }
 2722
 02723            if (video is null)
 2724            {
 02725                return;
 2726            }
 2727
 02728            if (video.OwnerId.IsEmpty())
 2729            {
 02730                video.OwnerId = Id;
 2731            }
 2732
 02733            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02734        }
 2735
 2736        public string GetEtag(User user)
 2737        {
 62738            var list = GetEtagValues(user);
 2739
 62740            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2741        }
 2742
 2743        protected virtual List<string> GetEtagValues(User user)
 2744        {
 62745            return
 62746            [
 62747                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62748            ];
 2749        }
 2750
 2751        public virtual IEnumerable<Guid> GetAncestorIds()
 2752        {
 1172753            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2754        }
 2755
 2756        public BaseItem GetTopParent()
 2757        {
 2012758            if (IsTopParent)
 2759            {
 222760                return this;
 2761            }
 2762
 1792763            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2764        }
 2765
 2766        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2767        {
 452768            return [Id];
 2769        }
 2770
 2771        public virtual double? GetRefreshProgress()
 2772        {
 02773            return null;
 2774        }
 2775
 2776        public virtual ItemUpdateType OnMetadataChanged()
 2777        {
 1162778            var updateType = ItemUpdateType.None;
 2779
 1162780            var item = this;
 2781
 1162782            var rating = item.GetParentalRatingScore();
 1162783            if (rating is not null)
 2784            {
 02785                if (rating.Score != item.InheritedParentalRatingValue)
 2786                {
 02787                    item.InheritedParentalRatingValue = rating.Score;
 02788                    updateType |= ItemUpdateType.MetadataImport;
 2789                }
 2790
 02791                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2792                {
 02793                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02794                    updateType |= ItemUpdateType.MetadataImport;
 2795                }
 2796            }
 2797            else
 2798            {
 1162799                if (item.InheritedParentalRatingValue is not null)
 2800                {
 02801                    item.InheritedParentalRatingValue = null;
 02802                    item.InheritedParentalRatingSubValue = null;
 02803                    updateType |= ItemUpdateType.MetadataImport;
 2804                }
 2805            }
 2806
 1162807            return updateType;
 2808        }
 2809
 2810        /// <summary>
 2811        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2812        /// </summary>
 2813        /// <param name="children">Media children.</param>
 2814        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2815        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2816        {
 02817            var currentOfficialRating = OfficialRating;
 2818
 2819            // Gather all possible ratings
 02820            var ratings = children
 02821                .Select(i => i.OfficialRating)
 02822                .Where(i => !string.IsNullOrEmpty(i))
 02823                .Distinct(StringComparer.OrdinalIgnoreCase)
 02824                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 02825                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02826                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02827                .Select(i => i.rating);
 2828
 02829            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2830
 02831            return !string.Equals(
 02832                currentOfficialRating ?? string.Empty,
 02833                OfficialRating ?? string.Empty,
 02834                StringComparison.OrdinalIgnoreCase);
 2835        }
 2836
 2837        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2838        {
 02839            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2840        }
 2841
 2842        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2843        {
 02844            return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), us
 2845        }
 2846
 2847        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2848        {
 02849            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2850        }
 2851
 2852        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2853        {
 02854            return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), u
 2855        }
 2856
 2857        /// <summary>
 2858        /// Gets the ids of the items whose owned extras belong to this item.
 2859        /// </summary>
 2860        /// <returns>An array containing the owner ids.</returns>
 2861        protected virtual Guid[] GetExtraOwnerIds()
 2862        {
 62863            return [Id];
 2864        }
 2865
 2866        /// <summary>
 2867        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2868        /// </summary>
 2869        /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</
 2870        /// <returns>An enumerable containing the items.</returns>
 2871        public IEnumerable<BaseItem> GetExtras(User user = null)
 2872        {
 62873            return LibraryManager.GetItemList(new InternalItemsQuery(user)
 62874            {
 62875                OwnerIds = GetExtraOwnerIds(),
 62876                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 62877            });
 2878        }
 2879
 2880        /// <summary>
 2881        /// Get all extras with specific types that are associated with this item.
 2882        /// </summary>
 2883        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2884        /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</
 2885        /// <returns>An enumerable containing the extras.</returns>
 2886        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes, User user = null)
 2887        {
 02888            return LibraryManager.GetItemList(new InternalItemsQuery(user)
 02889            {
 02890                OwnerIds = GetExtraOwnerIds(),
 02891                ExtraTypes = extraTypes.ToArray(),
 02892                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 02893            });
 2894        }
 2895
 2896        public virtual long GetRunTimeTicksForPlayState()
 2897        {
 02898            return RunTimeTicks ?? 0;
 2899        }
 2900
 2901        /// <inheritdoc />
 2902        public override bool Equals(object obj)
 2903        {
 982904            return obj is BaseItem baseItem && this.Equals(baseItem);
 2905        }
 2906
 2907        /// <inheritdoc />
 2572908        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2909
 2910        /// <inheritdoc />
 1022911        public override int GetHashCode() => HashCode.Combine(Id);
 2912    }
 2913}

Methods/Properties

.cctor()
.ctor()
get_SupportsAddingToPlaylist()
get_AlwaysScanInternalMetadataPath()
get_SupportsPlayedStatus()
get_SupportsPositionTicksResume()
get_SupportsRemoteImageDownloading()
get_Name()
set_Name(System.String)
get_IsUnaired()
get_IsThemeMedia()
get_OriginalLanguage()
set_OriginalLanguage(System.String)
get_DisplayPreferencesId()
get_SourceType()
get_ContainingFolderPath()
get_IsHidden()
get_LocationType()
get_PathProtocol()
get_IsFileProtocol()
get_HasPathProtocol()
get_SupportsLocalMetadata()
get_FileNameWithoutExtension()
get_EnableAlphaNumericSorting()
get_IsHD()
get_PrimaryImagePath()
get_MediaType()
get_PhysicalLocations()
get_EnableMediaSourceDisplay()
get_ForcedSortName()
set_ForcedSortName(System.String)
get_SortName()
set_SortName(System.String)
get_DisplayParentId()
get_DisplayParent()
get_HasLocalAlternateVersions()
get_OfficialRatingForComparison()
get_CustomRatingForComparison()
get_LatestItemsIndexContainer()
get_EnableRememberingTrackSelections()
get_IsTopParent()
get_SupportsAncestors()
get_SupportsOwnedItems()
get_SupportsPeople()
get_SupportsThemeMedia()
get_SupportsInheritedParentImages()
get_IsFolder()
get_IsDisplayedAsFolder()
GetCustomRatingForComparision(System.Collections.Generic.HashSet`1<System.Guid>)
GetDefaultPrimaryImageAspectRatio()
CreatePresentationUniqueKey()
CanDelete()
IsAuthorizedToDelete(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>)
GetOwner()
CanDelete(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.Folder>)
CanDelete(Jellyfin.Database.Implementations.Entities.User)
CanDownload()
IsAuthorizedToDownload(Jellyfin.Database.Implementations.Entities.User)
CanDownload(Jellyfin.Database.Implementations.Entities.User)
ToString()
GetInternalMetadataPath()
GetInternalMetadataPath(System.String)
CreateSortName()
ModifySortChunks(System.ReadOnlySpan`1<System.Char>)
GetParent()
GetParents()
FindParent()
GetPlayAccess(Jellyfin.Database.Implementations.Entities.User)
GetMediaStreams()
IsActiveRecording()
GetMediaSources(System.Boolean)
GetAllItemsForMediaSources()
GetVersionInfo(System.Boolean,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceType,System.String)
GetMediaSourceName(MediaBrowser.Controller.Entities.BaseItem,System.String)
GetCommonNamePrefix(System.Collections.Generic.IReadOnlyList`1<System.ValueTuple`2<MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.MediaSourceType>>)
GetCommonVersionPrefix(System.Collections.Generic.IReadOnlyList`1<System.String>)
RefreshMetadata(System.Threading.CancellationToken)
RefreshMetadata()
IsVisibleStandaloneInternal(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
SetParent(MediaBrowser.Controller.Entities.Folder)
RefreshedOwnedItems()
GetFileSystemChildren(MediaBrowser.Controller.Providers.IDirectoryService)
RefreshExtras()
GetPresentationUniqueKey()
RequiresRefresh()
GetUserDataKeys()
UpdateFromResolvedItem(MediaBrowser.Controller.Entities.BaseItem)
AfterMetadataRefresh()
GetPreferredMetadataLanguage()
GetPreferredMetadataCountryCode()
GetInheritedOriginalLanguage()
IsSaveLocalMetadataEnabled()
IsParentalAllowed(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
GetParentalRatingScore()
GetInheritedTags()
IsVisibleViaTags(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
GetBlockUnratedType()
GetBlockUnratedValue(Jellyfin.Database.Implementations.Entities.User)
IsVisible(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
IsVisibleStandalone(Jellyfin.Database.Implementations.Entities.User)
GetClientTypeName()
GetBaseItemKind()
GetLinkedChild(MediaBrowser.Controller.Entities.LinkedChild)
FindLinkedChild(MediaBrowser.Controller.Entities.LinkedChild)
AddStudio(System.String)
SetStudios(System.Collections.Generic.IEnumerable`1<System.String>)
AddGenre(System.String)
MarkPlayed(Jellyfin.Database.Implementations.Entities.User,System.Nullable`1<System.DateTime>,System.Boolean)
MarkUnplayed(Jellyfin.Database.Implementations.Entities.User)
ResetPlayedState(MediaBrowser.Controller.Entities.UserItemData)
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>>)
GetExtraOwnerIds()
GetExtras(Jellyfin.Database.Implementations.Entities.User)
GetExtras(System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Model.Entities.ExtraType>,Jellyfin.Database.Implementations.Entities.User)
GetRunTimeTicksForPlayState()
Equals(System.Object)
Equals(MediaBrowser.Controller.Entities.BaseItem)
GetHashCode()