< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.BaseItem
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/BaseItem.cs
Line coverage
56%
Covered lines: 547
Uncovered lines: 414
Coverable lines: 961
Total lines: 3020
Line coverage: 56.9%
Branch coverage
43%
Covered branches: 270
Total branches: 624
Branch coverage: 43.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 4/30/2026 - 12:14:58 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: 29137/26/2026 - 12:17:51 AM Line coverage: 55% (519/943) Branch coverage: 42% (258/614) Total lines: 29708/3/2026 - 12:16:46 AM Line coverage: 54.5% (519/952) Branch coverage: 41.7% (258/618) Total lines: 30028/6/2026 - 12:17:15 AM Line coverage: 56.9% (547/961) Branch coverage: 43.2% (270/624) Total lines: 3020 4/30/2026 - 12:14:58 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: 29137/26/2026 - 12:17:51 AM Line coverage: 55% (519/943) Branch coverage: 42% (258/614) Total lines: 29708/3/2026 - 12:16:46 AM Line coverage: 54.5% (519/952) Branch coverage: 41.7% (258/618) Total lines: 30028/6/2026 - 12:17:15 AM Line coverage: 56.9% (547/961) Branch coverage: 43.2% (270/624) Total lines: 3020

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()100%44100%
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_SearchesContainingFolderForExtras()50%1414100%
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()100%11100%
GetSortName(...)85.71%141487.5%
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()87.5%8887.5%
GetFileSystemChildren(...)100%11100%
RefreshExtras()36.36%1452236.73%
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%11100%
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%
InheritDatesFromOwner(...)91.66%1212100%
RefreshMetadataForOwnedItem()0%420200%
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%
GetOwnedVersionIds()100%210%
GetOwnerIdForExtra(...)100%210%
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.Configuration;
 31using MediaBrowser.Model.Dto;
 32using MediaBrowser.Model.Entities;
 33using MediaBrowser.Model.Globalization;
 34using MediaBrowser.Model.IO;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.MediaInfo;
 38using Microsoft.Extensions.Logging;
 39
 40namespace MediaBrowser.Controller.Entities
 41{
 42    /// <summary>
 43    /// Class BaseItem.
 44    /// </summary>
 45    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 46    {
 47        private BaseItemKind? _baseItemKind;
 48
 49        public const string ThemeSongFileName = "theme";
 50
 51        /// <summary>
 52        /// The supported image extensions.
 53        /// </summary>
 554        public static readonly string[] SupportedImageExtensions
 555            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 56
 557        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 558        {
 559            ".nfo",
 560            ".xml",
 561            ".srt",
 562            ".vtt",
 563            ".sub",
 564            ".sup",
 565            ".idx",
 566            ".txt",
 567            ".edl",
 568            ".bif",
 569            ".smi",
 570            ".ttml",
 571            ".lrc",
 572            ".elrc"
 573        };
 74
 75        /// <summary>
 76        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 77        /// </summary>
 578        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 579        {
 580            Model.Entities.ExtraType.Unknown,
 581            Model.Entities.ExtraType.BehindTheScenes,
 582            Model.Entities.ExtraType.Clip,
 583            Model.Entities.ExtraType.DeletedScene,
 584            Model.Entities.ExtraType.Interview,
 585            Model.Entities.ExtraType.Sample,
 586            Model.Entities.ExtraType.Scene,
 587            Model.Entities.ExtraType.Featurette,
 588            Model.Entities.ExtraType.Short
 589        };
 90
 591        private protected static readonly char[] VersionDelimiters = ['-', '_', '.'];
 92
 93        private string _sortName;
 94
 95        private string _forcedSortName;
 96
 97        private string _name;
 98
 99        private string _originalLanguage;
 100
 101        public const char SlugChar = '-';
 102
 103        protected BaseItem()
 104        {
 1324105            Tags = Array.Empty<string>();
 1324106            Genres = Array.Empty<string>();
 1324107            Studios = Array.Empty<string>();
 1324108            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 1324109            LockedFields = Array.Empty<MetadataField>();
 1324110            ImageInfos = Array.Empty<ItemImageInfo>();
 1324111            ProductionLocations = Array.Empty<string>();
 1324112            RemoteTrailers = Array.Empty<MediaUrl>();
 1324113            UserData = [];
 1324114        }
 115
 116        /// <summary>
 117        /// Gets or Sets the user data collection as cached from the last Db query.
 118        /// </summary>
 119        [JsonIgnore]
 120        public ICollection<UserData> UserData { get; set; }
 121
 122        [JsonIgnore]
 123        public string PreferredMetadataCountryCode { get; set; }
 124
 125        [JsonIgnore]
 126        public string PreferredMetadataLanguage { get; set; }
 127
 128        public long? Size { get; set; }
 129
 130        public string Container { get; set; }
 131
 132        [JsonIgnore]
 133        public string Tagline { get; set; }
 134
 135        [JsonIgnore]
 136        public virtual ItemImageInfo[] ImageInfos { get; set; }
 137
 138        [JsonIgnore]
 139        public bool IsVirtualItem { get; set; }
 140
 141        /// <summary>
 142        /// Gets or sets the album.
 143        /// </summary>
 144        /// <value>The album.</value>
 145        [JsonIgnore]
 146        public string Album { get; set; }
 147
 148        /// <summary>
 149        /// Gets or sets the LUFS value.
 150        /// </summary>
 151        /// <value>The LUFS Value.</value>
 152        [JsonIgnore]
 153        public float? LUFS { get; set; }
 154
 155        /// <summary>
 156        /// Gets or sets the gain required for audio normalization.
 157        /// </summary>
 158        /// <value>The gain required for audio normalization.</value>
 159        [JsonIgnore]
 160        public float? NormalizationGain { get; set; }
 161
 162        /// <summary>
 163        /// Gets or sets the channel identifier.
 164        /// </summary>
 165        /// <value>The channel identifier.</value>
 166        [JsonIgnore]
 167        public Guid ChannelId { get; set; }
 168
 169        [JsonIgnore]
 0170        public virtual bool SupportsAddingToPlaylist => false;
 171
 172        [JsonIgnore]
 17173        public virtual bool AlwaysScanInternalMetadataPath => false;
 174
 175        /// <summary>
 176        /// Gets or sets a value indicating whether this instance is in mixed folder.
 177        /// </summary>
 178        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 179        [JsonIgnore]
 180        public bool IsInMixedFolder { get; set; }
 181
 182        [JsonIgnore]
 0183        public virtual bool SupportsPlayedStatus => false;
 184
 185        [JsonIgnore]
 0186        public virtual bool SupportsPositionTicksResume => false;
 187
 188        [JsonIgnore]
 17189        public virtual bool SupportsRemoteImageDownloading => true;
 190
 191        /// <summary>
 192        /// Gets or sets the name.
 193        /// </summary>
 194        /// <value>The name.</value>
 195        [JsonIgnore]
 196        public virtual string Name
 197        {
 1030198            get => _name;
 199            set
 200            {
 502201                _name = value;
 202
 203                // lazy load this again
 502204                _sortName = null;
 502205            }
 206        }
 207
 208        [JsonIgnore]
 0209        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 210
 211        [JsonIgnore]
 212        public int? TotalBitrate { get; set; }
 213
 214        [JsonIgnore]
 215        public ExtraType? ExtraType { get; set; }
 216
 217        [JsonIgnore]
 0218        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 219
 220        [JsonIgnore]
 221        public string OriginalTitle { get; set; }
 222
 223        [JsonIgnore]
 224        public string OriginalLanguage
 225        {
 422226            get => _originalLanguage;
 234227            set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
 228        }
 229
 230        /// <summary>
 231        /// Gets or sets the id.
 232        /// </summary>
 233        /// <value>The id.</value>
 234        [JsonIgnore]
 235        public Guid Id { get; set; }
 236
 237        [JsonIgnore]
 238        public Guid OwnerId { get; set; }
 239
 240        /// <summary>
 241        /// Gets or sets the audio.
 242        /// </summary>
 243        /// <value>The audio.</value>
 244        [JsonIgnore]
 245        public ProgramAudio? Audio { get; set; }
 246
 247        /// <summary>
 248        /// Gets the id that should be used to key display prefs for this item.
 249        /// Default is based on the type for everything except actual generic folders.
 250        /// </summary>
 251        /// <value>The display prefs id.</value>
 252        [JsonIgnore]
 253        public virtual Guid DisplayPreferencesId
 254        {
 255            get
 256            {
 6257                var thisType = GetType();
 6258                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 259            }
 260        }
 261
 262        /// <summary>
 263        /// Gets or sets the path.
 264        /// </summary>
 265        /// <value>The path.</value>
 266        [JsonIgnore]
 267        public virtual string Path { get; set; }
 268
 269        [JsonIgnore]
 270        public virtual SourceType SourceType
 271        {
 272            get
 273            {
 2389274                if (!ChannelId.IsEmpty())
 275                {
 0276                    return SourceType.Channel;
 277                }
 278
 2389279                return SourceType.Library;
 280            }
 281        }
 282
 283        /// <summary>
 284        /// Gets the folder containing the item.
 285        /// If the item is a folder, it returns the folder itself.
 286        /// </summary>
 287        [JsonIgnore]
 288        public virtual string ContainingFolderPath
 289        {
 290            get
 291            {
 490292                if (IsFolder)
 293                {
 417294                    return Path;
 295                }
 296
 73297                return System.IO.Path.GetDirectoryName(Path);
 298            }
 299        }
 300
 301        /// <summary>
 302        /// Gets or sets the name of the service.
 303        /// </summary>
 304        /// <value>The name of the service.</value>
 305        [JsonIgnore]
 306        public string ServiceName { get; set; }
 307
 308        /// <summary>
 309        /// Gets or sets the external id.
 310        /// </summary>
 311        /// <remarks>
 312        /// If this content came from an external service, the id of the content on that service.
 313        /// </remarks>
 314        [JsonIgnore]
 315        public string ExternalId { get; set; }
 316
 317        [JsonIgnore]
 318        public string ExternalSeriesId { get; set; }
 319
 320        [JsonIgnore]
 0321        public virtual bool IsHidden => false;
 322
 323        /// <summary>
 324        /// Gets the type of the location.
 325        /// </summary>
 326        /// <value>The type of the location.</value>
 327        [JsonIgnore]
 328        public virtual LocationType LocationType
 329        {
 330            get
 331            {
 16332                var path = Path;
 16333                if (string.IsNullOrEmpty(path))
 334                {
 7335                    if (SourceType == SourceType.Channel)
 336                    {
 0337                        return LocationType.Remote;
 338                    }
 339
 7340                    return LocationType.Virtual;
 341                }
 342
 9343                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 344            }
 345        }
 346
 347        [JsonIgnore]
 348        public MediaProtocol? PathProtocol
 349        {
 350            get
 351            {
 2666352                var path = Path;
 353
 2666354                if (string.IsNullOrEmpty(path))
 355                {
 116356                    return null;
 357                }
 358
 2550359                return MediaSourceManager.GetPathProtocol(path);
 360            }
 361        }
 362
 363        [JsonIgnore]
 2627364        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 365
 366        [JsonIgnore]
 0367        public bool HasPathProtocol => PathProtocol.HasValue;
 368
 369        [JsonIgnore]
 370        public virtual bool SupportsLocalMetadata
 371        {
 372            get
 373            {
 1364374                if (SourceType == SourceType.Channel)
 375                {
 0376                    return false;
 377                }
 378
 1364379                return IsFileProtocol;
 380            }
 381        }
 382
 383        [JsonIgnore]
 384        public virtual string FileNameWithoutExtension
 385        {
 386            get
 387            {
 0388                if (IsFileProtocol)
 389                {
 0390                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 391                }
 392
 0393                return null;
 394            }
 395        }
 396
 397        [JsonIgnore]
 106398        public virtual bool EnableAlphaNumericSorting => true;
 399
 144400        public virtual bool IsHD => Height >= 720;
 401
 402        public bool IsShortcut { get; set; }
 403
 404        public string ShortcutPath { get; set; }
 405
 406        public int Width { get; set; }
 407
 408        public int Height { get; set; }
 409
 410        /// <summary>
 411        /// Gets the primary image path.
 412        /// </summary>
 413        /// <remarks>
 414        /// This is just a helper for convenience.
 415        /// </remarks>
 416        /// <value>The primary image path.</value>
 417        [JsonIgnore]
 0418        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 419
 420        /// <summary>
 421        /// Gets or sets the date created.
 422        /// </summary>
 423        /// <value>The date created.</value>
 424        [JsonIgnore]
 425        public DateTime DateCreated { get; set; }
 426
 427        /// <summary>
 428        /// Gets or sets the date modified.
 429        /// </summary>
 430        /// <value>The date modified.</value>
 431        [JsonIgnore]
 432        public DateTime DateModified { get; set; }
 433
 434        public DateTime DateLastSaved { get; set; }
 435
 436        [JsonIgnore]
 437        public DateTime DateLastRefreshed { get; set; }
 438
 439        [JsonIgnore]
 440        public bool IsLocked { get; set; }
 441
 442        /// <summary>
 443        /// Gets or sets the locked fields.
 444        /// </summary>
 445        /// <value>The locked fields.</value>
 446        [JsonIgnore]
 447        public MetadataField[] LockedFields { get; set; }
 448
 449        /// <summary>
 450        /// Gets the type of the media.
 451        /// </summary>
 452        /// <value>The type of the media.</value>
 453        [JsonIgnore]
 139454        public virtual MediaType MediaType => MediaType.Unknown;
 455
 456        [JsonIgnore]
 457        public virtual string[] PhysicalLocations
 458        {
 459            get
 460            {
 0461                if (!IsFileProtocol)
 462                {
 0463                    return Array.Empty<string>();
 464                }
 465
 0466                return [Path];
 467            }
 468        }
 469
 470        [JsonIgnore]
 471        public bool EnableMediaSourceDisplay
 472        {
 473            get
 474            {
 6475                if (SourceType == SourceType.Channel)
 476                {
 0477                    return ChannelManager.EnableMediaSourceDisplay(this);
 478                }
 479
 6480                return true;
 481            }
 482        }
 483
 484        [JsonIgnore]
 485        public Guid ParentId { get; set; }
 486
 487        /// <summary>
 488        /// Gets or sets the logger.
 489        /// </summary>
 490        public static ILogger<BaseItem> Logger { get; set; }
 491
 492        public static ILibraryManager LibraryManager { get; set; }
 493
 494        public static IServerConfigurationManager ConfigurationManager { get; set; }
 495
 496        public static IProviderManager ProviderManager { get; set; }
 497
 498        public static ILocalizationManager LocalizationManager { get; set; }
 499
 500        public static IItemRepository ItemRepository { get; set; }
 501
 502        public static IItemCountService ItemCountService { get; set; }
 503
 504        public static IChapterManager ChapterManager { get; set; }
 505
 506        public static IFileSystem FileSystem { get; set; }
 507
 508        public static IUserDataManager UserDataManager { get; set; }
 509
 510        public static IChannelManager ChannelManager { get; set; }
 511
 512        public static IMediaSourceManager MediaSourceManager { get; set; }
 513
 514        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 515
 516        /// <summary>
 517        /// Gets or sets the name of the forced sort.
 518        /// </summary>
 519        /// <value>The name of the forced sort.</value>
 520        [JsonIgnore]
 521        public string ForcedSortName
 522        {
 500523            get => _forcedSortName;
 524            set
 525            {
 99526                _forcedSortName = value;
 99527                _sortName = null;
 99528            }
 529        }
 530
 531        /// <summary>
 532        /// Gets or sets the name of the sort.
 533        /// </summary>
 534        /// <value>The name of the sort.</value>
 535        [JsonIgnore]
 536        public string SortName
 537        {
 538            get
 539            {
 180540                if (_sortName is null)
 541                {
 106542                    if (!string.IsNullOrEmpty(ForcedSortName))
 543                    {
 544                        // Run the forced sort name through the same cleaning as auto-generated sort names.
 1545                        _sortName = GetSortName(ForcedSortName, EnableAlphaNumericSorting, ConfigurationManager.Configur
 546                    }
 547                    else
 548                    {
 105549                        _sortName = CreateSortName();
 550                    }
 551                }
 552
 180553                return _sortName;
 554            }
 555
 135556            set => _sortName = value;
 557        }
 558
 559        [JsonIgnore]
 358560        public virtual Guid DisplayParentId => ParentId;
 561
 562        [JsonIgnore]
 563        public BaseItem DisplayParent
 564        {
 565            get
 566            {
 358567                var id = DisplayParentId;
 358568                if (id.IsEmpty())
 569                {
 264570                    return null;
 571                }
 572
 94573                return LibraryManager.GetItemById(id);
 574            }
 575        }
 576
 577        /// <summary>
 578        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 579        /// </summary>
 580        /// <value>The premiere date.</value>
 581        [JsonIgnore]
 582        public DateTime? PremiereDate { get; set; }
 583
 584        /// <summary>
 585        /// Gets or sets the end date.
 586        /// </summary>
 587        /// <value>The end date.</value>
 588        [JsonIgnore]
 589        public DateTime? EndDate { get; set; }
 590
 591        /// <summary>
 592        /// Gets or sets the official rating.
 593        /// </summary>
 594        /// <value>The official rating.</value>
 595        [JsonIgnore]
 596        public string OfficialRating { get; set; }
 597
 598        [JsonIgnore]
 599        public int? InheritedParentalRatingValue { get; set; }
 600
 601        [JsonIgnore]
 602        public int? InheritedParentalRatingSubValue { get; set; }
 603
 604        /// <summary>
 605        /// Gets or sets the critic rating.
 606        /// </summary>
 607        /// <value>The critic rating.</value>
 608        [JsonIgnore]
 609        public float? CriticRating { get; set; }
 610
 611        /// <summary>
 612        /// Gets or sets the custom rating.
 613        /// </summary>
 614        /// <value>The custom rating.</value>
 615        [JsonIgnore]
 616        public string CustomRating { get; set; }
 617
 618        /// <summary>
 619        /// Gets or sets the overview.
 620        /// </summary>
 621        /// <value>The overview.</value>
 622        [JsonIgnore]
 623        public string Overview { get; set; }
 624
 625        /// <summary>
 626        /// Gets or sets the studios.
 627        /// </summary>
 628        /// <value>The studios.</value>
 629        [JsonIgnore]
 630        public string[] Studios { get; set; }
 631
 632        /// <summary>
 633        /// Gets or sets the genres.
 634        /// </summary>
 635        /// <value>The genres.</value>
 636        [JsonIgnore]
 637        public string[] Genres { get; set; }
 638
 639        /// <summary>
 640        /// Gets or sets the tags.
 641        /// </summary>
 642        /// <value>The tags.</value>
 643        [JsonIgnore]
 644        public string[] Tags { get; set; }
 645
 646        [JsonIgnore]
 647        public string[] ProductionLocations { get; set; }
 648
 649        /// <summary>
 650        /// Gets or sets the home page URL.
 651        /// </summary>
 652        /// <value>The home page URL.</value>
 653        [JsonIgnore]
 654        public string HomePageUrl { get; set; }
 655
 656        /// <summary>
 657        /// Gets or sets the community rating.
 658        /// </summary>
 659        /// <value>The community rating.</value>
 660        [JsonIgnore]
 661        public float? CommunityRating { get; set; }
 662
 663        /// <summary>
 664        /// Gets or sets the run time ticks.
 665        /// </summary>
 666        /// <value>The run time ticks.</value>
 667        [JsonIgnore]
 668        public long? RunTimeTicks { get; set; }
 669
 670        /// <summary>
 671        /// Gets or sets the production year.
 672        /// </summary>
 673        /// <value>The production year.</value>
 674        [JsonIgnore]
 675        public int? ProductionYear { get; set; }
 676
 677        /// <summary>
 678        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 679        /// This could be episode number, album track number, etc.
 680        /// </summary>
 681        /// <value>The index number.</value>
 682        [JsonIgnore]
 683        public int? IndexNumber { get; set; }
 684
 685        /// <summary>
 686        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 687        /// </summary>
 688        /// <value>The parent index number.</value>
 689        [JsonIgnore]
 690        public int? ParentIndexNumber { get; set; }
 691
 692        [JsonIgnore]
 0693        public virtual bool HasLocalAlternateVersions => false;
 694
 695        [JsonIgnore]
 696        public string OfficialRatingForComparison
 697        {
 698            get
 699            {
 176700                var officialRating = OfficialRating;
 176701                if (!string.IsNullOrEmpty(officialRating))
 702                {
 0703                    return officialRating;
 704                }
 705
 176706                var parent = DisplayParent;
 176707                if (parent is not null)
 708                {
 44709                    return parent.OfficialRatingForComparison;
 710                }
 711
 132712                return null;
 713            }
 714        }
 715
 716        [JsonIgnore]
 717        public string CustomRatingForComparison
 718        {
 719            get
 720            {
 132721                return GetCustomRatingForComparision();
 722            }
 723        }
 724
 725        /// <summary>
 726        /// Gets or sets the provider ids.
 727        /// </summary>
 728        /// <value>The provider ids.</value>
 729        [JsonIgnore]
 730        public Dictionary<string, string> ProviderIds { get; set; }
 731
 732        [JsonIgnore]
 0733        public virtual Folder LatestItemsIndexContainer => null;
 734
 735        [JsonIgnore]
 736        public string PresentationUniqueKey { get; set; }
 737
 738        [JsonIgnore]
 31739        public virtual bool EnableRememberingTrackSelections => true;
 740
 741        [JsonIgnore]
 742        public virtual bool IsTopParent
 743        {
 744            get
 745            {
 358746                if (this is BasePluginFolder || this is Channel)
 747                {
 61748                    return true;
 749                }
 750
 297751                if (this is IHasCollectionType view)
 752                {
 13753                    if (view.CollectionType == CollectionType.livetv)
 754                    {
 0755                        return true;
 756                    }
 757                }
 758
 297759                if (GetParent() is AggregateFolder)
 760                {
 0761                    return true;
 762                }
 763
 297764                return false;
 765            }
 766        }
 767
 768        [JsonIgnore]
 464769        public virtual bool SupportsAncestors => true;
 770
 771        [JsonIgnore]
 101772        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 773
 774        /// <summary>
 775        /// Gets a value indicating whether this item searches the folder it lives in for its own extras.
 776        /// </summary>
 777        [JsonIgnore]
 778        protected virtual bool SearchesContainingFolderForExtras =>
 42779            IsFileProtocol
 42780            && SupportsOwnedItems
 42781            && !IsInMixedFolder
 42782            && this is not (ICollectionFolder or UserRootFolder or AggregateFolder)
 42783            && GetType() != typeof(Folder);
 784
 785        [JsonIgnore]
 72786        public virtual bool SupportsPeople => false;
 787
 788        [JsonIgnore]
 0789        public virtual bool SupportsThemeMedia => false;
 790
 791        [JsonIgnore]
 0792        public virtual bool SupportsInheritedParentImages => false;
 793
 794        /// <summary>
 795        /// Gets a value indicating whether this instance is folder.
 796        /// </summary>
 797        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 798        [JsonIgnore]
 106799        public virtual bool IsFolder => false;
 800
 801        [JsonIgnore]
 0802        public virtual bool IsDisplayedAsFolder => false;
 803
 804        /// <summary>
 805        /// Gets or sets the remote trailers.
 806        /// </summary>
 807        /// <value>The remote trailers.</value>
 808        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 809
 810        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 811        {
 176812            callstack ??= new();
 176813            var customRating = CustomRating;
 176814            if (!string.IsNullOrEmpty(customRating))
 815            {
 0816                return customRating;
 817            }
 818
 176819            callstack.Add(Id);
 820
 176821            var parent = DisplayParent;
 176822            if (parent is not null && !callstack.Contains(parent.Id))
 823            {
 44824                return parent.GetCustomRatingForComparision(callstack);
 825            }
 826
 132827            return null;
 828        }
 829
 830        public virtual double GetDefaultPrimaryImageAspectRatio()
 831        {
 0832            return 0;
 833        }
 834
 835        public virtual string CreatePresentationUniqueKey()
 836        {
 61837            return Id.ToString("N", CultureInfo.InvariantCulture);
 838        }
 839
 840        public virtual bool CanDelete()
 841        {
 0842            if (SourceType == SourceType.Channel)
 843            {
 0844                return ChannelManager.CanDelete(this);
 845            }
 846
 0847            return IsFileProtocol;
 848        }
 849
 850        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 851        {
 0852            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 853            {
 0854                return true;
 855            }
 856
 0857            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 858
 0859            if (SourceType == SourceType.Channel)
 860            {
 0861                return allowed.Contains(ChannelId);
 862            }
 863
 0864            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 865
 0866            foreach (var folder in collectionFolders)
 867            {
 0868                if (allowed.Contains(folder.Id))
 869                {
 0870                    return true;
 871                }
 872            }
 873
 0874            return false;
 0875        }
 876
 877        public BaseItem GetOwner()
 878        {
 665879            var ownerId = OwnerId;
 665880            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 881        }
 882
 883        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 884        {
 6885            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 886        }
 887
 888        public virtual bool CanDelete(User user)
 889        {
 6890            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 891
 6892            return CanDelete(user, allCollectionFolders);
 893        }
 894
 895        public virtual bool CanDownload()
 896        {
 6897            return false;
 898        }
 899
 900        public virtual bool IsAuthorizedToDownload(User user)
 901        {
 0902            return user.HasPermission(PermissionKind.EnableContentDownloading);
 903        }
 904
 905        public bool CanDownload(User user)
 906        {
 6907            return CanDownload() && IsAuthorizedToDownload(user);
 908        }
 909
 910        /// <inheritdoc />
 911        public override string ToString()
 912        {
 69913            return Name;
 914        }
 915
 916        public virtual string GetInternalMetadataPath()
 917        {
 72918            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 919
 72920            return GetInternalMetadataPath(basePath);
 921        }
 922
 923        protected virtual string GetInternalMetadataPath(string basePath)
 924        {
 72925            if (SourceType == SourceType.Channel)
 926            {
 0927                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 928            }
 929
 72930            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 931
 72932            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 933        }
 934
 935        /// <summary>
 936        /// Creates the name of the sort.
 937        /// </summary>
 938        /// <returns>System.String.</returns>
 939        protected virtual string CreateSortName()
 940        {
 105941            return GetSortName(Name, EnableAlphaNumericSorting, ConfigurationManager.Configuration);
 942        }
 943
 944        /// <summary>
 945        /// Cleans a raw name into its sortable form by applying the configured sort rules.
 946        /// </summary>
 947        /// <param name="name">The raw name to clean.</param>
 948        /// <param name="enableAlphaNumericSorting">Whether alphanumeric sorting rules should be applied.</param>
 949        /// <param name="configuration">The server configuration providing the sort rules.</param>
 950        /// <returns>The cleaned, sortable name, or <c>null</c> if <paramref name="name"/> is <c>null</c>.</returns>
 951        public static string GetSortName(string name, bool enableAlphaNumericSorting, ServerConfiguration configuration)
 952        {
 110953            if (name is null)
 954            {
 0955                return null; // some items may not have name filled in properly
 956            }
 957
 110958            if (!enableAlphaNumericSorting)
 959            {
 1960                return name.TrimStart();
 961            }
 962
 109963            var sortable = name.Trim().ToLowerInvariant();
 964
 872965            foreach (var search in configuration.SortRemoveWords)
 966            {
 967                // Remove from beginning if a space follows
 327968                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 969                {
 4970                    sortable = sortable.Remove(0, search.Length + 1);
 971                }
 972
 973                // Remove from middle if surrounded by spaces
 327974                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 975
 976                // Remove from end if preceeded by a space
 327977                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 978                {
 0979                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 980                }
 981            }
 982
 1526983            foreach (var removeChar in configuration.SortRemoveCharacters)
 984            {
 654985                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 986            }
 987
 872988            foreach (var replaceChar in configuration.SortReplaceCharacters)
 989            {
 327990                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 991            }
 992
 109993            return ModifySortChunks(sortable);
 994        }
 995
 996        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 997        {
 998            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 999            {
 1000                if (isDigitChunk && chunk.Length < 10)
 1001                {
 1002                    builder.Append('0', 10 - chunk.Length);
 1003                }
 1004
 1005                builder.Append(chunk);
 1006            }
 1007
 1151008            if (name.IsEmpty)
 1009            {
 11010                return string.Empty;
 1011            }
 1012
 1141013            var builder = new StringBuilder(name.Length);
 1014
 1141015            int chunkStart = 0;
 1141016            bool isDigitChunk = char.IsDigit(name[0]);
 18981017            for (int i = 0; i < name.Length; i++)
 1018            {
 8351019                var isDigit = char.IsDigit(name[i]);
 8351020                if (isDigit != isDigitChunk)
 1021                {
 61022                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 61023                    chunkStart = i;
 61024                    isDigitChunk = isDigit;
 1025                }
 1026            }
 1027
 1141028            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 1029
 1030            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 1141031            var result = builder.ToString().RemoveDiacritics();
 1141032            if (!result.All(char.IsAscii))
 1033            {
 01034                result = result.Transliterated();
 1035            }
 1036
 1141037            return result;
 1038        }
 1039
 1040        public BaseItem GetParent()
 1041        {
 21611042            var parentId = ParentId;
 21611043            if (parentId.IsEmpty())
 1044            {
 18611045                return null;
 1046            }
 1047
 3001048            return LibraryManager.GetItemById(parentId);
 1049        }
 1050
 1051        public IEnumerable<BaseItem> GetParents()
 1052        {
 7241053            var parent = GetParent();
 1054
 7971055            while (parent is not null)
 1056            {
 731057                yield return parent;
 1058
 731059                parent = parent.GetParent();
 1060            }
 7241061        }
 1062
 1063        /// <summary>
 1064        /// Finds a parent of a given type.
 1065        /// </summary>
 1066        /// <typeparam name="T">Type of parent.</typeparam>
 1067        /// <returns>``0.</returns>
 1068        public T FindParent<T>()
 1069            where T : Folder
 1070        {
 01071            foreach (var parent in GetParents())
 1072            {
 01073                if (parent is T item)
 1074                {
 01075                    return item;
 1076                }
 1077            }
 1078
 01079            return null;
 01080        }
 1081
 1082        /// <summary>
 1083        /// Gets the play access.
 1084        /// </summary>
 1085        /// <param name="user">The user.</param>
 1086        /// <returns>PlayAccess.</returns>
 1087        public PlayAccess GetPlayAccess(User user)
 1088        {
 61089            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1090            {
 01091                return PlayAccess.None;
 1092            }
 1093
 1094            // if (!user.IsParentalScheduleAllowed())
 1095            // {
 1096            //    return PlayAccess.None;
 1097            // }
 1098
 61099            return PlayAccess.Full;
 1100        }
 1101
 1102        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1103        {
 01104            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01105            {
 01106                ItemId = Id
 01107            });
 1108        }
 1109
 1110        protected virtual bool IsActiveRecording()
 1111        {
 01112            return false;
 1113        }
 1114
 1115        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1116        {
 61117            if (SourceType == SourceType.Channel)
 1118            {
 01119                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01120                           .ToList();
 1121
 01122                if (sources.Count > 0)
 1123                {
 01124                    return sources;
 1125                }
 1126            }
 1127
 61128            var list = GetAllItemsForMediaSources().ToList();
 61129            var commonPrefix = GetCommonNamePrefix(list);
 61130            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType, commonPrefix
 1131
 61132            if (IsActiveRecording())
 1133            {
 01134                foreach (var mediaSource in result)
 1135                {
 01136                    mediaSource.Type = MediaSourceType.Placeholder;
 1137                }
 1138            }
 1139
 1140            // The source belonging to the item being queried sorts first so it is the default the client plays.
 61141            var selfId = Id.ToString("N", CultureInfo.InvariantCulture);
 1142
 61143            return result
 61144                .OrderByDescending(i => string.Equals(i.Id, selfId, StringComparison.OrdinalIgnoreCase))
 61145                .ThenBy(i => i.VideoType == VideoType.VideoFile ? 0 : 1)
 61146                .ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 61147                .ThenByDescending(i => i, new MediaSourceWidthComparator())
 61148                .ToArray();
 1149        }
 1150
 1151        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1152        {
 01153            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1154        }
 1155
 1156        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type, string 
 1157        {
 181158            ArgumentNullException.ThrowIfNull(item);
 1159
 181160            var protocol = item.PathProtocol;
 181161            var itemPath = item.Path;
 1162
 181163            var info = new MediaSourceInfo
 181164            {
 181165                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 181166                Protocol = protocol ?? MediaProtocol.File,
 181167                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 181168                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 181169                Name = GetMediaSourceName(item, commonPrefix),
 181170                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 181171                RunTimeTicks = item.RunTimeTicks,
 181172                Container = item.Container,
 181173                Size = item.Size,
 181174                Type = type,
 181175                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 181176                    && (protocol is null or MediaProtocol.File)
 181177                    && MediaSegmentManager.HasSegments(item.Id)
 181178            };
 1179
 181180            if (string.IsNullOrEmpty(info.Path))
 1181            {
 01182                info.Type = MediaSourceType.Placeholder;
 1183            }
 1184
 181185            if (info.Protocol == MediaProtocol.File)
 1186            {
 181187                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1188            }
 1189
 181190            var video = item as Video;
 181191            if (video is not null)
 1192            {
 181193                info.IsoType = video.IsoType;
 181194                info.VideoType = video.VideoType;
 181195                info.Video3DFormat = video.Video3DFormat;
 181196                info.Timestamp = video.Timestamp;
 1197
 181198                if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
 1199                {
 01200                    var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
 1201
 1202                    // Only allow remote shortcut paths — local file paths in .strm files
 1203                    // could be used to read arbitrary files from the server.
 01204                    if (shortcutProtocol != MediaProtocol.File)
 1205                    {
 01206                        info.IsRemote = true;
 01207                        info.Path = video.ShortcutPath;
 01208                        info.Protocol = shortcutProtocol;
 1209                    }
 1210                }
 1211
 181212                if (string.IsNullOrEmpty(info.Container))
 1213                {
 181214                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1215                    {
 181216                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1217                        {
 181218                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1219                        }
 1220                    }
 1221                }
 1222            }
 1223
 181224            if (string.IsNullOrEmpty(info.Container))
 1225            {
 01226                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1227                {
 01228                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1229                }
 1230            }
 1231
 181232            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1233            {
 181234                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1235            }
 1236
 181237            if (video is not null && video.VideoType != VideoType.VideoFile)
 1238            {
 01239                info.SupportsDirectStream = false;
 1240            }
 1241
 181242            info.Bitrate = item.TotalBitrate;
 181243            info.InferTotalBitrate();
 1244
 181245            return info;
 1246        }
 1247
 1248        internal string GetMediaSourceName(BaseItem item, string commonPrefix = null)
 1249        {
 381250            var terms = new List<string>();
 1251
 381252            var path = item.Path;
 381253            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1254            {
 381255                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 1256
 1257                // Prefer the suffix that differs from the other versions: strip the prefix shared by
 1258                // all sibling files. This works regardless of folder layout, so it also labels episode
 1259                // versions that share a season folder (e.g. "Greyscale" instead of the full
 1260                // "Show - S01E02 - Title - Greyscale"). The prefix is already retreated to a delimiter
 1261                // boundary (see GetCommonVersionPrefix).
 381262                if (!string.IsNullOrEmpty(commonPrefix)
 381263                    && displayName.Length > commonPrefix.Length
 381264                    && displayName.StartsWith(commonPrefix, StringComparison.OrdinalIgnoreCase))
 1265                {
 271266                    var name = displayName.AsSpan(commonPrefix.Length).TrimStart([' ', .. VersionDelimiters]);
 271267                    if (!name.IsWhiteSpace())
 1268                    {
 271269                        terms.Add(name.ToString());
 1270                    }
 1271                }
 1272
 1273                // Fall back to the containing folder name (the common layout for movie versions, and
 1274                // the path taken when no common prefix could be derived).
 381275                if (terms.Count == 0 && HasLocalAlternateVersions)
 1276                {
 81277                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 81278                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1279                    {
 21280                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', .. VersionDelimiters]
 21281                        if (!name.IsWhiteSpace())
 1282                        {
 21283                            terms.Add(name.ToString());
 1284                        }
 1285                    }
 1286                }
 1287
 381288                if (terms.Count == 0)
 1289                {
 91290                    terms.Add(displayName);
 1291                }
 1292            }
 1293
 381294            if (terms.Count == 0)
 1295            {
 01296                terms.Add(item.Name);
 1297            }
 1298
 381299            if (item is Video video)
 1300            {
 381301                if (video.Video3DFormat.HasValue)
 1302                {
 01303                    terms.Add("3D");
 1304                }
 1305
 381306                if (video.VideoType == VideoType.BluRay)
 1307                {
 01308                    terms.Add("Bluray");
 1309                }
 381310                else if (video.VideoType == VideoType.Dvd)
 1311                {
 01312                    terms.Add("DVD");
 1313                }
 381314                else if (video.VideoType == VideoType.Iso)
 1315                {
 01316                    if (video.IsoType.HasValue)
 1317                    {
 01318                        if (video.IsoType.Value == IsoType.BluRay)
 1319                        {
 01320                            terms.Add("Bluray");
 1321                        }
 01322                        else if (video.IsoType.Value == IsoType.Dvd)
 1323                        {
 01324                            terms.Add("DVD");
 1325                        }
 1326                    }
 1327                    else
 1328                    {
 01329                        terms.Add("ISO");
 1330                    }
 1331                }
 1332            }
 1333
 381334            return string.Join('/', terms);
 1335        }
 1336
 1337        /// <summary>
 1338        /// Derives the prefix shared by the supplied media source items' file names, used to strip the
 1339        /// common part and surface a short version label per source. Returns null when there are fewer
 1340        /// than two file-based sources, since there is nothing to differentiate.
 1341        /// </summary>
 1342        /// <param name="items">The media source items.</param>
 1343        /// <returns>The shared prefix, or null when no useful prefix exists.</returns>
 1344        private static string GetCommonNamePrefix(IReadOnlyList<(BaseItem Item, MediaSourceType MediaSourceType)> items)
 1345        {
 61346            var fileNames = new List<string>();
 481347            foreach (var (item, _) in items)
 1348            {
 181349                if (item.IsFileProtocol && !string.IsNullOrEmpty(item.Path))
 1350                {
 181351                    fileNames.Add(System.IO.Path.GetFileNameWithoutExtension(item.Path));
 1352                }
 1353            }
 1354
 61355            if (fileNames.Count < 2)
 1356            {
 01357                return null;
 1358            }
 1359
 61360            var prefix = GetCommonVersionPrefix(fileNames);
 61361            return string.IsNullOrEmpty(prefix) ? null : prefix;
 1362        }
 1363
 1364        /// <summary>
 1365        /// Computes the case-insensitive longest common prefix of the supplied version file names,
 1366        /// retreated to the last delimiter boundary. Retreating keeps the differing suffix intact:
 1367        /// it avoids slicing through a word every version shares (e.g. "Grey" in "Greyscale" and
 1368        /// "Greyish") while still trimming the common part when every version is suffixed (e.g.
 1369        /// "- Greyscale" / "- Colorized"). It prefers a structural delimiter ('-', '_', '.') so a
 1370        /// token shared by the descriptors but separated only by spaces (e.g. a common "2160p ") is
 1371        /// kept in the label, falling back to a space only when no structural delimiter is shared. The
 1372        /// separators mirror the version delimiters recognised by the naming layer (Emby.Naming
 1373        /// VideoFlagDelimiters).
 1374        /// </summary>
 1375        /// <param name="fileNames">The version file names without extension; must contain at least one entry.</param>
 1376        /// <returns>The shared prefix retreated to a separator boundary, or an empty string when none is shared.</retur
 1377        internal static string GetCommonVersionPrefix(IReadOnlyList<string> fileNames)
 1378        {
 141379            var prefix = fileNames[0];
 681380            for (var i = 1; i < fileNames.Count && prefix.Length > 0; i++)
 1381            {
 201382                var name = fileNames[i];
 201383                var length = Math.Min(prefix.Length, name.Length);
 201384                var common = 0;
 2531385                while (common < length && char.ToUpperInvariant(prefix[common]) == char.ToUpperInvariant(name[common]))
 1386                {
 2331387                    common++;
 1388                }
 1389
 201390                prefix = prefix[..common];
 1391            }
 1392
 1393            // If the common prefix is itself a whole file name then one version is unlabelled (the
 1394            // base name); the boundary already sits at the end of that name, so don't retreat into it.
 141395            var prefixIsWholeName = false;
 601396            for (var i = 0; i < fileNames.Count; i++)
 1397            {
 231398                if (fileNames[i].Length == prefix.Length)
 1399                {
 71400                    prefixIsWholeName = true;
 71401                    break;
 1402                }
 1403            }
 1404
 141405            if (!prefixIsWholeName)
 1406            {
 1407                // Retreat to the last structural delimiter ('-', '_', '.').
 71408                var cut = prefix.Length;
 461409                while (cut > 0 && Array.IndexOf(VersionDelimiters, prefix[cut - 1]) < 0)
 1410                {
 391411                    cut--;
 1412                }
 1413
 71414                if (cut == 0)
 1415                {
 11416                    cut = prefix.Length;
 21417                    while (cut > 0 && prefix[cut - 1] != ' ')
 1418                    {
 11419                        cut--;
 1420                    }
 1421                }
 1422
 71423                prefix = prefix[..cut];
 1424            }
 1425
 141426            return prefix;
 1427        }
 1428
 1429        public Task RefreshMetadata(CancellationToken cancellationToken)
 1430        {
 541431            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1432        }
 1433
 1434        /// <summary>
 1435        /// The base implementation to refresh metadata.
 1436        /// </summary>
 1437        /// <param name="options">The options.</param>
 1438        /// <param name="cancellationToken">The cancellation token.</param>
 1439        /// <returns>true if a provider reports we changed.</returns>
 1440        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1441        {
 611442            var requiresSave = false;
 1443
 611444            if (SupportsOwnedItems)
 1445            {
 1446                try
 1447                {
 381448                    if (IsFileProtocol)
 1449                    {
 381450                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1451                    }
 1452
 381453                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 381454                }
 01455                catch (Exception ex)
 1456                {
 01457                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 01458                }
 1459            }
 1460
 611461            var refreshOptions = requiresSave
 611462                ? new MetadataRefreshOptions(options)
 611463                {
 611464                    ForceSave = true
 611465                }
 611466                : options;
 1467
 611468            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 601469        }
 1470
 1471        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1472        {
 01473            if (!IsVisible(user))
 1474            {
 01475                return false;
 1476            }
 1477
 01478            var parents = GetParents().ToList();
 01479            if (parents.Any(i => !i.IsVisible(user, true)))
 1480            {
 01481                return false;
 1482            }
 1483
 01484            if (checkFolders)
 1485            {
 01486                var topParent = parents.Count > 0 ? parents[^1] : this;
 1487
 01488                if (string.IsNullOrEmpty(topParent.Path))
 1489                {
 01490                    return true;
 1491                }
 1492
 01493                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1494
 01495                if (itemCollectionFolders.Count > 0)
 1496                {
 01497                    var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 1498                    IEnumerable<Guid> userCollectionFolderIds;
 01499                    if (blockedMediaFolders.Length > 0)
 1500                    {
 1501                        // User has blocked folders - get all library folders and exclude blocked ones
 01502                        userCollectionFolderIds = LibraryManager.GetUserRootFolder().Children
 01503                            .Select(i => i.Id)
 01504                            .Where(id => !blockedMediaFolders.Contains(id));
 1505                    }
 01506                    else if (user.HasPermission(PermissionKind.EnableAllFolders))
 1507                    {
 1508                        // User can access all folders - no need to filter
 01509                        return true;
 1510                    }
 1511                    else
 1512                    {
 1513                        // User has specific enabled folders
 01514                        userCollectionFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders);
 1515                    }
 1516
 01517                    if (!itemCollectionFolders.Any(userCollectionFolderIds.Contains))
 1518                    {
 01519                        return false;
 1520                    }
 1521                }
 1522            }
 1523
 01524            return true;
 1525        }
 1526
 1527        public void SetParent(Folder parent)
 1528        {
 91529            ParentId = parent is null ? Guid.Empty : parent.Id;
 91530        }
 1531
 1532        /// <summary>
 1533        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1534        /// Returns true or false indicating if changes were found.
 1535        /// </summary>
 1536        /// <param name="options">The metadata refresh options.</param>
 1537        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1538        /// <param name="cancellationToken">The cancellation token.</param>
 1539        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1540        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1541        {
 401542            if (!SearchesContainingFolderForExtras)
 1543            {
 381544                return false;
 1545            }
 1546
 21547            if (GetParent() is Folder container
 21548                && container.SearchesContainingFolderForExtras
 21549                && string.Equals(container.Path, ContainingFolderPath, StringComparison.OrdinalIgnoreCase))
 1550            {
 11551                return false;
 1552            }
 1553
 11554            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 401555        }
 1556
 1557        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1558        {
 421559            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1560        }
 1561
 1562        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1563        {
 1564            // An extra is owned by the version it is named after, so all of them are maintained together.
 11565            var currentExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 11566            {
 11567                OwnerIds = item.GetOwnedVersionIds()
 11568            }).Where(e => e.ExtraType.HasValue).ToList();
 1569
 11570            var currentExtraIds = currentExtras.Select(e => e.Id).ToArray();
 1571
 1572            // Snapshot the persisted names before resolving, as FindExtras corrects the name on the
 1573            // items it hands back and may well hand back these very instances.
 11574            var currentExtraNames = new Dictionary<Guid, string>();
 21575            foreach (var extra in currentExtras)
 1576            {
 01577                currentExtraNames[extra.Id] = extra.Name;
 1578            }
 1579
 11580            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 11581            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1582
 11583            var renamedExtraIds = extras
 11584                .Where(e => currentExtraNames.TryGetValue(e.Id, out var oldName) && !string.Equals(oldName, e.Name, Stri
 11585                .Select(e => e.Id)
 11586                .ToHashSet();
 1587
 11588            var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
 1589
 11590            if (!extrasChanged && renamedExtraIds.Count == 0 && !options.ReplaceAllMetadata && options.MetadataRefreshMo
 1591            {
 1592                // The owner's dates may only have become known after its extras were created, so keep
 1593                // them in sync even when there is nothing to refresh.
 21594                foreach (var extra in currentExtras)
 1595                {
 01596                    if (extra.ExtraType is not null && InheritDatesFromOwner(item, extra))
 1597                    {
 01598                        await extra.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwa
 1599                    }
 1600                }
 1601
 11602                return false;
 1603            }
 1604
 01605            var tasks = extras.Select(i =>
 01606            {
 01607                var ownerId = item.GetOwnerIdForExtra(i);
 01608                var subOptions = new MetadataRefreshOptions(options);
 01609                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty() || renamedExtraIds.Contains(i.Id))
 01610                {
 01611                    subOptions.ForceSave = true;
 01612                }
 01613
 01614                i.OwnerId = ownerId;
 01615                i.ParentId = Guid.Empty;
 01616
 01617                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 01618            });
 1619
 01620            var removedExtraIds = currentExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 01621            if (removedExtraIds.Length > 0)
 1622            {
 01623                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 01624                {
 01625                    ItemIds = removedExtraIds
 01626                });
 01627                foreach (var removedExtra in removedExtras)
 1628                {
 1629                    // Only delete items that are actual extras (have ExtraType set)
 1630                    // Items with OwnerId but no ExtraType might be alternate versions, not extras
 01631                    if (removedExtra.ExtraType.HasValue)
 1632                    {
 01633                        LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 01634                        {
 01635                            DeleteFileLocation = false
 01636                        });
 1637                    }
 1638                }
 1639            }
 1640
 01641            await Task.WhenAll(tasks).ConfigureAwait(false);
 1642
 01643            return true;
 11644        }
 1645
 1646        public string GetPresentationUniqueKey()
 1647        {
 01648            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1649        }
 1650
 1651        public virtual bool RequiresRefresh()
 1652        {
 611653            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1654            {
 611655                return false;
 1656            }
 1657
 01658            var info = FileSystem.GetFileSystemInfo(Path);
 1659
 01660            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1661        }
 1662
 1663        public virtual List<string> GetUserDataKeys()
 1664        {
 1721665            var list = new List<string>();
 1666
 1721667            if (SourceType == SourceType.Channel)
 1668            {
 01669                if (!string.IsNullOrEmpty(ExternalId))
 1670                {
 01671                    list.Add(ExternalId);
 1672                }
 1673            }
 1674
 1721675            list.Add(Id.ToString());
 1721676            return list;
 1677        }
 1678
 1679        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1680        {
 51681            var updateType = ItemUpdateType.None;
 1682
 51683            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1684            {
 01685                IsInMixedFolder = newItem.IsInMixedFolder;
 01686                updateType |= ItemUpdateType.MetadataImport;
 1687            }
 1688
 51689            return updateType;
 1690        }
 1691
 1692        public void AfterMetadataRefresh()
 1693        {
 601694            _sortName = null;
 601695        }
 1696
 1697        /// <summary>
 1698        /// Gets the preferred metadata language.
 1699        /// </summary>
 1700        /// <returns>System.String.</returns>
 1701        public string GetPreferredMetadataLanguage()
 1702        {
 361703            string lang = PreferredMetadataLanguage;
 1704
 361705            if (string.IsNullOrEmpty(lang))
 1706            {
 361707                lang = GetParents()
 361708                    .Select(i => i.PreferredMetadataLanguage)
 361709                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1710            }
 1711
 361712            if (string.IsNullOrEmpty(lang))
 1713            {
 361714                lang = LibraryManager.GetCollectionFolders(this)
 361715                    .Select(i => i.PreferredMetadataLanguage)
 361716                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1717            }
 1718
 361719            if (string.IsNullOrEmpty(lang))
 1720            {
 361721                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1722            }
 1723
 361724            if (string.IsNullOrEmpty(lang))
 1725            {
 361726                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1727            }
 1728
 361729            return lang;
 1730        }
 1731
 1732        /// <summary>
 1733        /// Gets the preferred metadata country code.
 1734        /// </summary>
 1735        /// <returns>System.String.</returns>
 1736        public string GetPreferredMetadataCountryCode()
 1737        {
 361738            string lang = PreferredMetadataCountryCode;
 1739
 361740            if (string.IsNullOrEmpty(lang))
 1741            {
 361742                lang = GetParents()
 361743                    .Select(i => i.PreferredMetadataCountryCode)
 361744                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1745            }
 1746
 361747            if (string.IsNullOrEmpty(lang))
 1748            {
 361749                lang = LibraryManager.GetCollectionFolders(this)
 361750                    .Select(i => i.PreferredMetadataCountryCode)
 361751                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1752            }
 1753
 361754            if (string.IsNullOrEmpty(lang))
 1755            {
 361756                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1757            }
 1758
 361759            if (string.IsNullOrEmpty(lang))
 1760            {
 361761                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1762            }
 1763
 361764            return lang;
 1765        }
 1766
 1767        /// <summary>
 1768        /// Gets the original language of the item, inheriting from parent items if necessary.
 1769        /// </summary>
 1770        /// <returns>System.String.</returns>
 1771        public virtual string GetInheritedOriginalLanguage()
 1772        {
 01773            return OriginalLanguage;
 1774        }
 1775
 1776        public virtual bool IsSaveLocalMetadataEnabled()
 1777        {
 881778            if (SourceType == SourceType.Channel)
 1779            {
 01780                return false;
 1781            }
 1782
 881783            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1784
 881785            return libraryOptions.SaveLocalMetadata;
 1786        }
 1787
 1788        /// <summary>
 1789        /// Determines if a given user has access to this item.
 1790        /// </summary>
 1791        /// <param name="user">The user.</param>
 1792        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1793        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1794        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1795        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1796        {
 101797            ArgumentNullException.ThrowIfNull(user);
 1798
 101799            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1800            {
 01801                return false;
 1802            }
 1803
 101804            var maxAllowedRating = user.MaxParentalRatingScore;
 101805            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 101806            var rating = CustomRatingForComparison;
 1807
 101808            if (string.IsNullOrEmpty(rating))
 1809            {
 101810                rating = OfficialRatingForComparison;
 1811            }
 1812
 101813            if (string.IsNullOrEmpty(rating))
 1814            {
 101815                return !GetBlockUnratedValue(user);
 1816            }
 1817
 01818            var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1819
 1820            // Could not determine rating level
 01821            if (ratingScore is null)
 1822            {
 01823                var isAllowed = !GetBlockUnratedValue(user);
 1824
 01825                if (!isAllowed)
 1826                {
 01827                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1828                }
 1829
 01830                return isAllowed;
 1831            }
 1832
 01833            if (!maxAllowedRating.HasValue)
 1834            {
 01835                return true;
 1836            }
 1837
 01838            if (ratingScore.Score != maxAllowedRating.Value)
 1839            {
 01840                return ratingScore.Score < maxAllowedRating.Value;
 1841            }
 1842
 01843            return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
 1844        }
 1845
 1846        public ParentalRatingScore GetParentalRatingScore()
 1847        {
 1221848            var rating = CustomRatingForComparison;
 1849
 1221850            if (string.IsNullOrEmpty(rating))
 1851            {
 1221852                rating = OfficialRatingForComparison;
 1853            }
 1854
 1221855            if (string.IsNullOrEmpty(rating))
 1856            {
 1221857                return null;
 1858            }
 1859
 01860            return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
 1861        }
 1862
 1863        public List<string> GetInheritedTags()
 1864        {
 1161865            var list = new List<string>();
 1161866            list.AddRange(Tags);
 1867
 2881868            foreach (var parent in GetParents())
 1869            {
 281870                list.AddRange(parent.Tags);
 1871            }
 1872
 2321873            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1874            {
 01875                list.AddRange(folder.Tags);
 1876            }
 1877
 1161878            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1879        }
 1880
 1881        protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1882        {
 101883            var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
 101884            var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
 1885
 101886            if (blockedTags.Length == 0 && allowedTags.Length == 0)
 1887            {
 101888                return true;
 1889            }
 1890
 1891            // Normalize tags using the same logic as database queries
 01892            var normalizedBlockedTags = blockedTags
 01893                .Where(t => !string.IsNullOrWhiteSpace(t))
 01894                .Select(t => t.GetCleanValue())
 01895                .ToHashSet(StringComparer.Ordinal);
 1896
 01897            var normalizedItemTags = GetInheritedTags()
 01898                .Select(t => t.GetCleanValue())
 01899                .ToHashSet(StringComparer.Ordinal);
 1900
 1901            // Check blocked tags - item is hidden if it has any blocked tag
 01902            if (normalizedBlockedTags.Overlaps(normalizedItemTags))
 1903            {
 01904                return false;
 1905            }
 1906
 01907            var parent = GetParents().FirstOrDefault() ?? this;
 01908            if (parent is UserRootFolder or AggregateFolder or UserView)
 1909            {
 01910                return true;
 1911            }
 1912
 1913            // Check allowed tags - item must have at least one allowed tag
 01914            if (!skipAllowedTagsCheck && allowedTags.Length > 0)
 1915            {
 01916                var normalizedAllowedTags = allowedTags
 01917                    .Where(t => !string.IsNullOrWhiteSpace(t))
 01918                    .Select(t => t.GetCleanValue())
 01919                    .ToHashSet(StringComparer.Ordinal);
 1920
 01921                if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
 1922                {
 01923                    return false;
 1924                }
 1925            }
 1926
 01927            return true;
 1928        }
 1929
 1930        public virtual UnratedItem GetBlockUnratedType()
 1931        {
 1161932            if (SourceType == SourceType.Channel)
 1933            {
 01934                return UnratedItem.ChannelContent;
 1935            }
 1936
 1161937            return UnratedItem.Other;
 1938        }
 1939
 1940        /// <summary>
 1941        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1942        /// </summary>
 1943        /// <param name="user">The configuration.</param>
 1944        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1945        protected virtual bool GetBlockUnratedValue(User user)
 1946        {
 1947            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1948            // Special folders like series and albums will override this method.
 101949            if (IsFolder || this is IItemByName)
 1950            {
 101951                return false;
 1952            }
 1953
 01954            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1955        }
 1956
 1957        /// <summary>
 1958        /// Determines if this folder should be visible to a given user.
 1959        /// Default is just parental allowed. Can be overridden for more functionality.
 1960        /// </summary>
 1961        /// <param name="user">The user.</param>
 1962        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1963        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1964        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1965        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1966        {
 101967            ArgumentNullException.ThrowIfNull(user);
 1968
 101969            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1970        }
 1971
 1972        public virtual bool IsVisibleStandalone(User user)
 1973        {
 01974            if (SourceType == SourceType.Channel)
 1975            {
 01976                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1977            }
 1978
 01979            return IsVisibleStandaloneInternal(user, true);
 1980        }
 1981
 1982        public virtual string GetClientTypeName()
 1983        {
 1211984            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1985            {
 01986                return "ChannelFolderItem";
 1987            }
 1988
 1211989            return GetType().Name;
 1990        }
 1991
 1992        public BaseItemKind GetBaseItemKind()
 1993        {
 2581994            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1995        }
 1996
 1997        /// <summary>
 1998        /// Gets the linked child.
 1999        /// </summary>
 2000        /// <param name="info">The info.</param>
 2001        /// <returns>BaseItem.</returns>
 2002        protected BaseItem GetLinkedChild(LinkedChild info)
 2003        {
 2004            // First get using the cached Id
 02005            if (info.ItemId.HasValue)
 2006            {
 02007                if (info.ItemId.Value.IsEmpty())
 2008                {
 02009                    return null;
 2010                }
 2011
 02012                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 2013
 02014                if (itemById is not null)
 2015                {
 02016                    return itemById;
 2017                }
 2018            }
 2019
 02020            var item = FindLinkedChild(info);
 2021
 2022            // If still null, log
 02023            if (item is null)
 2024            {
 2025                // Don't keep searching over and over
 02026                info.ItemId = Guid.Empty;
 2027            }
 2028            else
 2029            {
 2030                // Cache the id for next time
 02031                info.ItemId = item.Id;
 2032            }
 2033
 02034            return item;
 2035        }
 2036
 2037#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
 2038        private BaseItem FindLinkedChild(LinkedChild info)
 2039        {
 2040            // First try to find by ItemId (new preferred method)
 02041            if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
 2042            {
 02043                var item = LibraryManager.GetItemById(info.ItemId.Value);
 02044                if (item is not null)
 2045                {
 02046                    return item;
 2047                }
 2048
 02049                Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
 2050            }
 2051
 2052            // Fall back to Path (legacy method)
 02053            var path = info.Path;
 02054            if (!string.IsNullOrEmpty(path))
 2055            {
 02056                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 2057
 02058                var itemByPath = LibraryManager.FindByPath(path, null);
 2059
 02060                if (itemByPath is null)
 2061                {
 02062                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 2063                }
 2064
 02065                return itemByPath;
 2066            }
 2067
 2068            // Fall back to LibraryItemId (legacy method)
 02069            if (!string.IsNullOrEmpty(info.LibraryItemId))
 2070            {
 02071                var item = LibraryManager.GetItemById(info.LibraryItemId);
 2072
 02073                if (item is null)
 2074                {
 02075                    Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
 2076                }
 2077
 02078                return item;
 2079            }
 2080
 02081            return null;
 2082        }
 2083#pragma warning restore CS0618
 2084
 2085        /// <summary>
 2086        /// Adds a studio to the item.
 2087        /// </summary>
 2088        /// <param name="name">The name.</param>
 2089        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 2090        public void AddStudio(string name)
 2091        {
 42092            ArgumentException.ThrowIfNullOrEmpty(name);
 42093            var current = Studios;
 2094
 42095            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 2096            {
 42097                int curLen = current.Length;
 42098                if (curLen == 0)
 2099                {
 42100                    Studios = [name];
 2101                }
 2102                else
 2103                {
 02104                    Studios = [.. current, name];
 2105                }
 2106            }
 02107        }
 2108
 2109        public void SetStudios(IEnumerable<string> names)
 2110        {
 52111            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 52112        }
 2113
 2114        /// <summary>
 2115        /// Adds a genre to the item.
 2116        /// </summary>
 2117        /// <param name="name">The name.</param>
 2118        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 2119        public void AddGenre(string name)
 2120        {
 132121            ArgumentException.ThrowIfNullOrEmpty(name);
 2122
 132123            var genres = Genres;
 132124            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 2125            {
 132126                Genres = [.. genres, name];
 2127            }
 132128        }
 2129
 2130        /// <summary>
 2131        /// Marks the played.
 2132        /// </summary>
 2133        /// <param name="user">The user.</param>
 2134        /// <param name="datePlayed">The date played.</param>
 2135        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 2136        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2137        public virtual void MarkPlayed(
 2138            User user,
 2139            DateTime? datePlayed,
 2140            bool resetPosition)
 2141        {
 02142            ArgumentNullException.ThrowIfNull(user);
 2143
 02144            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 02145            {
 02146                Key = GetUserDataKeys().First(),
 02147            };
 2148
 02149            if (datePlayed.HasValue)
 2150            {
 2151                // Increment
 02152                data.PlayCount++;
 2153            }
 2154
 2155            // Ensure it's at least one
 02156            data.PlayCount = Math.Max(data.PlayCount, 1);
 2157
 02158            if (resetPosition)
 2159            {
 02160                data.PlaybackPositionTicks = 0;
 2161            }
 2162
 02163            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 02164            data.Played = true;
 2165
 02166            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02167        }
 2168
 2169        /// <summary>
 2170        /// Marks the unplayed.
 2171        /// </summary>
 2172        /// <param name="user">The user.</param>
 2173        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 2174        public virtual void MarkUnplayed(User user)
 2175        {
 02176            ArgumentNullException.ThrowIfNull(user);
 2177
 02178            var data = UserDataManager.GetUserData(user, this);
 2179
 2180            // I think it is okay to do this here.
 2181            // if this is only called when a user is manually forcing something to un-played
 2182            // then it probably is what we want to do...
 02183            ResetPlayedState(data);
 2184
 02185            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 02186        }
 2187
 2188        /// <summary>
 2189        /// Clears the played state on the supplied user data.
 2190        /// </summary>
 2191        /// <param name="data">The user data to reset.</param>
 2192        protected static void ResetPlayedState(UserItemData data)
 2193        {
 22194            ArgumentNullException.ThrowIfNull(data);
 2195
 22196            data.PlayCount = 0;
 22197            data.PlaybackPositionTicks = 0;
 22198            data.LastPlayedDate = null;
 22199            data.Played = false;
 22200        }
 2201
 2202        /// <summary>
 2203        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 2204        /// </summary>
 2205        public virtual void ChangedExternally()
 2206        {
 02207            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 02208        }
 2209
 2210        /// <summary>
 2211        /// Gets an image.
 2212        /// </summary>
 2213        /// <param name="type">The type.</param>
 2214        /// <param name="imageIndex">Index of the image.</param>
 2215        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 2216        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 2217        public bool HasImage(ImageType type, int imageIndex)
 2218        {
 1462219            return GetImageInfo(type, imageIndex) is not null;
 2220        }
 2221
 2222        public void SetImage(ItemImageInfo image, int index)
 2223        {
 192224            if (image.Type == ImageType.Chapter)
 2225            {
 02226                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2227            }
 2228
 192229            var existingImage = GetImageInfo(image.Type, index);
 2230
 192231            if (existingImage is null)
 2232            {
 172233                AddImage(image);
 2234            }
 2235            else
 2236            {
 22237                existingImage.Path = image.Path;
 22238                existingImage.DateModified = image.DateModified;
 22239                existingImage.Width = image.Width;
 22240                existingImage.Height = image.Height;
 22241                existingImage.BlurHash = image.BlurHash;
 2242            }
 22243        }
 2244
 2245        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 2246        {
 462247            if (type == ImageType.Chapter)
 2248            {
 02249                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 2250            }
 2251
 462252            var image = GetImageInfo(type, index);
 2253
 462254            if (image is null)
 2255            {
 452256                AddImage(GetImageInfo(file, type));
 2257            }
 2258            else
 2259            {
 12260                var imageInfo = GetImageInfo(file, type);
 2261
 12262                image.Path = file.FullName;
 12263                image.DateModified = imageInfo.DateModified;
 2264
 2265                // reset these values
 12266                image.Width = 0;
 12267                image.Height = 0;
 2268            }
 12269        }
 2270
 2271        /// <summary>
 2272        /// Deletes the image.
 2273        /// </summary>
 2274        /// <param name="type">The type.</param>
 2275        /// <param name="index">The index.</param>
 2276        /// <returns>A task.</returns>
 2277        public async Task DeleteImageAsync(ImageType type, int index)
 2278        {
 02279            var info = GetImageInfo(type, index);
 2280
 02281            if (info is null)
 2282            {
 2283                // Nothing to do
 02284                return;
 2285            }
 2286
 2287            // Remove from file system
 02288            var path = info.Path;
 02289            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2290            {
 02291                FileSystem.DeleteFile(path);
 2292            }
 2293
 2294            // Remove from item
 02295            RemoveImage(info);
 2296
 02297            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 02298        }
 2299
 2300        public void RemoveImage(ItemImageInfo image)
 2301        {
 02302            RemoveImages([image]);
 02303        }
 2304
 2305        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2306        {
 92307            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92308        }
 2309
 2310        public void AddImage(ItemImageInfo image)
 2311        {
 622312            ImageInfos = [.. ImageInfos, image];
 622313        }
 2314
 2315        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 1152316         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2317
 2318        public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
 342319            await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
 2320
 2321        /// <summary>
 2322        /// Validates that images within the item are still on the filesystem.
 2323        /// </summary>
 2324        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2325        public bool ValidateImages()
 2326        {
 752327            List<ItemImageInfo> deletedImages = null;
 1802328            foreach (var imageInfo in ImageInfos)
 2329            {
 152330                if (!imageInfo.IsLocalFile)
 2331                {
 2332                    continue;
 2333                }
 2334
 152335                if (File.Exists(imageInfo.Path))
 2336                {
 2337                    continue;
 2338                }
 2339
 32340                (deletedImages ??= []).Add(imageInfo);
 2341            }
 2342
 752343            var anyImagesRemoved = deletedImages?.Count > 0;
 752344            if (anyImagesRemoved)
 2345            {
 22346                RemoveImages(deletedImages);
 2347            }
 2348
 752349            return anyImagesRemoved;
 2350        }
 2351
 2352        /// <summary>
 2353        /// Gets the image path.
 2354        /// </summary>
 2355        /// <param name="imageType">Type of the image.</param>
 2356        /// <param name="imageIndex">Index of the image.</param>
 2357        /// <returns>System.String.</returns>
 2358        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2359        public string GetImagePath(ImageType imageType, int imageIndex)
 22360            => GetImageInfo(imageType, imageIndex)?.Path;
 2361
 2362        /// <summary>
 2363        /// Gets the image information.
 2364        /// </summary>
 2365        /// <param name="imageType">Type of the image.</param>
 2366        /// <param name="imageIndex">Index of the image.</param>
 2367        /// <returns>ItemImageInfo.</returns>
 2368        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2369        {
 3222370            if (imageType == ImageType.Chapter)
 2371            {
 02372                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2373
 02374                if (chapter is null)
 2375                {
 02376                    return null;
 2377                }
 2378
 02379                var path = chapter.ImagePath;
 2380
 02381                if (string.IsNullOrEmpty(path))
 2382                {
 02383                    return null;
 2384                }
 2385
 02386                return new ItemImageInfo
 02387                {
 02388                    Path = path,
 02389                    DateModified = chapter.ImageDateModified,
 02390                    Type = imageType
 02391                };
 2392            }
 2393
 3222394            return GetImages(imageType)
 3222395                .ElementAtOrDefault(imageIndex);
 2396        }
 2397
 2398        /// <summary>
 2399        /// Computes image index for given image or raises if no matching image found.
 2400        /// </summary>
 2401        /// <param name="image">Image to compute index for.</param>
 2402        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2403        /// </exception>
 2404        /// <returns>Image index.</returns>
 2405        public int GetImageIndex(ItemImageInfo image)
 2406        {
 02407            ArgumentNullException.ThrowIfNull(image);
 2408
 02409            if (image.Type == ImageType.Chapter)
 2410            {
 02411                var chapters = ChapterManager.GetChapters(Id);
 02412                for (var i = 0; i < chapters.Count; i++)
 2413                {
 02414                    if (chapters[i].ImagePath == image.Path)
 2415                    {
 02416                        return i;
 2417                    }
 2418                }
 2419
 02420                throw new ArgumentException("No chapter index found for image path", image.Path);
 2421            }
 2422
 02423            var images = GetImages(image.Type).ToArray();
 02424            for (var i = 0; i < images.Length; i++)
 2425            {
 02426                if (images[i].Path == image.Path)
 2427                {
 02428                    return i;
 2429                }
 2430            }
 2431
 02432            throw new ArgumentException("No image index found for image path", image.Path);
 2433        }
 2434
 2435        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2436        {
 4322437            if (imageType == ImageType.Chapter)
 2438            {
 02439                throw new ArgumentException("No image info for chapter images");
 2440            }
 2441
 2442            // Yield return is more performant than LINQ Where on an Array
 14202443            for (var i = 0; i < ImageInfos.Length; i++)
 2444            {
 3032445                var imageInfo = ImageInfos[i];
 3032446                if (imageInfo.Type == imageType)
 2447                {
 1572448                    yield return imageInfo;
 2449                }
 2450            }
 4072451        }
 2452
 2453        /// <summary>
 2454        /// Adds the images, updating metadata if they already are part of this item.
 2455        /// </summary>
 2456        /// <param name="imageType">Type of the image.</param>
 2457        /// <param name="images">The images.</param>
 2458        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2459        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2460        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2461        {
 42462            if (imageType == ImageType.Chapter)
 2463            {
 02464                throw new ArgumentException("Cannot call AddImages with chapter images");
 2465            }
 2466
 42467            var existingImages = GetImages(imageType)
 42468                .ToList();
 2469
 42470            var newImageList = new List<FileSystemMetadata>();
 42471            var imageUpdated = false;
 2472
 242473            foreach (var newImage in images)
 2474            {
 82475                if (newImage is null)
 2476                {
 02477                    throw new ArgumentException("null image found in list");
 2478                }
 2479
 82480                var existing = existingImages
 82481                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2482
 82483                if (existing is null)
 2484                {
 42485                    newImageList.Add(newImage);
 2486                }
 2487                else
 2488                {
 42489                    if (existing.IsLocalFile)
 2490                    {
 42491                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2492
 2493                        // If date changed then we need to reset saved image dimensions
 42494                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2495                        {
 22496                            existing.Width = 0;
 22497                            existing.Height = 0;
 22498                            imageUpdated = true;
 2499                        }
 2500
 42501                        existing.DateModified = newDateModified;
 2502                    }
 2503                }
 2504            }
 2505
 42506            if (newImageList.Count > 0)
 2507            {
 22508                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2509            }
 2510
 42511            return imageUpdated || newImageList.Count > 0;
 2512        }
 2513
 2514        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2515        {
 502516            return new ItemImageInfo
 502517            {
 502518                Path = file.FullName,
 502519                Type = type,
 502520                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502521            };
 2522        }
 2523
 2524        /// <summary>
 2525        /// Gets the file system path to delete when the item is to be deleted.
 2526        /// </summary>
 2527        /// <returns>The metadata for the deleted paths.</returns>
 2528        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2529        {
 02530            return new[]
 02531            {
 02532                FileSystem.GetFileSystemInfo(Path)
 02533            }.Concat(GetLocalMetadataFilesToDelete());
 2534        }
 2535
 2536        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2537        {
 02538            if (IsFolder || !IsInMixedFolder)
 2539            {
 02540                return [];
 2541            }
 2542
 02543            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2544
 02545            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02546                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02547                .ToList();
 2548        }
 2549
 2550        public bool AllowsMultipleImages(ImageType type)
 2551        {
 232552            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2553        }
 2554
 2555        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2556        {
 02557            if (!AllowsMultipleImages(type))
 2558            {
 02559                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2560            }
 2561
 02562            var info1 = GetImageInfo(type, index1);
 02563            var info2 = GetImageInfo(type, index2);
 2564
 02565            if (info1 is null || info2 is null)
 2566            {
 2567                // Nothing to do
 02568                return Task.CompletedTask;
 2569            }
 2570
 02571            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2572            {
 2573                // TODO: Not supported  yet
 02574                return Task.CompletedTask;
 2575            }
 2576
 02577            var path1 = info1.Path;
 02578            var path2 = info2.Path;
 2579
 02580            FileSystem.SwapFiles(path1, path2);
 2581
 2582            // Refresh these values
 02583            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02584            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2585
 02586            info1.Width = 0;
 02587            info1.Height = 0;
 02588            info2.Width = 0;
 02589            info2.Height = 0;
 2590
 02591            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2592        }
 2593
 2594        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2595        {
 02596            userItemData ??= UserDataManager.GetUserData(user, this);
 2597
 02598            return userItemData is not null && userItemData.Played;
 2599        }
 2600
 2601        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2602        {
 02603            userItemData ??= UserDataManager.GetUserData(user, this);
 2604
 02605            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2606        }
 2607
 2608        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2609        {
 02610            ArgumentNullException.ThrowIfNull(user);
 2611
 02612            userItemData ??= UserDataManager.GetUserData(user, this);
 2613
 02614            return userItemData is null || !userItemData.Played;
 2615        }
 2616
 2617        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2618        {
 362619            return GetItemLookupInfo<ItemLookupInfo>();
 2620        }
 2621
 2622        protected T GetItemLookupInfo<T>()
 2623            where T : ItemLookupInfo, new()
 2624        {
 362625            return new T
 362626            {
 362627                Path = Path,
 362628                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 362629                MetadataLanguage = GetPreferredMetadataLanguage(),
 362630                Name = GetNameForMetadataLookup(),
 362631                OriginalTitle = OriginalTitle,
 362632                ProviderIds = ProviderIds,
 362633                IndexNumber = IndexNumber,
 362634                ParentIndexNumber = ParentIndexNumber,
 362635                Year = ProductionYear,
 362636                PremiereDate = PremiereDate
 362637            };
 2638        }
 2639
 2640        protected virtual string GetNameForMetadataLookup()
 2641        {
 362642            return Name;
 2643        }
 2644
 2645        /// <summary>
 2646        /// This is called before any metadata refresh and returns true if changes were made.
 2647        /// </summary>
 2648        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2649        /// <returns>true if the item has change, else false.</returns>
 2650        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2651        {
 362652            _sortName = null;
 2653
 362654            var hasChanges = false;
 2655
 362656            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2657            {
 02658                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02659                hasChanges = true;
 2660            }
 2661
 362662            return hasChanges;
 2663        }
 2664
 2665        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2666        {
 02667            if (protocol == MediaProtocol.File)
 2668            {
 02669                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2670            }
 2671
 02672            return path;
 2673        }
 2674
 2675        public virtual void FillUserDataDtoValues(
 2676            UserItemDataDto dto,
 2677            UserItemData userData,
 2678            BaseItemDto itemDto,
 2679            User user,
 2680            DtoOptions fields,
 2681            (int Played, int Total)? precomputedCounts = null)
 2682        {
 02683            if (RunTimeTicks.HasValue)
 2684            {
 02685                double pct = RunTimeTicks.Value;
 2686
 02687                if (pct > 0)
 2688                {
 02689                    pct = userData.PlaybackPositionTicks / pct;
 2690
 02691                    if (pct > 0)
 2692                    {
 02693                        dto.PlayedPercentage = 100 * pct;
 2694                    }
 2695                }
 2696            }
 02697        }
 2698
 2699        /// <summary>
 2700        /// Applies the owner's premiere date and production year to an owned item, returning whether anything changed.
 2701        /// </summary>
 2702        /// <param name="owner">The owner.</param>
 2703        /// <param name="ownedItem">The owned item.</param>
 2704        /// <returns><c>true</c> if the owned item was changed, else <c>false</c>.</returns>
 2705        internal static bool InheritDatesFromOwner(BaseItem owner, BaseItem ownedItem)
 2706        {
 2707            // Extras have no release date of their own, so the owner's is authoritative.
 42708            var changed = false;
 2709
 42710            if (owner.ProductionYear is not null && ownedItem.ProductionYear != owner.ProductionYear)
 2711            {
 22712                ownedItem.ProductionYear = owner.ProductionYear;
 22713                changed = true;
 2714            }
 2715
 42716            if (owner.PremiereDate is not null && ownedItem.PremiereDate != owner.PremiereDate)
 2717            {
 22718                ownedItem.PremiereDate = owner.PremiereDate;
 22719                changed = true;
 2720            }
 2721
 42722            return changed;
 2723        }
 2724
 2725        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2726        {
 02727            var newOptions = new MetadataRefreshOptions(options)
 02728            {
 02729                SearchResult = null
 02730            };
 2731
 02732            var item = this;
 2733
 02734            if (copyTitleMetadata)
 2735            {
 2736                // Take some data from the main item, for querying purposes
 02737                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2738                {
 02739                    newOptions.ForceSave = true;
 02740                    ownedItem.Genres = item.Genres;
 2741                }
 2742
 02743                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2744                {
 02745                    newOptions.ForceSave = true;
 02746                    ownedItem.Studios = item.Studios;
 2747                }
 2748
 02749                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2750                {
 02751                    newOptions.ForceSave = true;
 02752                    ownedItem.ProductionLocations = item.ProductionLocations;
 2753                }
 2754
 02755                if (item.CommunityRating != ownedItem.CommunityRating)
 2756                {
 02757                    ownedItem.CommunityRating = item.CommunityRating;
 02758                    newOptions.ForceSave = true;
 2759                }
 2760
 02761                if (item.CriticRating != ownedItem.CriticRating)
 2762                {
 02763                    ownedItem.CriticRating = item.CriticRating;
 02764                    newOptions.ForceSave = true;
 2765                }
 2766
 02767                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2768                {
 02769                    ownedItem.Overview = item.Overview;
 02770                    newOptions.ForceSave = true;
 2771                }
 2772
 02773                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2774                {
 02775                    ownedItem.OfficialRating = item.OfficialRating;
 02776                    newOptions.ForceSave = true;
 2777                }
 2778
 02779                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2780                {
 02781                    ownedItem.CustomRating = item.CustomRating;
 02782                    newOptions.ForceSave = true;
 2783                }
 2784
 02785                if (InheritDatesFromOwner(item, ownedItem))
 2786                {
 02787                    newOptions.ForceSave = true;
 2788                }
 2789            }
 2790
 02791            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 02792        }
 2793
 2794        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2795        {
 02796            var newOptions = new MetadataRefreshOptions(options)
 02797            {
 02798                SearchResult = null
 02799            };
 2800
 02801            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2802
 2803            // Try to retrieve it from the db. If we don't find it, use the resolved version
 02804            if (LibraryManager.GetItemById(id) is not Video video)
 2805            {
 02806                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2807
 02808                newOptions.ForceSave = true;
 2809            }
 2810
 02811            if (video is null)
 2812            {
 02813                return;
 2814            }
 2815
 02816            if (video.OwnerId.IsEmpty())
 2817            {
 02818                video.OwnerId = Id;
 2819            }
 2820
 02821            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 02822        }
 2823
 2824        public string GetEtag(User user)
 2825        {
 62826            var list = GetEtagValues(user);
 2827
 62828            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2829        }
 2830
 2831        protected virtual List<string> GetEtagValues(User user)
 2832        {
 62833            return
 62834            [
 62835                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62836            ];
 2837        }
 2838
 2839        public virtual IEnumerable<Guid> GetAncestorIds()
 2840        {
 1162841            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2842        }
 2843
 2844        public BaseItem GetTopParent()
 2845        {
 2002846            if (IsTopParent)
 2847            {
 222848                return this;
 2849            }
 2850
 1782851            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2852        }
 2853
 2854        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2855        {
 452856            return [Id];
 2857        }
 2858
 2859        public virtual double? GetRefreshProgress()
 2860        {
 02861            return null;
 2862        }
 2863
 2864        public virtual ItemUpdateType OnMetadataChanged()
 2865        {
 1222866            var updateType = ItemUpdateType.None;
 2867
 1222868            var item = this;
 2869
 1222870            var rating = item.GetParentalRatingScore();
 1222871            if (rating is not null)
 2872            {
 02873                if (rating.Score != item.InheritedParentalRatingValue)
 2874                {
 02875                    item.InheritedParentalRatingValue = rating.Score;
 02876                    updateType |= ItemUpdateType.MetadataImport;
 2877                }
 2878
 02879                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2880                {
 02881                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02882                    updateType |= ItemUpdateType.MetadataImport;
 2883                }
 2884            }
 2885            else
 2886            {
 1222887                if (item.InheritedParentalRatingValue is not null)
 2888                {
 02889                    item.InheritedParentalRatingValue = null;
 02890                    item.InheritedParentalRatingSubValue = null;
 02891                    updateType |= ItemUpdateType.MetadataImport;
 2892                }
 2893            }
 2894
 1222895            return updateType;
 2896        }
 2897
 2898        /// <summary>
 2899        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2900        /// </summary>
 2901        /// <param name="children">Media children.</param>
 2902        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2903        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2904        {
 02905            var currentOfficialRating = OfficialRating;
 2906
 2907            // Gather all possible ratings
 02908            var ratings = children
 02909                .Select(i => i.OfficialRating)
 02910                .Where(i => !string.IsNullOrEmpty(i))
 02911                .Distinct(StringComparer.OrdinalIgnoreCase)
 02912                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())
 02913                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02914                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02915                .Select(i => i.rating);
 2916
 02917            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2918
 02919            return !string.Equals(
 02920                currentOfficialRating ?? string.Empty,
 02921                OfficialRating ?? string.Empty,
 02922                StringComparison.OrdinalIgnoreCase);
 2923        }
 2924
 2925        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2926        {
 02927            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2928        }
 2929
 2930        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2931        {
 02932            return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), us
 2933        }
 2934
 2935        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2936        {
 02937            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2938        }
 2939
 2940        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2941        {
 02942            return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), u
 2943        }
 2944
 2945        /// <summary>
 2946        /// Gets the ids of the items whose owned extras belong to this item.
 2947        /// </summary>
 2948        /// <returns>An array containing the owner ids.</returns>
 2949        protected virtual Guid[] GetExtraOwnerIds()
 2950        {
 62951            return [Id];
 2952        }
 2953
 2954        /// <summary>
 2955        /// Gets the ids of this item and the versions of it whose extras it maintains.
 2956        /// </summary>
 2957        /// <returns>An array containing the version ids.</returns>
 2958        protected virtual Guid[] GetOwnedVersionIds()
 2959        {
 02960            return [Id];
 2961        }
 2962
 2963        /// <summary>
 2964        /// Gets the id of the version an extra belongs to.
 2965        /// </summary>
 2966        /// <param name="extra">The extra.</param>
 2967        /// <returns>The id of the owning version.</returns>
 2968        protected virtual Guid GetOwnerIdForExtra(BaseItem extra)
 2969        {
 02970            return Id;
 2971        }
 2972
 2973        /// <summary>
 2974        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2975        /// </summary>
 2976        /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</
 2977        /// <returns>An enumerable containing the items.</returns>
 2978        public IEnumerable<BaseItem> GetExtras(User user = null)
 2979        {
 62980            return LibraryManager.GetItemList(new InternalItemsQuery(user)
 62981            {
 62982                OwnerIds = GetExtraOwnerIds(),
 62983                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 62984            });
 2985        }
 2986
 2987        /// <summary>
 2988        /// Get all extras with specific types that are associated with this item.
 2989        /// </summary>
 2990        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2991        /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</
 2992        /// <returns>An enumerable containing the extras.</returns>
 2993        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes, User user = null)
 2994        {
 02995            return LibraryManager.GetItemList(new InternalItemsQuery(user)
 02996            {
 02997                OwnerIds = GetExtraOwnerIds(),
 02998                ExtraTypes = extraTypes.ToArray(),
 02999                OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
 03000            });
 3001        }
 3002
 3003        public virtual long GetRunTimeTicksForPlayState()
 3004        {
 03005            return RunTimeTicks ?? 0;
 3006        }
 3007
 3008        /// <inheritdoc />
 3009        public override bool Equals(object obj)
 3010        {
 1383011            return obj is BaseItem baseItem && this.Equals(baseItem);
 3012        }
 3013
 3014        /// <inheritdoc />
 2963015        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 3016
 3017        /// <inheritdoc />
 1023018        public override int GetHashCode() => HashCode.Combine(Id);
 3019    }
 3020}

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_SearchesContainingFolderForExtras()
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()
GetSortName(System.String,System.Boolean,MediaBrowser.Model.Configuration.ServerConfiguration)
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>>)
InheritDatesFromOwner(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
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()
GetOwnedVersionIds()
GetOwnerIdForExtra(MediaBrowser.Controller.Entities.BaseItem)
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()