< Summary - Jellyfin

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

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/18/2025 - 12:09:37 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/24/2025 - 12:11:19 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26678/25/2025 - 12:11:25 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/26/2025 - 12:09:43 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26678/27/2025 - 12:10:55 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/28/2025 - 12:10:57 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26678/29/2025 - 12:09:55 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/30/2025 - 12:09:58 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26679/2/2025 - 12:10:53 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26679/5/2025 - 12:11:19 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26679/7/2025 - 12:11:13 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26679/13/2025 - 12:11:04 AM Line coverage: 51.5% (376/729) Branch coverage: 42% (190/452) Total lines: 26759/15/2025 - 12:10:55 AM Line coverage: 51.5% (376/729) Branch coverage: 41.4% (190/458) Total lines: 26759/17/2025 - 12:11:23 AM Line coverage: 51.6% (377/730) Branch coverage: 41.4% (190/458) Total lines: 26829/27/2025 - 12:11:20 AM Line coverage: 51.6% (377/730) Branch coverage: 41.2% (189/458) Total lines: 26829/28/2025 - 12:11:28 AM Line coverage: 51.6% (377/730) Branch coverage: 41.4% (190/458) Total lines: 268210/12/2025 - 12:11:07 AM Line coverage: 51.6% (377/730) Branch coverage: 41.2% (189/458) Total lines: 268210/27/2025 - 12:11:33 AM Line coverage: 51.6% (377/730) Branch coverage: 41% (188/458) Total lines: 268210/28/2025 - 12:11:27 AM Line coverage: 51.5% (376/730) Branch coverage: 41% (188/458) Total lines: 268211/18/2025 - 12:11:25 AM Line coverage: 51.2% (376/733) Branch coverage: 40.3% (188/466) Total lines: 2692 8/18/2025 - 12:09:37 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/24/2025 - 12:11:19 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26678/25/2025 - 12:11:25 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/26/2025 - 12:09:43 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26678/27/2025 - 12:10:55 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/28/2025 - 12:10:57 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26678/29/2025 - 12:09:55 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26678/30/2025 - 12:09:58 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26679/2/2025 - 12:10:53 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26679/5/2025 - 12:11:19 AM Line coverage: 51.3% (373/726) Branch coverage: 41.2% (185/448) Total lines: 26679/7/2025 - 12:11:13 AM Line coverage: 51.3% (373/726) Branch coverage: 41.5% (186/448) Total lines: 26679/13/2025 - 12:11:04 AM Line coverage: 51.5% (376/729) Branch coverage: 42% (190/452) Total lines: 26759/15/2025 - 12:10:55 AM Line coverage: 51.5% (376/729) Branch coverage: 41.4% (190/458) Total lines: 26759/17/2025 - 12:11:23 AM Line coverage: 51.6% (377/730) Branch coverage: 41.4% (190/458) Total lines: 26829/27/2025 - 12:11:20 AM Line coverage: 51.6% (377/730) Branch coverage: 41.2% (189/458) Total lines: 26829/28/2025 - 12:11:28 AM Line coverage: 51.6% (377/730) Branch coverage: 41.4% (190/458) Total lines: 268210/12/2025 - 12:11:07 AM Line coverage: 51.6% (377/730) Branch coverage: 41.2% (189/458) Total lines: 268210/27/2025 - 12:11:33 AM Line coverage: 51.6% (377/730) Branch coverage: 41% (188/458) Total lines: 268210/28/2025 - 12:11:27 AM Line coverage: 51.5% (376/730) Branch coverage: 41% (188/458) Total lines: 268211/18/2025 - 12:11:25 AM Line coverage: 51.2% (376/733) Branch coverage: 40.3% (188/466) Total lines: 2692

Metrics

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

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591, SA1401
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.Globalization;
 9using System.IO;
 10using System.Linq;
 11using System.Text;
 12using System.Text.Json.Serialization;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Common.Extensions;
 21using MediaBrowser.Controller.Channels;
 22using MediaBrowser.Controller.Chapters;
 23using MediaBrowser.Controller.Configuration;
 24using MediaBrowser.Controller.Dto;
 25using MediaBrowser.Controller.Entities.Audio;
 26using MediaBrowser.Controller.Entities.TV;
 27using MediaBrowser.Controller.IO;
 28using MediaBrowser.Controller.Library;
 29using MediaBrowser.Controller.MediaSegments;
 30using MediaBrowser.Controller.Persistence;
 31using MediaBrowser.Controller.Providers;
 32using MediaBrowser.Model.Dto;
 33using MediaBrowser.Model.Entities;
 34using MediaBrowser.Model.Globalization;
 35using MediaBrowser.Model.IO;
 36using MediaBrowser.Model.Library;
 37using MediaBrowser.Model.LiveTv;
 38using MediaBrowser.Model.MediaInfo;
 39using Microsoft.Extensions.Logging;
 40
 41namespace MediaBrowser.Controller.Entities
 42{
 43    /// <summary>
 44    /// Class BaseItem.
 45    /// </summary>
 46    public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
 47    {
 48        private BaseItemKind? _baseItemKind;
 49
 50        public const string ThemeSongFileName = "theme";
 51
 52        /// <summary>
 53        /// The supported image extensions.
 54        /// </summary>
 455        public static readonly string[] SupportedImageExtensions
 456            = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
 57
 458        private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
 459        {
 460            ".nfo",
 461            ".xml",
 462            ".srt",
 463            ".vtt",
 464            ".sub",
 465            ".sup",
 466            ".idx",
 467            ".txt",
 468            ".edl",
 469            ".bif",
 470            ".smi",
 471            ".ttml",
 472            ".lrc",
 473            ".elrc"
 474        };
 75
 76        /// <summary>
 77        /// Extra types that should be counted and displayed as "Special Features" in the UI.
 78        /// </summary>
 479        public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
 480        {
 481            Model.Entities.ExtraType.Unknown,
 482            Model.Entities.ExtraType.BehindTheScenes,
 483            Model.Entities.ExtraType.Clip,
 484            Model.Entities.ExtraType.DeletedScene,
 485            Model.Entities.ExtraType.Interview,
 486            Model.Entities.ExtraType.Sample,
 487            Model.Entities.ExtraType.Scene,
 488            Model.Entities.ExtraType.Featurette,
 489            Model.Entities.ExtraType.Short
 490        };
 91
 92        private string _sortName;
 93
 94        private string _forcedSortName;
 95
 96        private string _name;
 97
 98        public const char SlugChar = '-';
 99
 100        protected BaseItem()
 101        {
 962102            Tags = Array.Empty<string>();
 962103            Genres = Array.Empty<string>();
 962104            Studios = Array.Empty<string>();
 962105            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 962106            LockedFields = Array.Empty<MetadataField>();
 962107            ImageInfos = Array.Empty<ItemImageInfo>();
 962108            ProductionLocations = Array.Empty<string>();
 962109            RemoteTrailers = Array.Empty<MediaUrl>();
 962110            ExtraIds = Array.Empty<Guid>();
 962111            UserData = [];
 962112        }
 113
 114        /// <summary>
 115        /// Gets or Sets the user data collection as cached from the last Db query.
 116        /// </summary>
 117        [JsonIgnore]
 118        public ICollection<UserData> UserData { get; set; }
 119
 120        [JsonIgnore]
 121        public string PreferredMetadataCountryCode { get; set; }
 122
 123        [JsonIgnore]
 124        public string PreferredMetadataLanguage { get; set; }
 125
 126        public long? Size { get; set; }
 127
 128        public string Container { get; set; }
 129
 130        [JsonIgnore]
 131        public string Tagline { get; set; }
 132
 133        [JsonIgnore]
 134        public virtual ItemImageInfo[] ImageInfos { get; set; }
 135
 136        [JsonIgnore]
 137        public bool IsVirtualItem { get; set; }
 138
 139        /// <summary>
 140        /// Gets or sets the album.
 141        /// </summary>
 142        /// <value>The album.</value>
 143        [JsonIgnore]
 144        public string Album { get; set; }
 145
 146        /// <summary>
 147        /// Gets or sets the LUFS value.
 148        /// </summary>
 149        /// <value>The LUFS Value.</value>
 150        [JsonIgnore]
 151        public float? LUFS { get; set; }
 152
 153        /// <summary>
 154        /// Gets or sets the gain required for audio normalization.
 155        /// </summary>
 156        /// <value>The gain required for audio normalization.</value>
 157        [JsonIgnore]
 158        public float? NormalizationGain { get; set; }
 159
 160        /// <summary>
 161        /// Gets or sets the channel identifier.
 162        /// </summary>
 163        /// <value>The channel identifier.</value>
 164        [JsonIgnore]
 165        public Guid ChannelId { get; set; }
 166
 167        [JsonIgnore]
 0168        public virtual bool SupportsAddingToPlaylist => false;
 169
 170        [JsonIgnore]
 13171        public virtual bool AlwaysScanInternalMetadataPath => false;
 172
 173        /// <summary>
 174        /// Gets or sets a value indicating whether this instance is in mixed folder.
 175        /// </summary>
 176        /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
 177        [JsonIgnore]
 178        public bool IsInMixedFolder { get; set; }
 179
 180        [JsonIgnore]
 0181        public virtual bool SupportsPlayedStatus => false;
 182
 183        [JsonIgnore]
 0184        public virtual bool SupportsPositionTicksResume => false;
 185
 186        [JsonIgnore]
 17187        public virtual bool SupportsRemoteImageDownloading => true;
 188
 189        /// <summary>
 190        /// Gets or sets the name.
 191        /// </summary>
 192        /// <value>The name.</value>
 193        [JsonIgnore]
 194        public virtual string Name
 195        {
 945196            get => _name;
 197            set
 198            {
 338199                _name = value;
 200
 201                // lazy load this again
 338202                _sortName = null;
 338203            }
 204        }
 205
 206        [JsonIgnore]
 0207        public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date;
 208
 209        [JsonIgnore]
 210        public int? TotalBitrate { get; set; }
 211
 212        [JsonIgnore]
 213        public ExtraType? ExtraType { get; set; }
 214
 215        [JsonIgnore]
 0216        public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || Extr
 217
 218        [JsonIgnore]
 219        public string OriginalTitle { get; set; }
 220
 221        /// <summary>
 222        /// Gets or sets the id.
 223        /// </summary>
 224        /// <value>The id.</value>
 225        [JsonIgnore]
 226        public Guid Id { get; set; }
 227
 228        [JsonIgnore]
 229        public Guid OwnerId { get; set; }
 230
 231        /// <summary>
 232        /// Gets or sets the audio.
 233        /// </summary>
 234        /// <value>The audio.</value>
 235        [JsonIgnore]
 236        public ProgramAudio? Audio { get; set; }
 237
 238        /// <summary>
 239        /// Gets the id that should be used to key display prefs for this item.
 240        /// Default is based on the type for everything except actual generic folders.
 241        /// </summary>
 242        /// <value>The display prefs id.</value>
 243        [JsonIgnore]
 244        public virtual Guid DisplayPreferencesId
 245        {
 246            get
 247            {
 6248                var thisType = GetType();
 6249                return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
 250            }
 251        }
 252
 253        /// <summary>
 254        /// Gets or sets the path.
 255        /// </summary>
 256        /// <value>The path.</value>
 257        [JsonIgnore]
 258        public virtual string Path { get; set; }
 259
 260        [JsonIgnore]
 261        public virtual SourceType SourceType
 262        {
 263            get
 264            {
 1850265                if (!ChannelId.IsEmpty())
 266                {
 0267                    return SourceType.Channel;
 268                }
 269
 1850270                return SourceType.Library;
 271            }
 272        }
 273
 274        /// <summary>
 275        /// Gets the folder containing the item.
 276        /// If the item is a folder, it returns the folder itself.
 277        /// </summary>
 278        [JsonIgnore]
 279        public virtual string ContainingFolderPath
 280        {
 281            get
 282            {
 425283                if (IsFolder)
 284                {
 374285                    return Path;
 286                }
 287
 51288                return System.IO.Path.GetDirectoryName(Path);
 289            }
 290        }
 291
 292        /// <summary>
 293        /// Gets or sets the name of the service.
 294        /// </summary>
 295        /// <value>The name of the service.</value>
 296        [JsonIgnore]
 297        public string ServiceName { get; set; }
 298
 299        /// <summary>
 300        /// Gets or sets the external id.
 301        /// </summary>
 302        /// <remarks>
 303        /// If this content came from an external service, the id of the content on that service.
 304        /// </remarks>
 305        [JsonIgnore]
 306        public string ExternalId { get; set; }
 307
 308        [JsonIgnore]
 309        public string ExternalSeriesId { get; set; }
 310
 311        [JsonIgnore]
 0312        public virtual bool IsHidden => false;
 313
 314        /// <summary>
 315        /// Gets the type of the location.
 316        /// </summary>
 317        /// <value>The type of the location.</value>
 318        [JsonIgnore]
 319        public virtual LocationType LocationType
 320        {
 321            get
 322            {
 9323                var path = Path;
 9324                if (string.IsNullOrEmpty(path))
 325                {
 0326                    if (SourceType == SourceType.Channel)
 327                    {
 0328                        return LocationType.Remote;
 329                    }
 330
 0331                    return LocationType.Virtual;
 332                }
 333
 9334                return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
 335            }
 336        }
 337
 338        [JsonIgnore]
 339        public MediaProtocol? PathProtocol
 340        {
 341            get
 342            {
 2364343                var path = Path;
 344
 2364345                if (string.IsNullOrEmpty(path))
 346                {
 116347                    return null;
 348                }
 349
 2248350                return MediaSourceManager.GetPathProtocol(path);
 351            }
 352        }
 353
 354        [JsonIgnore]
 2343355        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 356
 357        [JsonIgnore]
 0358        public bool HasPathProtocol => PathProtocol.HasValue;
 359
 360        [JsonIgnore]
 361        public virtual bool SupportsLocalMetadata
 362        {
 363            get
 364            {
 1220365                if (SourceType == SourceType.Channel)
 366                {
 0367                    return false;
 368                }
 369
 1220370                return IsFileProtocol;
 371            }
 372        }
 373
 374        [JsonIgnore]
 375        public virtual string FileNameWithoutExtension
 376        {
 377            get
 378            {
 0379                if (IsFileProtocol)
 380                {
 0381                    return System.IO.Path.GetFileNameWithoutExtension(Path);
 382                }
 383
 0384                return null;
 385            }
 386        }
 387
 388        [JsonIgnore]
 101389        public virtual bool EnableAlphaNumericSorting => true;
 390
 82391        public virtual bool IsHD => Height >= 720;
 392
 393        public bool IsShortcut { get; set; }
 394
 395        public string ShortcutPath { get; set; }
 396
 397        public int Width { get; set; }
 398
 399        public int Height { get; set; }
 400
 401        public Guid[] ExtraIds { get; set; }
 402
 403        /// <summary>
 404        /// Gets the primary image path.
 405        /// </summary>
 406        /// <remarks>
 407        /// This is just a helper for convenience.
 408        /// </remarks>
 409        /// <value>The primary image path.</value>
 410        [JsonIgnore]
 0411        public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
 412
 413        /// <summary>
 414        /// Gets or sets the date created.
 415        /// </summary>
 416        /// <value>The date created.</value>
 417        [JsonIgnore]
 418        public DateTime DateCreated { get; set; }
 419
 420        /// <summary>
 421        /// Gets or sets the date modified.
 422        /// </summary>
 423        /// <value>The date modified.</value>
 424        [JsonIgnore]
 425        public DateTime DateModified { get; set; }
 426
 427        public DateTime DateLastSaved { get; set; }
 428
 429        [JsonIgnore]
 430        public DateTime DateLastRefreshed { get; set; }
 431
 432        [JsonIgnore]
 433        public bool IsLocked { get; set; }
 434
 435        /// <summary>
 436        /// Gets or sets the locked fields.
 437        /// </summary>
 438        /// <value>The locked fields.</value>
 439        [JsonIgnore]
 440        public MetadataField[] LockedFields { get; set; }
 441
 442        /// <summary>
 443        /// Gets the type of the media.
 444        /// </summary>
 445        /// <value>The type of the media.</value>
 446        [JsonIgnore]
 88447        public virtual MediaType MediaType => MediaType.Unknown;
 448
 449        [JsonIgnore]
 450        public virtual string[] PhysicalLocations
 451        {
 452            get
 453            {
 0454                if (!IsFileProtocol)
 455                {
 0456                    return Array.Empty<string>();
 457                }
 458
 0459                return [Path];
 460            }
 461        }
 462
 463        [JsonIgnore]
 464        public bool EnableMediaSourceDisplay
 465        {
 466            get
 467            {
 6468                if (SourceType == SourceType.Channel)
 469                {
 0470                    return ChannelManager.EnableMediaSourceDisplay(this);
 471                }
 472
 6473                return true;
 474            }
 475        }
 476
 477        [JsonIgnore]
 478        public Guid ParentId { get; set; }
 479
 480        /// <summary>
 481        /// Gets or sets the logger.
 482        /// </summary>
 483        public static ILogger<BaseItem> Logger { get; set; }
 484
 485        public static ILibraryManager LibraryManager { get; set; }
 486
 487        public static IServerConfigurationManager ConfigurationManager { get; set; }
 488
 489        public static IProviderManager ProviderManager { get; set; }
 490
 491        public static ILocalizationManager LocalizationManager { get; set; }
 492
 493        public static IItemRepository ItemRepository { get; set; }
 494
 495        public static IChapterManager ChapterManager { get; set; }
 496
 497        public static IFileSystem FileSystem { get; set; }
 498
 499        public static IUserDataManager UserDataManager { get; set; }
 500
 501        public static IChannelManager ChannelManager { get; set; }
 502
 503        public static IMediaSourceManager MediaSourceManager { get; set; }
 504
 505        public static IMediaSegmentManager MediaSegmentManager { get; set; }
 506
 507        /// <summary>
 508        /// Gets or sets the name of the forced sort.
 509        /// </summary>
 510        /// <value>The name of the forced sort.</value>
 511        [JsonIgnore]
 512        public string ForcedSortName
 513        {
 431514            get => _forcedSortName;
 515            set
 516            {
 86517                _forcedSortName = value;
 86518                _sortName = null;
 86519            }
 520        }
 521
 522        /// <summary>
 523        /// Gets or sets the name of the sort.
 524        /// </summary>
 525        /// <value>The name of the sort.</value>
 526        [JsonIgnore]
 527        public string SortName
 528        {
 529            get
 530            {
 128531                if (_sortName is null)
 532                {
 101533                    if (!string.IsNullOrEmpty(ForcedSortName))
 534                    {
 535                        // Need the ToLower because that's what CreateSortName does
 0536                        _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
 537                    }
 538                    else
 539                    {
 101540                        _sortName = CreateSortName();
 541                    }
 542                }
 543
 128544                return _sortName;
 545            }
 546
 115547            set => _sortName = value;
 548        }
 549
 550        [JsonIgnore]
 338551        public virtual Guid DisplayParentId => ParentId;
 552
 553        [JsonIgnore]
 554        public BaseItem DisplayParent
 555        {
 556            get
 557            {
 332558                var id = DisplayParentId;
 332559                if (id.IsEmpty())
 560                {
 254561                    return null;
 562                }
 563
 78564                return LibraryManager.GetItemById(id);
 565            }
 566        }
 567
 568        /// <summary>
 569        /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be
 570        /// </summary>
 571        /// <value>The premiere date.</value>
 572        [JsonIgnore]
 573        public DateTime? PremiereDate { get; set; }
 574
 575        /// <summary>
 576        /// Gets or sets the end date.
 577        /// </summary>
 578        /// <value>The end date.</value>
 579        [JsonIgnore]
 580        public DateTime? EndDate { get; set; }
 581
 582        /// <summary>
 583        /// Gets or sets the official rating.
 584        /// </summary>
 585        /// <value>The official rating.</value>
 586        [JsonIgnore]
 587        public string OfficialRating { get; set; }
 588
 589        [JsonIgnore]
 590        public int? InheritedParentalRatingValue { get; set; }
 591
 592        [JsonIgnore]
 593        public int? InheritedParentalRatingSubValue { get; set; }
 594
 595        /// <summary>
 596        /// Gets or sets the critic rating.
 597        /// </summary>
 598        /// <value>The critic rating.</value>
 599        [JsonIgnore]
 600        public float? CriticRating { get; set; }
 601
 602        /// <summary>
 603        /// Gets or sets the custom rating.
 604        /// </summary>
 605        /// <value>The custom rating.</value>
 606        [JsonIgnore]
 607        public string CustomRating { get; set; }
 608
 609        /// <summary>
 610        /// Gets or sets the overview.
 611        /// </summary>
 612        /// <value>The overview.</value>
 613        [JsonIgnore]
 614        public string Overview { get; set; }
 615
 616        /// <summary>
 617        /// Gets or sets the studios.
 618        /// </summary>
 619        /// <value>The studios.</value>
 620        [JsonIgnore]
 621        public string[] Studios { get; set; }
 622
 623        /// <summary>
 624        /// Gets or sets the genres.
 625        /// </summary>
 626        /// <value>The genres.</value>
 627        [JsonIgnore]
 628        public string[] Genres { get; set; }
 629
 630        /// <summary>
 631        /// Gets or sets the tags.
 632        /// </summary>
 633        /// <value>The tags.</value>
 634        [JsonIgnore]
 635        public string[] Tags { get; set; }
 636
 637        [JsonIgnore]
 638        public string[] ProductionLocations { get; set; }
 639
 640        /// <summary>
 641        /// Gets or sets the home page URL.
 642        /// </summary>
 643        /// <value>The home page URL.</value>
 644        [JsonIgnore]
 645        public string HomePageUrl { get; set; }
 646
 647        /// <summary>
 648        /// Gets or sets the community rating.
 649        /// </summary>
 650        /// <value>The community rating.</value>
 651        [JsonIgnore]
 652        public float? CommunityRating { get; set; }
 653
 654        /// <summary>
 655        /// Gets or sets the run time ticks.
 656        /// </summary>
 657        /// <value>The run time ticks.</value>
 658        [JsonIgnore]
 659        public long? RunTimeTicks { get; set; }
 660
 661        /// <summary>
 662        /// Gets or sets the production year.
 663        /// </summary>
 664        /// <value>The production year.</value>
 665        [JsonIgnore]
 666        public int? ProductionYear { get; set; }
 667
 668        /// <summary>
 669        /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
 670        /// This could be episode number, album track number, etc.
 671        /// </summary>
 672        /// <value>The index number.</value>
 673        [JsonIgnore]
 674        public int? IndexNumber { get; set; }
 675
 676        /// <summary>
 677        /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this cou
 678        /// </summary>
 679        /// <value>The parent index number.</value>
 680        [JsonIgnore]
 681        public int? ParentIndexNumber { get; set; }
 682
 683        [JsonIgnore]
 0684        public virtual bool HasLocalAlternateVersions => false;
 685
 686        [JsonIgnore]
 687        public string OfficialRatingForComparison
 688        {
 689            get
 690            {
 166691                var officialRating = OfficialRating;
 166692                if (!string.IsNullOrEmpty(officialRating))
 693                {
 0694                    return officialRating;
 695                }
 696
 166697                var parent = DisplayParent;
 166698                if (parent is not null)
 699                {
 39700                    return parent.OfficialRatingForComparison;
 701                }
 702
 127703                return null;
 704            }
 705        }
 706
 707        [JsonIgnore]
 708        public string CustomRatingForComparison
 709        {
 710            get
 711            {
 127712                return GetCustomRatingForComparision();
 713            }
 714        }
 715
 716        /// <summary>
 717        /// Gets or sets the provider ids.
 718        /// </summary>
 719        /// <value>The provider ids.</value>
 720        [JsonIgnore]
 721        public Dictionary<string, string> ProviderIds { get; set; }
 722
 723        [JsonIgnore]
 0724        public virtual Folder LatestItemsIndexContainer => null;
 725
 726        [JsonIgnore]
 727        public string PresentationUniqueKey { get; set; }
 728
 729        [JsonIgnore]
 0730        public virtual bool EnableRememberingTrackSelections => true;
 731
 732        [JsonIgnore]
 733        public virtual bool IsTopParent
 734        {
 735            get
 736            {
 242737                if (this is BasePluginFolder || this is Channel)
 738                {
 59739                    return true;
 740                }
 741
 183742                if (this is IHasCollectionType view)
 743                {
 11744                    if (view.CollectionType == CollectionType.livetv)
 745                    {
 0746                        return true;
 747                    }
 748                }
 749
 183750                if (GetParent() is AggregateFolder)
 751                {
 0752                    return true;
 753                }
 754
 183755                return false;
 756            }
 757        }
 758
 759        [JsonIgnore]
 158760        public virtual bool SupportsAncestors => true;
 761
 762        [JsonIgnore]
 92763        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 764
 765        [JsonIgnore]
 39766        public virtual bool SupportsPeople => false;
 767
 768        [JsonIgnore]
 0769        public virtual bool SupportsThemeMedia => false;
 770
 771        [JsonIgnore]
 0772        public virtual bool SupportsInheritedParentImages => false;
 773
 774        /// <summary>
 775        /// Gets a value indicating whether this instance is folder.
 776        /// </summary>
 777        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 778        [JsonIgnore]
 71779        public virtual bool IsFolder => false;
 780
 781        [JsonIgnore]
 0782        public virtual bool IsDisplayedAsFolder => false;
 783
 784        /// <summary>
 785        /// Gets or sets the remote trailers.
 786        /// </summary>
 787        /// <value>The remote trailers.</value>
 788        public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
 789
 790        private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
 791        {
 166792            callstack ??= new();
 166793            var customRating = CustomRating;
 166794            if (!string.IsNullOrEmpty(customRating))
 795            {
 0796                return customRating;
 797            }
 798
 166799            callstack.Add(Id);
 800
 166801            var parent = DisplayParent;
 166802            if (parent is not null && !callstack.Contains(parent.Id))
 803            {
 39804                return parent.GetCustomRatingForComparision(callstack);
 805            }
 806
 127807            return null;
 808        }
 809
 810        public virtual double GetDefaultPrimaryImageAspectRatio()
 811        {
 0812            return 0;
 813        }
 814
 815        public virtual string CreatePresentationUniqueKey()
 816        {
 57817            return Id.ToString("N", CultureInfo.InvariantCulture);
 818        }
 819
 820        public virtual bool CanDelete()
 821        {
 0822            if (SourceType == SourceType.Channel)
 823            {
 0824                return ChannelManager.CanDelete(this);
 825            }
 826
 0827            return IsFileProtocol;
 828        }
 829
 830        public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
 831        {
 0832            if (user.HasPermission(PermissionKind.EnableContentDeletion))
 833            {
 0834                return true;
 835            }
 836
 0837            var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
 838
 0839            if (SourceType == SourceType.Channel)
 840            {
 0841                return allowed.Contains(ChannelId);
 842            }
 843
 0844            var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
 845
 0846            foreach (var folder in collectionFolders)
 847            {
 0848                if (allowed.Contains(folder.Id))
 849                {
 0850                    return true;
 851                }
 852            }
 853
 0854            return false;
 0855        }
 856
 857        public BaseItem GetOwner()
 858        {
 606859            var ownerId = OwnerId;
 606860            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
 861        }
 862
 863        public bool CanDelete(User user, List<Folder> allCollectionFolders)
 864        {
 6865            return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
 866        }
 867
 868        public virtual bool CanDelete(User user)
 869        {
 6870            var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
 871
 6872            return CanDelete(user, allCollectionFolders);
 873        }
 874
 875        public virtual bool CanDownload()
 876        {
 6877            return false;
 878        }
 879
 880        public virtual bool IsAuthorizedToDownload(User user)
 881        {
 0882            return user.HasPermission(PermissionKind.EnableContentDownloading);
 883        }
 884
 885        public bool CanDownload(User user)
 886        {
 6887            return CanDownload() && IsAuthorizedToDownload(user);
 888        }
 889
 890        /// <inheritdoc />
 891        public override string ToString()
 892        {
 69893            return Name;
 894        }
 895
 896        public virtual string GetInternalMetadataPath()
 897        {
 72898            var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
 899
 72900            return GetInternalMetadataPath(basePath);
 901        }
 902
 903        protected virtual string GetInternalMetadataPath(string basePath)
 904        {
 72905            if (SourceType == SourceType.Channel)
 906            {
 0907                return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), 
 908            }
 909
 72910            ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 911
 72912            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
 913        }
 914
 915        /// <summary>
 916        /// Creates the name of the sort.
 917        /// </summary>
 918        /// <returns>System.String.</returns>
 919        protected virtual string CreateSortName()
 920        {
 101921            if (Name is null)
 922            {
 0923                return null; // some items may not have name filled in properly
 924            }
 925
 101926            if (!EnableAlphaNumericSorting)
 927            {
 0928                return Name.TrimStart();
 929            }
 930
 101931            var sortable = Name.Trim().ToLowerInvariant();
 932
 808933            foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
 934            {
 935                // Remove from beginning if a space follows
 303936                if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
 937                {
 0938                    sortable = sortable.Remove(0, search.Length + 1);
 939                }
 940
 941                // Remove from middle if surrounded by spaces
 303942                sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
 943
 944                // Remove from end if preceeded by a space
 303945                if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
 946                {
 0947                    sortable = sortable.Remove(sortable.Length - (search.Length + 1));
 948                }
 949            }
 950
 1414951            foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
 952            {
 606953                sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
 954            }
 955
 808956            foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
 957            {
 303958                sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
 959            }
 960
 101961            return ModifySortChunks(sortable);
 962        }
 963
 964        internal static string ModifySortChunks(ReadOnlySpan<char> name)
 965        {
 966            static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
 967            {
 968                if (isDigitChunk && chunk.Length < 10)
 969                {
 970                    builder.Append('0', 10 - chunk.Length);
 971                }
 972
 973                builder.Append(chunk);
 974            }
 975
 107976            if (name.IsEmpty)
 977            {
 1978                return string.Empty;
 979            }
 980
 106981            var builder = new StringBuilder(name.Length);
 982
 106983            int chunkStart = 0;
 106984            bool isDigitChunk = char.IsDigit(name[0]);
 1684985            for (int i = 0; i < name.Length; i++)
 986            {
 736987                var isDigit = char.IsDigit(name[i]);
 736988                if (isDigit != isDigitChunk)
 989                {
 5990                    AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
 5991                    chunkStart = i;
 5992                    isDigitChunk = isDigit;
 993                }
 994            }
 995
 106996            AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
 997
 998            // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
 106999            var result = builder.ToString().RemoveDiacritics();
 1061000            if (!result.All(char.IsAscii))
 1001            {
 01002                result = result.Transliterated();
 1003            }
 1004
 1061005            return result;
 1006        }
 1007
 1008        public BaseItem GetParent()
 1009        {
 16791010            var parentId = ParentId;
 16791011            if (parentId.IsEmpty())
 1012            {
 13801013                return null;
 1014            }
 1015
 2991016            return LibraryManager.GetItemById(parentId);
 1017        }
 1018
 1019        public IEnumerable<BaseItem> GetParents()
 1020        {
 1021            var parent = GetParent();
 1022
 1023            while (parent is not null)
 1024            {
 1025                yield return parent;
 1026
 1027                parent = parent.GetParent();
 1028            }
 1029        }
 1030
 1031        /// <summary>
 1032        /// Finds a parent of a given type.
 1033        /// </summary>
 1034        /// <typeparam name="T">Type of parent.</typeparam>
 1035        /// <returns>``0.</returns>
 1036        public T FindParent<T>()
 1037            where T : Folder
 1038        {
 01039            foreach (var parent in GetParents())
 1040            {
 01041                if (parent is T item)
 1042                {
 01043                    return item;
 1044                }
 1045            }
 1046
 01047            return null;
 01048        }
 1049
 1050        /// <summary>
 1051        /// Gets the play access.
 1052        /// </summary>
 1053        /// <param name="user">The user.</param>
 1054        /// <returns>PlayAccess.</returns>
 1055        public PlayAccess GetPlayAccess(User user)
 1056        {
 61057            if (!user.HasPermission(PermissionKind.EnableMediaPlayback))
 1058            {
 01059                return PlayAccess.None;
 1060            }
 1061
 1062            // if (!user.IsParentalScheduleAllowed())
 1063            // {
 1064            //    return PlayAccess.None;
 1065            // }
 1066
 61067            return PlayAccess.Full;
 1068        }
 1069
 1070        public virtual IReadOnlyList<MediaStream> GetMediaStreams()
 1071        {
 01072            return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
 01073            {
 01074                ItemId = Id
 01075            });
 1076        }
 1077
 1078        protected virtual bool IsActiveRecording()
 1079        {
 01080            return false;
 1081        }
 1082
 1083        public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 1084        {
 01085            if (SourceType == SourceType.Channel)
 1086            {
 01087                var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
 01088                           .ToList();
 1089
 01090                if (sources.Count > 0)
 1091                {
 01092                    return sources;
 1093                }
 1094            }
 1095
 01096            var list = GetAllItemsForMediaSources();
 01097            var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
 1098
 01099            if (IsActiveRecording())
 1100            {
 01101                foreach (var mediaSource in result)
 1102                {
 01103                    mediaSource.Type = MediaSourceType.Placeholder;
 1104                }
 1105            }
 1106
 01107            return result.OrderBy(i =>
 01108            {
 01109                if (i.VideoType == VideoType.VideoFile)
 01110                {
 01111                    return 0;
 01112                }
 01113
 01114                return 1;
 01115            }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
 01116            .ThenByDescending(i => i, new MediaSourceWidthComparator())
 01117            .ToArray();
 1118        }
 1119
 1120        protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
 1121        {
 01122            return Enumerable.Empty<(BaseItem, MediaSourceType)>();
 1123        }
 1124
 1125        private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
 1126        {
 01127            ArgumentNullException.ThrowIfNull(item);
 1128
 01129            var protocol = item.PathProtocol;
 1130
 1131            // Resolve the item path so everywhere we use the media source it will always point to
 1132            // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
 1133            // path will return null, so it's safe to check for all paths.
 01134            var itemPath = item.Path;
 01135            if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) 
 1136            {
 01137                itemPath = linkInfo.FullName;
 1138            }
 1139
 01140            var info = new MediaSourceInfo
 01141            {
 01142                Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
 01143                Protocol = protocol ?? MediaProtocol.File,
 01144                MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
 01145                MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
 01146                Name = GetMediaSourceName(item),
 01147                Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
 01148                RunTimeTicks = item.RunTimeTicks,
 01149                Container = item.Container,
 01150                Size = item.Size,
 01151                Type = type,
 01152                HasSegments = MediaSegmentManager.IsTypeSupported(item)
 01153                    && (protocol is null or MediaProtocol.File)
 01154                    && MediaSegmentManager.HasSegments(item.Id)
 01155            };
 1156
 01157            if (string.IsNullOrEmpty(info.Path))
 1158            {
 01159                info.Type = MediaSourceType.Placeholder;
 1160            }
 1161
 01162            if (info.Protocol == MediaProtocol.File)
 1163            {
 01164                info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", Cultur
 1165            }
 1166
 01167            var video = item as Video;
 01168            if (video is not null)
 1169            {
 01170                info.IsoType = video.IsoType;
 01171                info.VideoType = video.VideoType;
 01172                info.Video3DFormat = video.Video3DFormat;
 01173                info.Timestamp = video.Timestamp;
 1174
 01175                if (video.IsShortcut)
 1176                {
 01177                    info.IsRemote = true;
 01178                    info.Path = video.ShortcutPath;
 01179                    info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
 1180                }
 1181
 01182                if (string.IsNullOrEmpty(info.Container))
 1183                {
 01184                    if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
 1185                    {
 01186                        if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1187                        {
 01188                            info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1189                        }
 1190                    }
 1191                }
 1192            }
 1193
 01194            if (string.IsNullOrEmpty(info.Container))
 1195            {
 01196                if (protocol.HasValue && protocol.Value == MediaProtocol.File)
 1197                {
 01198                    info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
 1199                }
 1200            }
 1201
 01202            if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
 1203            {
 01204                info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
 1205            }
 1206
 01207            if (video is not null && video.VideoType != VideoType.VideoFile)
 1208            {
 01209                info.SupportsDirectStream = false;
 1210            }
 1211
 01212            info.Bitrate = item.TotalBitrate;
 01213            info.InferTotalBitrate();
 1214
 01215            return info;
 1216        }
 1217
 1218        internal string GetMediaSourceName(BaseItem item)
 1219        {
 41220            var terms = new List<string>();
 1221
 41222            var path = item.Path;
 41223            if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
 1224            {
 41225                var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
 41226                if (HasLocalAlternateVersions)
 1227                {
 41228                    var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
 41229                    if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName,
 1230                    {
 21231                        var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
 21232                        if (!name.IsWhiteSpace())
 1233                        {
 21234                            terms.Add(name.ToString());
 1235                        }
 1236                    }
 1237                }
 1238
 41239                if (terms.Count == 0)
 1240                {
 21241                    terms.Add(displayName);
 1242                }
 1243            }
 1244
 41245            if (terms.Count == 0)
 1246            {
 01247                terms.Add(item.Name);
 1248            }
 1249
 41250            if (item is Video video)
 1251            {
 41252                if (video.Video3DFormat.HasValue)
 1253                {
 01254                    terms.Add("3D");
 1255                }
 1256
 41257                if (video.VideoType == VideoType.BluRay)
 1258                {
 01259                    terms.Add("Bluray");
 1260                }
 41261                else if (video.VideoType == VideoType.Dvd)
 1262                {
 01263                    terms.Add("DVD");
 1264                }
 41265                else if (video.VideoType == VideoType.Iso)
 1266                {
 01267                    if (video.IsoType.HasValue)
 1268                    {
 01269                        if (video.IsoType.Value == IsoType.BluRay)
 1270                        {
 01271                            terms.Add("Bluray");
 1272                        }
 01273                        else if (video.IsoType.Value == IsoType.Dvd)
 1274                        {
 01275                            terms.Add("DVD");
 1276                        }
 1277                    }
 1278                    else
 1279                    {
 01280                        terms.Add("ISO");
 1281                    }
 1282                }
 1283            }
 1284
 41285            return string.Join('/', terms);
 1286        }
 1287
 1288        public Task RefreshMetadata(CancellationToken cancellationToken)
 1289        {
 501290            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
 1291        }
 1292
 1293        /// <summary>
 1294        /// The base implementation to refresh metadata.
 1295        /// </summary>
 1296        /// <param name="options">The options.</param>
 1297        /// <param name="cancellationToken">The cancellation token.</param>
 1298        /// <returns>true if a provider reports we changed.</returns>
 1299        public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellation
 1300        {
 1301            var requiresSave = false;
 1302
 1303            if (SupportsOwnedItems)
 1304            {
 1305                try
 1306                {
 1307                    if (IsFileProtocol)
 1308                    {
 1309                        requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService
 1310                    }
 1311
 1312                    await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties i
 1313                }
 1314                catch (Exception ex)
 1315                {
 1316                    Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
 1317                }
 1318            }
 1319
 1320            var refreshOptions = requiresSave
 1321                ? new MetadataRefreshOptions(options)
 1322                {
 1323                    ForceSave = true
 1324                }
 1325                : options;
 1326
 1327            return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false
 1328        }
 1329
 1330        protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
 1331        {
 01332            if (!IsVisible(user))
 1333            {
 01334                return false;
 1335            }
 1336
 01337            if (GetParents().Any(i => !i.IsVisible(user, true)))
 1338            {
 01339                return false;
 1340            }
 1341
 01342            if (checkFolders)
 1343            {
 01344                var topParent = GetParents().LastOrDefault() ?? this;
 1345
 01346                if (string.IsNullOrEmpty(topParent.Path))
 1347                {
 01348                    return true;
 1349                }
 1350
 01351                var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
 1352
 01353                if (itemCollectionFolders.Count > 0)
 1354                {
 01355                    var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i
 01356                    if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
 1357                    {
 01358                        return false;
 1359                    }
 1360                }
 1361            }
 1362
 01363            return true;
 1364        }
 1365
 1366        public void SetParent(Folder parent)
 1367        {
 91368            ParentId = parent is null ? Guid.Empty : parent.Id;
 91369        }
 1370
 1371        /// <summary>
 1372        /// Refreshes owned items such as trailers, theme videos, special features, etc.
 1373        /// Returns true or false indicating if changes were found.
 1374        /// </summary>
 1375        /// <param name="options">The metadata refresh options.</param>
 1376        /// <param name="fileSystemChildren">The list of filesystem children.</param>
 1377        /// <param name="cancellationToken">The cancellation token.</param>
 1378        /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
 1379        protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemM
 1380        {
 1381            if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder
 1382            {
 1383                return false;
 1384            }
 1385
 1386            return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 1387        }
 1388
 1389        protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
 1390        {
 411391            return directoryService.GetFileSystemEntries(ContainingFolderPath);
 1392        }
 1393
 1394        private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMe
 1395        {
 1396            var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
 1397            var newExtraIds = Array.ConvertAll(extras, x => x.Id);
 1398            var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
 1399
 1400            if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.Full
 1401            {
 1402                return false;
 1403            }
 1404
 1405            var ownerId = item.Id;
 1406
 1407            var tasks = extras.Select(i =>
 1408            {
 1409                var subOptions = new MetadataRefreshOptions(options);
 1410                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
 1411                {
 1412                    i.OwnerId = ownerId;
 1413                    i.ParentId = Guid.Empty;
 1414                    subOptions.ForceSave = true;
 1415                }
 1416
 1417                return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
 1418            });
 1419
 1420            // Cleanup removed extras
 1421            var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
 1422            if (removedExtraIds.Length > 0)
 1423            {
 1424                var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
 1425                {
 1426                    ItemIds = removedExtraIds
 1427                });
 1428                foreach (var removedExtra in removedExtras)
 1429                {
 1430                    LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
 1431                    {
 1432                        DeleteFileLocation = false
 1433                    });
 1434                }
 1435            }
 1436
 1437            await Task.WhenAll(tasks).ConfigureAwait(false);
 1438
 1439            item.ExtraIds = newExtraIds;
 1440
 1441            return true;
 1442        }
 1443
 1444        public string GetPresentationUniqueKey()
 1445        {
 01446            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
 1447        }
 1448
 1449        public virtual bool RequiresRefresh()
 1450        {
 571451            if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
 1452            {
 571453                return false;
 1454            }
 1455
 01456            var info = FileSystem.GetFileSystemInfo(Path);
 1457
 01458            return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
 1459        }
 1460
 1461        public virtual List<string> GetUserDataKeys()
 1462        {
 881463            var list = new List<string>();
 1464
 881465            if (SourceType == SourceType.Channel)
 1466            {
 01467                if (!string.IsNullOrEmpty(ExternalId))
 1468                {
 01469                    list.Add(ExternalId);
 1470                }
 1471            }
 1472
 881473            list.Add(Id.ToString());
 881474            return list;
 1475        }
 1476
 1477        internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
 1478        {
 51479            var updateType = ItemUpdateType.None;
 1480
 51481            if (IsInMixedFolder != newItem.IsInMixedFolder)
 1482            {
 01483                IsInMixedFolder = newItem.IsInMixedFolder;
 01484                updateType |= ItemUpdateType.MetadataImport;
 1485            }
 1486
 51487            return updateType;
 1488        }
 1489
 1490        public void AfterMetadataRefresh()
 1491        {
 561492            _sortName = null;
 561493        }
 1494
 1495        /// <summary>
 1496        /// Gets the preferred metadata language.
 1497        /// </summary>
 1498        /// <returns>System.String.</returns>
 1499        public string GetPreferredMetadataLanguage()
 1500        {
 571501            string lang = PreferredMetadataLanguage;
 1502
 571503            if (string.IsNullOrEmpty(lang))
 1504            {
 571505                lang = GetParents()
 571506                    .Select(i => i.PreferredMetadataLanguage)
 571507                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1508            }
 1509
 571510            if (string.IsNullOrEmpty(lang))
 1511            {
 571512                lang = LibraryManager.GetCollectionFolders(this)
 571513                    .Select(i => i.PreferredMetadataLanguage)
 571514                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1515            }
 1516
 571517            if (string.IsNullOrEmpty(lang))
 1518            {
 571519                lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
 1520            }
 1521
 571522            if (string.IsNullOrEmpty(lang))
 1523            {
 571524                lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 1525            }
 1526
 571527            return lang;
 1528        }
 1529
 1530        /// <summary>
 1531        /// Gets the preferred metadata language.
 1532        /// </summary>
 1533        /// <returns>System.String.</returns>
 1534        public string GetPreferredMetadataCountryCode()
 1535        {
 571536            string lang = PreferredMetadataCountryCode;
 1537
 571538            if (string.IsNullOrEmpty(lang))
 1539            {
 571540                lang = GetParents()
 571541                    .Select(i => i.PreferredMetadataCountryCode)
 571542                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1543            }
 1544
 571545            if (string.IsNullOrEmpty(lang))
 1546            {
 571547                lang = LibraryManager.GetCollectionFolders(this)
 571548                    .Select(i => i.PreferredMetadataCountryCode)
 571549                    .FirstOrDefault(i => !string.IsNullOrEmpty(i));
 1550            }
 1551
 571552            if (string.IsNullOrEmpty(lang))
 1553            {
 571554                lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
 1555            }
 1556
 571557            if (string.IsNullOrEmpty(lang))
 1558            {
 571559                lang = ConfigurationManager.Configuration.MetadataCountryCode;
 1560            }
 1561
 571562            return lang;
 1563        }
 1564
 1565        public virtual bool IsSaveLocalMetadataEnabled()
 1566        {
 881567            if (SourceType == SourceType.Channel)
 1568            {
 01569                return false;
 1570            }
 1571
 881572            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 1573
 881574            return libraryOptions.SaveLocalMetadata;
 1575        }
 1576
 1577        /// <summary>
 1578        /// Determines if a given user has access to this item.
 1579        /// </summary>
 1580        /// <param name="user">The user.</param>
 1581        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1582        /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
 1583        /// <exception cref="ArgumentNullException">If user is null.</exception>
 1584        public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
 1585        {
 131586            ArgumentNullException.ThrowIfNull(user);
 1587
 131588            if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
 1589            {
 01590                return false;
 1591            }
 1592
 131593            var maxAllowedRating = user.MaxParentalRatingScore;
 131594            var maxAllowedSubRating = user.MaxParentalRatingSubScore;
 131595            var rating = CustomRatingForComparison;
 1596
 131597            if (string.IsNullOrEmpty(rating))
 1598            {
 131599                rating = OfficialRatingForComparison;
 1600            }
 1601
 131602            if (string.IsNullOrEmpty(rating))
 1603            {
 131604                Logger.LogDebug("{0} has no parental rating set.", Name);
 131605                return !GetBlockUnratedValue(user);
 1606            }
 1607
 01608            var ratingScore = LocalizationManager.GetRatingScore(rating);
 1609
 1610            // Could not determine rating level
 01611            if (ratingScore is null)
 1612            {
 01613                var isAllowed = !GetBlockUnratedValue(user);
 1614
 01615                if (!isAllowed)
 1616                {
 01617                    Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating);
 1618                }
 1619
 01620                return isAllowed;
 1621            }
 1622
 01623            if (maxAllowedSubRating is not null)
 1624            {
 01625                return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value
 1626            }
 1627
 01628            return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
 1629        }
 1630
 1631        public ParentalRatingScore GetParentalRatingScore()
 1632        {
 1141633            var rating = CustomRatingForComparison;
 1634
 1141635            if (string.IsNullOrEmpty(rating))
 1636            {
 1141637                rating = OfficialRatingForComparison;
 1638            }
 1639
 1141640            if (string.IsNullOrEmpty(rating))
 1641            {
 1141642                return null;
 1643            }
 1644
 01645            return LocalizationManager.GetRatingScore(rating);
 1646        }
 1647
 1648        public List<string> GetInheritedTags()
 1649        {
 921650            var list = new List<string>();
 921651            list.AddRange(Tags);
 1652
 2601653            foreach (var parent in GetParents())
 1654            {
 381655                list.AddRange(parent.Tags);
 1656            }
 1657
 1841658            foreach (var folder in LibraryManager.GetCollectionFolders(this))
 1659            {
 01660                list.AddRange(folder.Tags);
 1661            }
 1662
 921663            return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
 1664        }
 1665
 1666        private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
 1667        {
 131668            var allTags = GetInheritedTags();
 131669            if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgno
 1670            {
 01671                return false;
 1672            }
 1673
 131674            var parent = GetParents().FirstOrDefault() ?? this;
 131675            if (parent is UserRootFolder or AggregateFolder or UserView)
 1676            {
 131677                return true;
 1678            }
 1679
 01680            var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
 01681            if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Co
 1682            {
 01683                return false;
 1684            }
 1685
 01686            return true;
 1687        }
 1688
 1689        public virtual UnratedItem GetBlockUnratedType()
 1690        {
 791691            if (SourceType == SourceType.Channel)
 1692            {
 01693                return UnratedItem.ChannelContent;
 1694            }
 1695
 791696            return UnratedItem.Other;
 1697        }
 1698
 1699        /// <summary>
 1700        /// Gets a bool indicating if access to the unrated item is blocked or not.
 1701        /// </summary>
 1702        /// <param name="user">The configuration.</param>
 1703        /// <returns><c>true</c> if blocked, <c>false</c> otherwise.</returns>
 1704        protected virtual bool GetBlockUnratedValue(User user)
 1705        {
 1706            // Don't block plain folders that are unrated. Let the media underneath get blocked
 1707            // Special folders like series and albums will override this method.
 131708            if (IsFolder || this is IItemByName)
 1709            {
 131710                return false;
 1711            }
 1712
 01713            return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType(
 1714        }
 1715
 1716        /// <summary>
 1717        /// Determines if this folder should be visible to a given user.
 1718        /// Default is just parental allowed. Can be overridden for more functionality.
 1719        /// </summary>
 1720        /// <param name="user">The user.</param>
 1721        /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
 1722        /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
 1723        /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
 1724        public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 1725        {
 131726            ArgumentNullException.ThrowIfNull(user);
 1727
 131728            return IsParentalAllowed(user, skipAllowedTagsCheck);
 1729        }
 1730
 1731        public virtual bool IsVisibleStandalone(User user)
 1732        {
 01733            if (SourceType == SourceType.Channel)
 1734            {
 01735                return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
 1736            }
 1737
 01738            return IsVisibleStandaloneInternal(user, true);
 1739        }
 1740
 1741        public virtual string GetClientTypeName()
 1742        {
 1091743            if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not
 1744            {
 01745                return "ChannelFolderItem";
 1746            }
 1747
 1091748            return GetType().Name;
 1749        }
 1750
 1751        public BaseItemKind GetBaseItemKind()
 1752        {
 2401753            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
 1754        }
 1755
 1756        /// <summary>
 1757        /// Gets the linked child.
 1758        /// </summary>
 1759        /// <param name="info">The info.</param>
 1760        /// <returns>BaseItem.</returns>
 1761        protected BaseItem GetLinkedChild(LinkedChild info)
 1762        {
 1763            // First get using the cached Id
 01764            if (info.ItemId.HasValue)
 1765            {
 01766                if (info.ItemId.Value.IsEmpty())
 1767                {
 01768                    return null;
 1769                }
 1770
 01771                var itemById = LibraryManager.GetItemById(info.ItemId.Value);
 1772
 01773                if (itemById is not null)
 1774                {
 01775                    return itemById;
 1776                }
 1777            }
 1778
 01779            var item = FindLinkedChild(info);
 1780
 1781            // If still null, log
 01782            if (item is null)
 1783            {
 1784                // Don't keep searching over and over
 01785                info.ItemId = Guid.Empty;
 1786            }
 1787            else
 1788            {
 1789                // Cache the id for next time
 01790                info.ItemId = item.Id;
 1791            }
 1792
 01793            return item;
 1794        }
 1795
 1796        private BaseItem FindLinkedChild(LinkedChild info)
 1797        {
 01798            var path = info.Path;
 1799
 01800            if (!string.IsNullOrEmpty(path))
 1801            {
 01802                path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
 1803
 01804                var itemByPath = LibraryManager.FindByPath(path, null);
 1805
 01806                if (itemByPath is null)
 1807                {
 01808                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1809                }
 1810
 01811                return itemByPath;
 1812            }
 1813
 01814            if (!string.IsNullOrEmpty(info.LibraryItemId))
 1815            {
 01816                var item = LibraryManager.GetItemById(info.LibraryItemId);
 1817
 01818                if (item is null)
 1819                {
 01820                    Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
 1821                }
 1822
 01823                return item;
 1824            }
 1825
 01826            return null;
 1827        }
 1828
 1829        /// <summary>
 1830        /// Adds a studio to the item.
 1831        /// </summary>
 1832        /// <param name="name">The name.</param>
 1833        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1834        public void AddStudio(string name)
 1835        {
 41836            ArgumentException.ThrowIfNullOrEmpty(name);
 41837            var current = Studios;
 1838
 41839            if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
 1840            {
 41841                int curLen = current.Length;
 41842                if (curLen == 0)
 1843                {
 41844                    Studios = [name];
 1845                }
 1846                else
 1847                {
 01848                    Studios = [.. current, name];
 1849                }
 1850            }
 01851        }
 1852
 1853        public void SetStudios(IEnumerable<string> names)
 1854        {
 01855            Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 01856        }
 1857
 1858        /// <summary>
 1859        /// Adds a genre to the item.
 1860        /// </summary>
 1861        /// <param name="name">The name.</param>
 1862        /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
 1863        public void AddGenre(string name)
 1864        {
 131865            ArgumentException.ThrowIfNullOrEmpty(name);
 1866
 131867            var genres = Genres;
 131868            if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
 1869            {
 131870                Genres = [.. genres, name];
 1871            }
 131872        }
 1873
 1874        /// <summary>
 1875        /// Marks the played.
 1876        /// </summary>
 1877        /// <param name="user">The user.</param>
 1878        /// <param name="datePlayed">The date played.</param>
 1879        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1880        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1881        public virtual void MarkPlayed(
 1882            User user,
 1883            DateTime? datePlayed,
 1884            bool resetPosition)
 1885        {
 01886            ArgumentNullException.ThrowIfNull(user);
 1887
 01888            var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
 01889            {
 01890                Key = GetUserDataKeys().First(),
 01891            };
 1892
 01893            if (datePlayed.HasValue)
 1894            {
 1895                // Increment
 01896                data.PlayCount++;
 1897            }
 1898
 1899            // Ensure it's at least one
 01900            data.PlayCount = Math.Max(data.PlayCount, 1);
 1901
 01902            if (resetPosition)
 1903            {
 01904                data.PlaybackPositionTicks = 0;
 1905            }
 1906
 01907            data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
 01908            data.Played = true;
 1909
 01910            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01911        }
 1912
 1913        /// <summary>
 1914        /// Marks the unplayed.
 1915        /// </summary>
 1916        /// <param name="user">The user.</param>
 1917        /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
 1918        public virtual void MarkUnplayed(User user)
 1919        {
 01920            ArgumentNullException.ThrowIfNull(user);
 1921
 01922            var data = UserDataManager.GetUserData(user, this);
 1923
 1924            // I think it is okay to do this here.
 1925            // if this is only called when a user is manually forcing something to un-played
 1926            // then it probably is what we want to do...
 01927            data.PlayCount = 0;
 01928            data.PlaybackPositionTicks = 0;
 01929            data.LastPlayedDate = null;
 01930            data.Played = false;
 1931
 01932            UserDataManager.SaveUserData(user, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
 01933        }
 1934
 1935        /// <summary>
 1936        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
 1937        /// </summary>
 1938        public virtual void ChangedExternally()
 1939        {
 01940            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriori
 01941        }
 1942
 1943        /// <summary>
 1944        /// Gets an image.
 1945        /// </summary>
 1946        /// <param name="type">The type.</param>
 1947        /// <param name="imageIndex">Index of the image.</param>
 1948        /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
 1949        /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
 1950        public bool HasImage(ImageType type, int imageIndex)
 1951        {
 1431952            return GetImageInfo(type, imageIndex) is not null;
 1953        }
 1954
 1955        public void SetImage(ItemImageInfo image, int index)
 1956        {
 131957            if (image.Type == ImageType.Chapter)
 1958            {
 01959                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 1960            }
 1961
 131962            var existingImage = GetImageInfo(image.Type, index);
 1963
 131964            if (existingImage is null)
 1965            {
 111966                AddImage(image);
 1967            }
 1968            else
 1969            {
 21970                existingImage.Path = image.Path;
 21971                existingImage.DateModified = image.DateModified;
 21972                existingImage.Width = image.Width;
 21973                existingImage.Height = image.Height;
 21974                existingImage.BlurHash = image.BlurHash;
 1975            }
 21976        }
 1977
 1978        public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
 1979        {
 461980            if (type == ImageType.Chapter)
 1981            {
 01982                throw new ArgumentException("Cannot set chapter images using SetImagePath");
 1983            }
 1984
 461985            var image = GetImageInfo(type, index);
 1986
 461987            if (image is null)
 1988            {
 451989                AddImage(GetImageInfo(file, type));
 1990            }
 1991            else
 1992            {
 11993                var imageInfo = GetImageInfo(file, type);
 1994
 11995                image.Path = file.FullName;
 11996                image.DateModified = imageInfo.DateModified;
 1997
 1998                // reset these values
 11999                image.Width = 0;
 12000                image.Height = 0;
 2001            }
 12002        }
 2003
 2004        /// <summary>
 2005        /// Deletes the image.
 2006        /// </summary>
 2007        /// <param name="type">The type.</param>
 2008        /// <param name="index">The index.</param>
 2009        /// <returns>A task.</returns>
 2010        public async Task DeleteImageAsync(ImageType type, int index)
 2011        {
 2012            var info = GetImageInfo(type, index);
 2013
 2014            if (info is null)
 2015            {
 2016                // Nothing to do
 2017                return;
 2018            }
 2019
 2020            // Remove from file system
 2021            var path = info.Path;
 2022            if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
 2023            {
 2024                FileSystem.DeleteFile(path);
 2025            }
 2026
 2027            // Remove from item
 2028            RemoveImage(info);
 2029
 2030            await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 2031        }
 2032
 2033        public void RemoveImage(ItemImageInfo image)
 2034        {
 02035            RemoveImages([image]);
 02036        }
 2037
 2038        public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
 2039        {
 92040            ImageInfos = ImageInfos.Except(deletedImages).ToArray();
 92041        }
 2042
 2043        public void AddImage(ItemImageInfo image)
 2044        {
 562045            ImageInfos = [.. ImageInfos, image];
 562046        }
 2047
 2048        public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationTok
 2049         => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(fals
 2050
 2051        /// <summary>
 2052        /// Validates that images within the item are still on the filesystem.
 2053        /// </summary>
 2054        /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
 2055        public bool ValidateImages()
 2056        {
 712057            List<ItemImageInfo> deletedImages = null;
 1722058            foreach (var imageInfo in ImageInfos)
 2059            {
 152060                if (!imageInfo.IsLocalFile)
 2061                {
 2062                    continue;
 2063                }
 2064
 152065                if (File.Exists(imageInfo.Path))
 2066                {
 2067                    continue;
 2068                }
 2069
 32070                (deletedImages ??= []).Add(imageInfo);
 2071            }
 2072
 712073            var anyImagesRemoved = deletedImages?.Count > 0;
 712074            if (anyImagesRemoved)
 2075            {
 22076                RemoveImages(deletedImages);
 2077            }
 2078
 712079            return anyImagesRemoved;
 2080        }
 2081
 2082        /// <summary>
 2083        /// Gets the image path.
 2084        /// </summary>
 2085        /// <param name="imageType">Type of the image.</param>
 2086        /// <param name="imageIndex">Index of the image.</param>
 2087        /// <returns>System.String.</returns>
 2088        /// <exception cref="ArgumentNullException">Item is null.</exception>
 2089        public string GetImagePath(ImageType imageType, int imageIndex)
 22090            => GetImageInfo(imageType, imageIndex)?.Path;
 2091
 2092        /// <summary>
 2093        /// Gets the image information.
 2094        /// </summary>
 2095        /// <param name="imageType">Type of the image.</param>
 2096        /// <param name="imageIndex">Index of the image.</param>
 2097        /// <returns>ItemImageInfo.</returns>
 2098        public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
 2099        {
 3202100            if (imageType == ImageType.Chapter)
 2101            {
 02102                var chapter = ChapterManager.GetChapter(Id, imageIndex);
 2103
 02104                if (chapter is null)
 2105                {
 02106                    return null;
 2107                }
 2108
 02109                var path = chapter.ImagePath;
 2110
 02111                if (string.IsNullOrEmpty(path))
 2112                {
 02113                    return null;
 2114                }
 2115
 02116                return new ItemImageInfo
 02117                {
 02118                    Path = path,
 02119                    DateModified = chapter.ImageDateModified,
 02120                    Type = imageType
 02121                };
 2122            }
 2123
 2124            // Music albums usually don't have dedicated backdrops, so return one from the artist instead
 3202125            if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
 2126            {
 02127                var artist = FindParent<MusicArtist>();
 2128
 02129                if (artist is not null)
 2130                {
 02131                    return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
 2132                }
 2133            }
 2134
 3202135            return GetImages(imageType)
 3202136                .ElementAtOrDefault(imageIndex);
 2137        }
 2138
 2139        /// <summary>
 2140        /// Computes image index for given image or raises if no matching image found.
 2141        /// </summary>
 2142        /// <param name="image">Image to compute index for.</param>
 2143        /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found.
 2144        /// </exception>
 2145        /// <returns>Image index.</returns>
 2146        public int GetImageIndex(ItemImageInfo image)
 2147        {
 02148            ArgumentNullException.ThrowIfNull(image);
 2149
 02150            if (image.Type == ImageType.Chapter)
 2151            {
 02152                var chapters = ChapterManager.GetChapters(Id);
 02153                for (var i = 0; i < chapters.Count; i++)
 2154                {
 02155                    if (chapters[i].ImagePath == image.Path)
 2156                    {
 02157                        return i;
 2158                    }
 2159                }
 2160
 02161                throw new ArgumentException("No chapter index found for image path", image.Path);
 2162            }
 2163
 02164            var images = GetImages(image.Type).ToArray();
 02165            for (var i = 0; i < images.Length; i++)
 2166            {
 02167                if (images[i].Path == image.Path)
 2168                {
 02169                    return i;
 2170                }
 2171            }
 2172
 02173            throw new ArgumentException("No image index found for image path", image.Path);
 2174        }
 2175
 2176        public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
 2177        {
 2178            if (imageType == ImageType.Chapter)
 2179            {
 2180                throw new ArgumentException("No image info for chapter images");
 2181            }
 2182
 2183            // Yield return is more performant than LINQ Where on an Array
 2184            for (var i = 0; i < ImageInfos.Length; i++)
 2185            {
 2186                var imageInfo = ImageInfos[i];
 2187                if (imageInfo.Type == imageType)
 2188                {
 2189                    yield return imageInfo;
 2190                }
 2191            }
 2192        }
 2193
 2194        /// <summary>
 2195        /// Adds the images, updating metadata if they already are part of this item.
 2196        /// </summary>
 2197        /// <param name="imageType">Type of the image.</param>
 2198        /// <param name="images">The images.</param>
 2199        /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
 2200        /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
 2201        public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
 2202        {
 712203            if (imageType == ImageType.Chapter)
 2204            {
 02205                throw new ArgumentException("Cannot call AddImages with chapter images");
 2206            }
 2207
 712208            var existingImages = GetImages(imageType)
 712209                .ToList();
 2210
 712211            var newImageList = new List<FileSystemMetadata>();
 712212            var imageUpdated = false;
 2213
 1582214            foreach (var newImage in images)
 2215            {
 82216                if (newImage is null)
 2217                {
 02218                    throw new ArgumentException("null image found in list");
 2219                }
 2220
 82221                var existing = existingImages
 82222                    .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
 2223
 82224                if (existing is null)
 2225                {
 42226                    newImageList.Add(newImage);
 2227                }
 2228                else
 2229                {
 42230                    if (existing.IsLocalFile)
 2231                    {
 42232                        var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
 2233
 2234                        // If date changed then we need to reset saved image dimensions
 42235                        if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
 2236                        {
 22237                            existing.Width = 0;
 22238                            existing.Height = 0;
 22239                            imageUpdated = true;
 2240                        }
 2241
 42242                        existing.DateModified = newDateModified;
 2243                    }
 2244                }
 2245            }
 2246
 712247            if (newImageList.Count > 0)
 2248            {
 22249                ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
 2250            }
 2251
 712252            return imageUpdated || newImageList.Count > 0;
 2253        }
 2254
 2255        private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
 2256        {
 502257            return new ItemImageInfo
 502258            {
 502259                Path = file.FullName,
 502260                Type = type,
 502261                DateModified = FileSystem.GetLastWriteTimeUtc(file)
 502262            };
 2263        }
 2264
 2265        /// <summary>
 2266        /// Gets the file system path to delete when the item is to be deleted.
 2267        /// </summary>
 2268        /// <returns>The metadata for the deleted paths.</returns>
 2269        public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
 2270        {
 02271            return new[]
 02272            {
 02273                FileSystem.GetFileSystemInfo(Path)
 02274            }.Concat(GetLocalMetadataFilesToDelete());
 2275        }
 2276
 2277        protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
 2278        {
 02279            if (IsFolder || !IsInMixedFolder)
 2280            {
 02281                return [];
 2282            }
 2283
 02284            var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
 2285
 02286            return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false)
 02287                .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison
 02288                .ToList();
 2289        }
 2290
 2291        public bool AllowsMultipleImages(ImageType type)
 2292        {
 182293            return type == ImageType.Backdrop || type == ImageType.Chapter;
 2294        }
 2295
 2296        public Task SwapImagesAsync(ImageType type, int index1, int index2)
 2297        {
 02298            if (!AllowsMultipleImages(type))
 2299            {
 02300                throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots
 2301            }
 2302
 02303            var info1 = GetImageInfo(type, index1);
 02304            var info2 = GetImageInfo(type, index2);
 2305
 02306            if (info1 is null || info2 is null)
 2307            {
 2308                // Nothing to do
 02309                return Task.CompletedTask;
 2310            }
 2311
 02312            if (!info1.IsLocalFile || !info2.IsLocalFile)
 2313            {
 2314                // TODO: Not supported  yet
 02315                return Task.CompletedTask;
 2316            }
 2317
 02318            var path1 = info1.Path;
 02319            var path2 = info2.Path;
 2320
 02321            FileSystem.SwapFiles(path1, path2);
 2322
 2323            // Refresh these values
 02324            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
 02325            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 2326
 02327            info1.Width = 0;
 02328            info1.Height = 0;
 02329            info2.Width = 0;
 02330            info2.Height = 0;
 2331
 02332            return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
 2333        }
 2334
 2335        public virtual bool IsPlayed(User user, UserItemData userItemData)
 2336        {
 02337            userItemData ??= UserDataManager.GetUserData(user, this);
 2338
 02339            return userItemData is not null && userItemData.Played;
 2340        }
 2341
 2342        public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
 2343        {
 02344            userItemData ??= UserDataManager.GetUserData(user, this);
 2345
 02346            return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
 2347        }
 2348
 2349        public virtual bool IsUnplayed(User user, UserItemData userItemData)
 2350        {
 02351            ArgumentNullException.ThrowIfNull(user);
 2352
 02353            userItemData ??= UserDataManager.GetUserData(user, this);
 2354
 02355            return userItemData is null || !userItemData.Played;
 2356        }
 2357
 2358        ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
 2359        {
 572360            return GetItemLookupInfo<ItemLookupInfo>();
 2361        }
 2362
 2363        protected T GetItemLookupInfo<T>()
 2364            where T : ItemLookupInfo, new()
 2365        {
 572366            return new T
 572367            {
 572368                Path = Path,
 572369                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 572370                MetadataLanguage = GetPreferredMetadataLanguage(),
 572371                Name = GetNameForMetadataLookup(),
 572372                OriginalTitle = OriginalTitle,
 572373                ProviderIds = ProviderIds,
 572374                IndexNumber = IndexNumber,
 572375                ParentIndexNumber = ParentIndexNumber,
 572376                Year = ProductionYear,
 572377                PremiereDate = PremiereDate
 572378            };
 2379        }
 2380
 2381        protected virtual string GetNameForMetadataLookup()
 2382        {
 572383            return Name;
 2384        }
 2385
 2386        /// <summary>
 2387        /// This is called before any metadata refresh and returns true if changes were made.
 2388        /// </summary>
 2389        /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
 2390        /// <returns>true if the item has change, else false.</returns>
 2391        public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
 2392        {
 572393            _sortName = null;
 2394
 572395            var hasChanges = false;
 2396
 572397            if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
 2398            {
 02399                Name = System.IO.Path.GetFileNameWithoutExtension(Path);
 02400                hasChanges = true;
 2401            }
 2402
 572403            return hasChanges;
 2404        }
 2405
 2406        protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
 2407        {
 02408            if (protocol == MediaProtocol.File)
 2409            {
 02410                return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
 2411            }
 2412
 02413            return path;
 2414        }
 2415
 2416        public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User 
 2417        {
 02418            if (RunTimeTicks.HasValue)
 2419            {
 02420                double pct = RunTimeTicks.Value;
 2421
 02422                if (pct > 0)
 2423                {
 02424                    pct = userData.PlaybackPositionTicks / pct;
 2425
 02426                    if (pct > 0)
 2427                    {
 02428                        dto.PlayedPercentage = 100 * pct;
 2429                    }
 2430                }
 2431            }
 02432        }
 2433
 2434        protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOpti
 2435        {
 2436            var newOptions = new MetadataRefreshOptions(options)
 2437            {
 2438                SearchResult = null
 2439            };
 2440
 2441            var item = this;
 2442
 2443            if (copyTitleMetadata)
 2444            {
 2445                // Take some data from the main item, for querying purposes
 2446                if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
 2447                {
 2448                    newOptions.ForceSave = true;
 2449                    ownedItem.Genres = item.Genres;
 2450                }
 2451
 2452                if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
 2453                {
 2454                    newOptions.ForceSave = true;
 2455                    ownedItem.Studios = item.Studios;
 2456                }
 2457
 2458                if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
 2459                {
 2460                    newOptions.ForceSave = true;
 2461                    ownedItem.ProductionLocations = item.ProductionLocations;
 2462                }
 2463
 2464                if (item.CommunityRating != ownedItem.CommunityRating)
 2465                {
 2466                    ownedItem.CommunityRating = item.CommunityRating;
 2467                    newOptions.ForceSave = true;
 2468                }
 2469
 2470                if (item.CriticRating != ownedItem.CriticRating)
 2471                {
 2472                    ownedItem.CriticRating = item.CriticRating;
 2473                    newOptions.ForceSave = true;
 2474                }
 2475
 2476                if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
 2477                {
 2478                    ownedItem.Overview = item.Overview;
 2479                    newOptions.ForceSave = true;
 2480                }
 2481
 2482                if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
 2483                {
 2484                    ownedItem.OfficialRating = item.OfficialRating;
 2485                    newOptions.ForceSave = true;
 2486                }
 2487
 2488                if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
 2489                {
 2490                    ownedItem.CustomRating = item.CustomRating;
 2491                    newOptions.ForceSave = true;
 2492                }
 2493            }
 2494
 2495            await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
 2496        }
 2497
 2498        protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string
 2499        {
 2500            var newOptions = new MetadataRefreshOptions(options)
 2501            {
 2502                SearchResult = null
 2503            };
 2504
 2505            var id = LibraryManager.GetNewItemId(path, typeof(Video));
 2506
 2507            // Try to retrieve it from the db. If we don't find it, use the resolved version
 2508            if (LibraryManager.GetItemById(id) is not Video video)
 2509            {
 2510                video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
 2511
 2512                newOptions.ForceSave = true;
 2513            }
 2514
 2515            if (video is null)
 2516            {
 2517                return;
 2518            }
 2519
 2520            if (video.OwnerId.IsEmpty())
 2521            {
 2522                video.OwnerId = Id;
 2523            }
 2524
 2525            await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(fa
 2526        }
 2527
 2528        public string GetEtag(User user)
 2529        {
 62530            var list = GetEtagValues(user);
 2531
 62532            return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 2533        }
 2534
 2535        protected virtual List<string> GetEtagValues(User user)
 2536        {
 62537            return
 62538            [
 62539                DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
 62540            ];
 2541        }
 2542
 2543        public virtual IEnumerable<Guid> GetAncestorIds()
 2544        {
 792545            return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
 2546        }
 2547
 2548        public BaseItem GetTopParent()
 2549        {
 792550            if (IsTopParent)
 2551            {
 212552                return this;
 2553            }
 2554
 582555            return GetParents().FirstOrDefault(parent => parent.IsTopParent);
 2556        }
 2557
 2558        public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
 2559        {
 452560            return [Id];
 2561        }
 2562
 2563        public virtual double? GetRefreshProgress()
 2564        {
 02565            return null;
 2566        }
 2567
 2568        public virtual ItemUpdateType OnMetadataChanged()
 2569        {
 1142570            var updateType = ItemUpdateType.None;
 2571
 1142572            var item = this;
 2573
 1142574            var rating = item.GetParentalRatingScore();
 1142575            if (rating is not null)
 2576            {
 02577                if (rating.Score != item.InheritedParentalRatingValue)
 2578                {
 02579                    item.InheritedParentalRatingValue = rating.Score;
 02580                    updateType |= ItemUpdateType.MetadataImport;
 2581                }
 2582
 02583                if (rating.SubScore != item.InheritedParentalRatingSubValue)
 2584                {
 02585                    item.InheritedParentalRatingSubValue = rating.SubScore;
 02586                    updateType |= ItemUpdateType.MetadataImport;
 2587                }
 2588            }
 2589            else
 2590            {
 1142591                if (item.InheritedParentalRatingValue is not null)
 2592                {
 02593                    item.InheritedParentalRatingValue = null;
 02594                    item.InheritedParentalRatingSubValue = null;
 02595                    updateType |= ItemUpdateType.MetadataImport;
 2596                }
 2597            }
 2598
 1142599            return updateType;
 2600        }
 2601
 2602        /// <summary>
 2603        /// Updates the official rating based on content and returns true or false indicating if it changed.
 2604        /// </summary>
 2605        /// <param name="children">Media children.</param>
 2606        /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
 2607        public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
 2608        {
 02609            var currentOfficialRating = OfficialRating;
 2610
 2611            // Gather all possible ratings
 02612            var ratings = children
 02613                .Select(i => i.OfficialRating)
 02614                .Where(i => !string.IsNullOrEmpty(i))
 02615                .Distinct(StringComparer.OrdinalIgnoreCase)
 02616                .Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
 02617                .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
 02618                .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
 02619                .Select(i => i.rating);
 2620
 02621            OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
 2622
 02623            return !string.Equals(
 02624                currentOfficialRating ?? string.Empty,
 02625                OfficialRating ?? string.Empty,
 02626                StringComparison.OrdinalIgnoreCase);
 2627        }
 2628
 2629        public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
 2630        {
 02631            return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2632        }
 2633
 2634        public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> or
 2635        {
 02636            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, 
 2637        }
 2638
 2639        public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
 2640        {
 02641            return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
 2642        }
 2643
 2644        public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> o
 2645        {
 02646            return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user,
 2647        }
 2648
 2649        /// <summary>
 2650        /// Get all extras associated with this item, sorted by <see cref="SortName"/>.
 2651        /// </summary>
 2652        /// <returns>An enumerable containing the items.</returns>
 2653        public IEnumerable<BaseItem> GetExtras()
 2654        {
 62655            return ExtraIds
 62656                .Select(LibraryManager.GetItemById)
 62657                .Where(i => i is not null)
 62658                .OrderBy(i => i.SortName);
 2659        }
 2660
 2661        /// <summary>
 2662        /// Get all extras with specific types that are associated with this item.
 2663        /// </summary>
 2664        /// <param name="extraTypes">The types of extras to retrieve.</param>
 2665        /// <returns>An enumerable containing the extras.</returns>
 2666        public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
 2667        {
 02668            return ExtraIds
 02669                .Select(LibraryManager.GetItemById)
 02670                .Where(i => i is not null)
 02671                .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value))
 02672                .OrderBy(i => i.SortName);
 2673        }
 2674
 2675        public virtual long GetRunTimeTicksForPlayState()
 2676        {
 02677            return RunTimeTicks ?? 0;
 2678        }
 2679
 2680        /// <inheritdoc />
 2681        public override bool Equals(object obj)
 2682        {
 02683            return obj is BaseItem baseItem && this.Equals(baseItem);
 2684        }
 2685
 2686        /// <inheritdoc />
 52687        public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id);
 2688
 2689        /// <inheritdoc />
 612690        public override int GetHashCode() => HashCode.Combine(Id);
 2691    }
 2692}

Methods/Properties

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