using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// /// Class BaseItem /// public abstract class BaseItem : IHasProviderIds, IHasLookupInfo, IEquatable { /// /// The supported image extensions /// public static readonly string[] SupportedImageExtensions = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; private static readonly List _supportedExtensions = new List(SupportedImageExtensions) { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; protected BaseItem() { ThemeSongIds = Array.Empty(); ThemeVideoIds = Array.Empty(); Tags = Array.Empty(); Genres = Array.Empty(); Studios = Array.Empty(); ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); LockedFields = Array.Empty(); ImageInfos = Array.Empty(); ProductionLocations = Array.Empty(); RemoteTrailers = Array.Empty(); ExtraIds = Array.Empty(); } public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; public static char SlugChar = '-'; /// /// The trailer folder name /// public const string TrailerFolderName = "trailers"; public const string ThemeSongsFolderName = "theme-music"; public const string ThemeSongFilename = "theme"; public const string ThemeVideosFolderName = "backdrops"; public const string ExtrasFolderName = "extras"; public const string BehindTheScenesFolderName = "behind the scenes"; public const string DeletedScenesFolderName = "deleted scenes"; public const string InterviewFolderName = "interviews"; public const string SceneFolderName = "scenes"; public const string SampleFolderName = "samples"; public static readonly string[] AllExtrasTypesFolderNames = { ExtrasFolderName, BehindTheScenesFolderName, DeletedScenesFolderName, InterviewFolderName, SceneFolderName, SampleFolderName }; [JsonIgnore] public Guid[] ThemeSongIds { get; set; } [JsonIgnore] public Guid[] ThemeVideoIds { get; set; } [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } [JsonIgnore] public string PreferredMetadataLanguage { get; set; } public long? Size { get; set; } public string Container { get; set; } [JsonIgnore] public string Tagline { get; set; } [JsonIgnore] public virtual ItemImageInfo[] ImageInfos { get; set; } [JsonIgnore] public bool IsVirtualItem { get; set; } /// /// Gets or sets the album. /// /// The album. [JsonIgnore] public string Album { get; set; } /// /// Gets or sets the channel identifier. /// /// The channel identifier. [JsonIgnore] public Guid ChannelId { get; set; } [JsonIgnore] public virtual bool SupportsAddingToPlaylist => false; [JsonIgnore] public virtual bool AlwaysScanInternalMetadataPath => false; /// /// Gets a value indicating whether this instance is in mixed folder. /// /// true if this instance is in mixed folder; otherwise, false. [JsonIgnore] public bool IsInMixedFolder { get; set; } [JsonIgnore] public virtual bool SupportsPlayedStatus => false; [JsonIgnore] public virtual bool SupportsPositionTicksResume => false; [JsonIgnore] public virtual bool SupportsRemoteImageDownloading => true; private string _name; /// /// Gets or sets the name. /// /// The name. [JsonIgnore] public virtual string Name { get => _name; set { _name = value; // lazy load this again _sortName = null; } } [JsonIgnore] public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; [JsonIgnore] public int? TotalBitrate { get; set; } [JsonIgnore] public ExtraType? ExtraType { get; set; } [JsonIgnore] public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); [JsonIgnore] public string OriginalTitle { get; set; } /// /// Gets or sets the id. /// /// The id. [JsonIgnore] public Guid Id { get; set; } [JsonIgnore] public Guid OwnerId { get; set; } /// /// Gets or sets the audio. /// /// The audio. [JsonIgnore] public ProgramAudio? Audio { get; set; } /// /// Return the id that should be used to key display prefs for this item. /// Default is based on the type for everything except actual generic folders. /// /// The display prefs id. [JsonIgnore] public virtual Guid DisplayPreferencesId { get { var thisType = GetType(); return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5(); } } /// /// Gets or sets the path. /// /// The path. [JsonIgnore] public virtual string Path { get; set; } [JsonIgnore] public virtual SourceType SourceType { get { if (!ChannelId.Equals(Guid.Empty)) { return SourceType.Channel; } return SourceType.Library; } } /// /// Returns the folder containing the item. /// If the item is a folder, it returns the folder itself /// [JsonIgnore] public virtual string ContainingFolderPath { get { if (IsFolder) { return Path; } return System.IO.Path.GetDirectoryName(Path); } } /// /// Gets or sets the name of the service. /// /// The name of the service. [JsonIgnore] public string ServiceName { get; set; } /// /// If this content came from an external service, the id of the content on that service /// [JsonIgnore] public string ExternalId { get; set; } [JsonIgnore] public string ExternalSeriesId { get; set; } /// /// Gets or sets the etag. /// /// The etag. [JsonIgnore] public string ExternalEtag { get; set; } [JsonIgnore] public virtual bool IsHidden => false; public BaseItem GetOwner() { var ownerId = OwnerId; return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); } /// /// Gets or sets the type of the location. /// /// The type of the location. [JsonIgnore] public virtual LocationType LocationType { get { //if (IsOffline) //{ // return LocationType.Offline; //} var path = Path; if (string.IsNullOrEmpty(path)) { if (SourceType == SourceType.Channel) { return LocationType.Remote; } return LocationType.Virtual; } return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote; } } [JsonIgnore] public MediaProtocol? PathProtocol { get { var path = Path; if (string.IsNullOrEmpty(path)) { return null; } return MediaSourceManager.GetPathProtocol(path); } } public bool IsPathProtocol(MediaProtocol protocol) { var current = PathProtocol; return current.HasValue && current.Value == protocol; } [JsonIgnore] public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File); [JsonIgnore] public bool HasPathProtocol => PathProtocol.HasValue; [JsonIgnore] public virtual bool SupportsLocalMetadata { get { if (SourceType == SourceType.Channel) { return false; } return IsFileProtocol; } } [JsonIgnore] public virtual string FileNameWithoutExtension { get { if (IsFileProtocol) { return System.IO.Path.GetFileNameWithoutExtension(Path); } return null; } } [JsonIgnore] public virtual bool EnableAlphaNumericSorting => true; private List> GetSortChunks(string s1) { var list = new List>(); int thisMarker = 0; while (thisMarker < s1.Length) { char thisCh = s1[thisMarker]; var thisChunk = new StringBuilder(); bool isNumeric = char.IsDigit(thisCh); while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric) { thisChunk.Append(thisCh); thisMarker++; if (thisMarker < s1.Length) { thisCh = s1[thisMarker]; } } list.Add(new Tuple(thisChunk, isNumeric)); } return list; } /// /// This is just a helper for convenience /// /// The primary image path. [JsonIgnore] public string PrimaryImagePath => this.GetImagePath(ImageType.Primary); public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name) { if (SourceType == SourceType.Channel) { // hack alert return !EnableMediaSourceDisplay; } var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); if (typeOptions != null) { return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) { return false; } var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); } public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name) { if (this is Channel) { // hack alert return true; } if (SourceType == SourceType.Channel) { // hack alert return !EnableMediaSourceDisplay; } var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); if (typeOptions != null) { return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) { return false; } var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); } public virtual bool CanDelete() { if (SourceType == SourceType.Channel) { return ChannelManager.CanDelete(this); } return IsFileProtocol; } public virtual bool IsAuthorizedToDelete(User user, List allCollectionFolders) { if (user.HasPermission(PermissionKind.EnableContentDeletion)) { return true; } var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders); if (SourceType == SourceType.Channel) { return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase); } else { var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); foreach (var folder in collectionFolders) { if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return true; } } } return false; } public bool CanDelete(User user, List allCollectionFolders) { return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); } public bool CanDelete(User user) { var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType().ToList(); return CanDelete(user, allCollectionFolders); } public virtual bool CanDownload() { return false; } public virtual bool IsAuthorizedToDownload(User user) { return user.HasPermission(PermissionKind.EnableContentDownloading); } public bool CanDownload(User user) { return CanDownload() && IsAuthorizedToDownload(user); } /// /// Gets or sets the date created. /// /// The date created. [JsonIgnore] public DateTime DateCreated { get; set; } /// /// Gets or sets the date modified. /// /// The date modified. [JsonIgnore] public DateTime DateModified { get; set; } public DateTime DateLastSaved { get; set; } [JsonIgnore] public DateTime DateLastRefreshed { get; set; } /// /// The logger /// public static ILogger Logger { get; set; } public static ILibraryManager LibraryManager { get; set; } public static IServerConfigurationManager ConfigurationManager { get; set; } public static IProviderManager ProviderManager { get; set; } public static ILocalizationManager LocalizationManager { get; set; } public static IItemRepository ItemRepository { get; set; } public static IFileSystem FileSystem { get; set; } public static IUserDataManager UserDataManager { get; set; } public static IChannelManager ChannelManager { get; set; } public static IMediaSourceManager MediaSourceManager { get; set; } /// /// Returns a that represents this instance. /// /// A that represents this instance. public override string ToString() { return Name; } [JsonIgnore] public bool IsLocked { get; set; } /// /// Gets or sets the locked fields. /// /// The locked fields. [JsonIgnore] public MetadataFields[] LockedFields { get; set; } /// /// Gets the type of the media. /// /// The type of the media. [JsonIgnore] public virtual string MediaType => null; [JsonIgnore] public virtual string[] PhysicalLocations { get { if (!IsFileProtocol) { return new string[] { }; } return new[] { Path }; } } private string _forcedSortName; /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. [JsonIgnore] public string ForcedSortName { get => _forcedSortName; set { _forcedSortName = value; _sortName = null; } } private string _sortName; /// /// Gets the name of the sort. /// /// The name of the sort. [JsonIgnore] public string SortName { get { if (_sortName == null) { if (!string.IsNullOrEmpty(ForcedSortName)) { // Need the ToLower because that's what CreateSortName does _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant(); } else { _sortName = CreateSortName(); } } return _sortName; } set => _sortName = value; } public string GetInternalMetadataPath() { var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; return GetInternalMetadataPath(basePath); } protected virtual string GetInternalMetadataPath(string basePath) { if (SourceType == SourceType.Channel) { return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } var idString = Id.ToString("N", CultureInfo.InvariantCulture); basePath = System.IO.Path.Combine(basePath, "library"); return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); } /// /// Creates the name of the sort. /// /// System.String. protected virtual string CreateSortName() { if (Name == null) return null; //some items may not have name filled in properly if (!EnableAlphaNumericSorting) { return Name.TrimStart(); } var sortable = Name.Trim().ToLowerInvariant(); foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) { sortable = sortable.Replace(removeChar, string.Empty); } foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) { sortable = sortable.Replace(replaceChar, " "); } foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { // Remove from beginning if a space follows if (sortable.StartsWith(search + " ")) { sortable = sortable.Remove(0, search.Length + 1); } // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + search + " ", " "); // Remove from end if followed by a space if (sortable.EndsWith(" " + search)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); } } return ModifySortChunks(sortable); } private string ModifySortChunks(string name) { var chunks = GetSortChunks(name); var builder = new StringBuilder(); foreach (var chunk in chunks) { var chunkBuilder = chunk.Item1; // This chunk is numeric if (chunk.Item2) { while (chunkBuilder.Length < 10) { chunkBuilder.Insert(0, '0'); } } builder.Append(chunkBuilder); } //logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); } [JsonIgnore] public bool EnableMediaSourceDisplay { get { if (SourceType == SourceType.Channel) { return ChannelManager.EnableMediaSourceDisplay(this); } return true; } } [JsonIgnore] public Guid ParentId { get; set; } /// /// Gets or sets the parent. /// /// The parent. [JsonIgnore] public Folder Parent { get => GetParent() as Folder; set { } } public void SetParent(Folder parent) { ParentId = parent == null ? Guid.Empty : parent.Id; } public BaseItem GetParent() { var parentId = ParentId; if (!parentId.Equals(Guid.Empty)) { return LibraryManager.GetItemById(parentId); } return null; } public IEnumerable GetParents() { var parent = GetParent(); while (parent != null) { yield return parent; parent = parent.GetParent(); } } /// /// Finds a parent of a given type /// /// /// ``0. public T FindParent() where T : Folder { foreach (var parent in GetParents()) { var item = parent as T; if (item != null) { return item; } } return null; } [JsonIgnore] public virtual Guid DisplayParentId { get { var parentId = ParentId; return parentId; } } [JsonIgnore] public BaseItem DisplayParent { get { var id = DisplayParentId; if (id.Equals(Guid.Empty)) { return null; } return LibraryManager.GetItemById(id); } } /// /// When the item first debuted. For movies this could be premiere date, episodes would be first aired /// /// The premiere date. [JsonIgnore] public DateTime? PremiereDate { get; set; } /// /// Gets or sets the end date. /// /// The end date. [JsonIgnore] public DateTime? EndDate { get; set; } /// /// Gets or sets the official rating. /// /// The official rating. [JsonIgnore] public string OfficialRating { get; set; } [JsonIgnore] public int InheritedParentalRatingValue { get; set; } /// /// Gets or sets the critic rating. /// /// The critic rating. [JsonIgnore] public float? CriticRating { get; set; } /// /// Gets or sets the custom rating. /// /// The custom rating. [JsonIgnore] public string CustomRating { get; set; } /// /// Gets or sets the overview. /// /// The overview. [JsonIgnore] public string Overview { get; set; } /// /// Gets or sets the studios. /// /// The studios. [JsonIgnore] public string[] Studios { get; set; } /// /// Gets or sets the genres. /// /// The genres. [JsonIgnore] public string[] Genres { get; set; } /// /// Gets or sets the tags. /// /// The tags. [JsonIgnore] public string[] Tags { get; set; } [JsonIgnore] public string[] ProductionLocations { get; set; } /// /// Gets or sets the home page URL. /// /// The home page URL. [JsonIgnore] public string HomePageUrl { get; set; } /// /// Gets or sets the community rating. /// /// The community rating. [JsonIgnore] public float? CommunityRating { get; set; } /// /// Gets or sets the run time ticks. /// /// The run time ticks. [JsonIgnore] public long? RunTimeTicks { get; set; } /// /// Gets or sets the production year. /// /// The production year. [JsonIgnore] public int? ProductionYear { get; set; } /// /// If the item is part of a series, this is it's number in the series. /// This could be episode number, album track number, etc. /// /// The index number. [JsonIgnore] public int? IndexNumber { get; set; } /// /// For an episode this could be the season number, or for a song this could be the disc number. /// /// The parent index number. [JsonIgnore] public int? ParentIndexNumber { get; set; } [JsonIgnore] public virtual bool HasLocalAlternateVersions => false; [JsonIgnore] public string OfficialRatingForComparison { get { var officialRating = OfficialRating; if (!string.IsNullOrEmpty(officialRating)) { return officialRating; } var parent = DisplayParent; if (parent != null) { return parent.OfficialRatingForComparison; } return null; } } [JsonIgnore] public string CustomRatingForComparison { get { var customRating = CustomRating; if (!string.IsNullOrEmpty(customRating)) { return customRating; } var parent = DisplayParent; if (parent != null) { return parent.CustomRatingForComparison; } return null; } } /// /// Gets the play access. /// /// The user. /// PlayAccess. public PlayAccess GetPlayAccess(User user) { if (!user.HasPermission(PermissionKind.EnableMediaPlayback)) { return PlayAccess.None; } //if (!user.IsParentalScheduleAllowed()) //{ // return PlayAccess.None; //} return PlayAccess.Full; } public virtual List GetMediaStreams() { return MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = Id }); } protected virtual bool IsActiveRecording() { return false; } public virtual List GetMediaSources(bool enablePathSubstitution) { if (SourceType == SourceType.Channel) { var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None) .ToList(); if (sources.Count > 0) { return sources; } } var list = GetAllItemsForMediaSources(); var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item1, i.Item2)).ToList(); if (IsActiveRecording()) { foreach (var mediaSource in result) { mediaSource.Type = MediaSourceType.Placeholder; } } return result.OrderBy(i => { if (i.VideoType == VideoType.VideoFile) { return 0; } return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => { var stream = i.VideoStream; return stream == null || stream.Width == null ? 0 : stream.Width.Value; }) .ToList(); } protected virtual List> GetAllItemsForMediaSources() { return new List>(); } private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type) { if (item == null) { throw new ArgumentNullException(nameof(item)); } var protocol = item.PathProtocol; var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), Protocol = protocol ?? MediaProtocol.File, MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, Type = type }; if (string.IsNullOrEmpty(info.Path)) { info.Type = MediaSourceType.Placeholder; } if (info.Protocol == MediaProtocol.File) { info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", CultureInfo.InvariantCulture); } var video = item as Video; if (video != null) { info.IsoType = video.IsoType; info.VideoType = video.VideoType; info.Video3DFormat = video.Video3DFormat; info.Timestamp = video.Timestamp; if (video.IsShortcut) { info.IsRemote = true; info.Path = video.ShortcutPath; info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); } if (string.IsNullOrEmpty(info.Container)) { if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) { if (protocol.HasValue && protocol.Value == MediaProtocol.File) { info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); } } } } if (string.IsNullOrEmpty(info.Container)) { if (protocol.HasValue && protocol.Value == MediaProtocol.File) { info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); } } if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path)) { info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol); } if (video != null && video.VideoType != VideoType.VideoFile) { info.SupportsDirectStream = false; } info.Bitrate = item.TotalBitrate; info.InferTotalBitrate(); return info; } private string GetMediaSourceName(BaseItem item) { var terms = new List(); var path = item.Path; if (item.IsFileProtocol && !string.IsNullOrEmpty(path)) { if (HasLocalAlternateVersions) { var displayName = System.IO.Path.GetFileNameWithoutExtension(path) .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) .TrimStart(new char[] { ' ', '-' }); if (!string.IsNullOrEmpty(displayName)) { terms.Add(displayName); } } if (terms.Count == 0) { var displayName = System.IO.Path.GetFileNameWithoutExtension(path); terms.Add(displayName); } } if (terms.Count == 0) { terms.Add(item.Name); } var video = item as Video; if (video != null) { if (video.Video3DFormat.HasValue) { terms.Add("3D"); } if (video.VideoType == VideoType.BluRay) { terms.Add("Bluray"); } else if (video.VideoType == VideoType.Dvd) { terms.Add("DVD"); } else if (video.VideoType == VideoType.Iso) { if (video.IsoType.HasValue) { if (video.IsoType.Value == IsoType.BluRay) { terms.Add("Bluray"); } else if (video.IsoType.Value == IsoType.Dvd) { terms.Add("DVD"); } } else { terms.Add("ISO"); } } } return string.Join("/", terms.ToArray()); } /// /// Loads the theme songs. /// /// List{Audio.Audio}. private static Audio.Audio[] LoadThemeSongs(List fileSystemChildren, IDirectoryService directoryService) { var files = fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => FileSystem.GetFiles(i.FullName)) .ToList(); // Support plex/xbmc convention files.AddRange(fileSystemChildren .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) ); return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType() .Select(audio => { // Try to retrieve it from the db. If we don't find it, use the resolved version var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio; if (dbItem != null) { audio = dbItem; } else { // item is new audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong; } return audio; // Sort them so that the list can be easily compared for changes }).OrderBy(i => i.Path).ToArray(); } /// /// Loads the video backdrops. /// /// List{Video}. private static Video[] LoadThemeVideos(IEnumerable fileSystemChildren, IDirectoryService directoryService) { var files = fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => FileSystem.GetFiles(i.FullName)); return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType