using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Users; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// /// Class BaseItem /// public abstract class BaseItem : IHasMetadata, IHasLookupInfo { protected static Guid[] EmptyGuidArray = new Guid[] { }; protected static MetadataFields[] EmptyMetadataFieldsArray = new MetadataFields[] { }; protected static string[] EmptyStringArray = new string[] { }; protected static MediaUrl[] EmptyMediaUrlArray = new MediaUrl[] { }; protected static ItemImageInfo[] EmptyItemImageInfoArray = new ItemImageInfo[] { }; public static readonly LinkedChild[] EmptyLinkedChildArray = new LinkedChild[] { }; protected BaseItem() { ThemeSongIds = EmptyGuidArray; ThemeVideoIds = EmptyGuidArray; Tags = EmptyStringArray; Genres = new List(); Studios = EmptyStringArray; ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); LockedFields = EmptyMetadataFieldsArray; ImageInfos = EmptyItemImageInfoArray; ProductionLocations = EmptyStringArray; } public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; public static char SlugChar = '-'; /// /// The supported image extensions /// public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; public static readonly List SupportedImageExtensionsList = SupportedImageExtensions.ToList(); /// /// The trailer folder name /// public static string TrailerFolderName = "trailers"; public static string ThemeSongsFolderName = "theme-music"; public static string ThemeSongFilename = "theme"; public static string ThemeVideosFolderName = "backdrops"; [IgnoreDataMember] public Guid[] ThemeSongIds { get; set; } [IgnoreDataMember] public Guid[] ThemeVideoIds { get; set; } [IgnoreDataMember] public string PreferredMetadataCountryCode { get; set; } [IgnoreDataMember] public string PreferredMetadataLanguage { get; set; } public long? Size { get; set; } public string Container { get; set; } [IgnoreDataMember] public string Tagline { get; set; } [IgnoreDataMember] public virtual ItemImageInfo[] ImageInfos { get; set; } [IgnoreDataMember] public bool IsVirtualItem { get; set; } /// /// Gets or sets the album. /// /// The album. [IgnoreDataMember] public string Album { get; set; } /// /// Gets or sets the channel identifier. /// /// The channel identifier. [IgnoreDataMember] public string ChannelId { get; set; } [IgnoreDataMember] public virtual bool SupportsAddingToPlaylist { get { return false; } } [IgnoreDataMember] public virtual bool AlwaysScanInternalMetadataPath { get { return false; } } /// /// Gets a value indicating whether this instance is in mixed folder. /// /// true if this instance is in mixed folder; otherwise, false. [IgnoreDataMember] public bool IsInMixedFolder { get; set; } [IgnoreDataMember] public virtual bool SupportsPlayedStatus { get { return false; } } [IgnoreDataMember] public virtual bool SupportsPositionTicksResume { get { return false; } } [IgnoreDataMember] public virtual bool SupportsRemoteImageDownloading { get { return true; } } private string _name; /// /// Gets or sets the name. /// /// The name. [IgnoreDataMember] public virtual string Name { get { return _name; } set { _name = value; // lazy load this again _sortName = null; } } [IgnoreDataMember] public bool IsUnaired { get { return PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; } } [IgnoreDataMember] public int? TotalBitrate { get; set; } [IgnoreDataMember] public ExtraType? ExtraType { get; set; } [IgnoreDataMember] public bool IsThemeMedia { get { return ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); } } [IgnoreDataMember] public string OriginalTitle { get; set; } /// /// Gets or sets the id. /// /// The id. [IgnoreDataMember] public Guid Id { get; set; } [IgnoreDataMember] public Guid OwnerId { get; set; } /// /// Gets or sets a value indicating whether this instance is hd. /// /// true if this instance is hd; otherwise, false. [IgnoreDataMember] public bool? IsHD { get; set; } /// /// Gets or sets the audio. /// /// The audio. [IgnoreDataMember] 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. [IgnoreDataMember] public virtual Guid DisplayPreferencesId { get { var thisType = GetType(); return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5(); } } /// /// Gets or sets the path. /// /// The path. [IgnoreDataMember] public virtual string Path { get; set; } [IgnoreDataMember] public virtual SourceType SourceType { get { if (!string.IsNullOrWhiteSpace(ChannelId)) { return SourceType.Channel; } return SourceType.Library; } } /// /// Returns the folder containing the item. /// If the item is a folder, it returns the folder itself /// [IgnoreDataMember] public virtual string ContainingFolderPath { get { if (IsFolder) { return Path; } return FileSystem.GetDirectoryName(Path); } } /// /// Gets or sets the name of the service. /// /// The name of the service. [IgnoreDataMember] public string ServiceName { get; set; } /// /// If this content came from an external service, the id of the content on that service /// [IgnoreDataMember] public string ExternalId { get; set; } [IgnoreDataMember] public string ExternalSeriesId { get; set; } /// /// Gets or sets the etag. /// /// The etag. [IgnoreDataMember] public string ExternalEtag { get; set; } [IgnoreDataMember] public virtual bool IsHidden { get { return false; } } [IgnoreDataMember] public virtual bool IsOwnedItem { get { if (OwnerId != Guid.Empty) { return true; } // legacy // Local trailer, special feature, theme video, etc. // An item that belongs to another item but is not part of the Parent-Child tree // This is a hack for now relying on ExtraType. Eventually we may need to persist this if (ParentId == Guid.Empty && !IsFolder && LocationType == LocationType.FileSystem) { return true; } return false; } } public BaseItem GetOwner() { var ownerId = OwnerId; return ownerId == Guid.Empty ? null : LibraryManager.GetItemById(ownerId); } /// /// Gets or sets the type of the location. /// /// The type of the location. [IgnoreDataMember] public virtual LocationType LocationType { get { //if (IsOffline) //{ // return LocationType.Offline; //} if (string.IsNullOrWhiteSpace(Path)) { if (SourceType == SourceType.Channel) { return LocationType.Remote; } return LocationType.Virtual; } return FileSystem.IsPathFile(Path) ? LocationType.FileSystem : LocationType.Remote; } } [IgnoreDataMember] public virtual bool SupportsLocalMetadata { get { if (SourceType == SourceType.Channel) { return false; } var locationType = LocationType; return locationType != LocationType.Remote && locationType != LocationType.Virtual; } } [IgnoreDataMember] public virtual string FileNameWithoutExtension { get { if (LocationType == LocationType.FileSystem) { return System.IO.Path.GetFileNameWithoutExtension(Path); } return null; } } [IgnoreDataMember] public virtual bool EnableAlphaNumericSorting { get { return true; } } private List> GetSortChunks(string s1) { var list = new List>(); int thisMarker = 0, thisNumericChunk = 0; while (thisMarker < s1.Length) { if (thisMarker >= s1.Length) { break; } char thisCh = s1[thisMarker]; StringBuilder thisChunk = new StringBuilder(); while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) { thisChunk.Append(thisCh); thisMarker++; if (thisMarker < s1.Length) { thisCh = s1[thisMarker]; } } var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); list.Add(new Tuple(thisChunk, isNumeric)); } return list; } /// /// This is just a helper for convenience /// /// The primary image path. [IgnoreDataMember] public string PrimaryImagePath { get { return this.GetImagePath(ImageType.Primary); } } public virtual bool IsInternetMetadataEnabled() { return LibraryManager.GetLibraryOptions(this).EnableInternetProviders; } public virtual bool CanDelete() { if (SourceType == SourceType.Channel) { return false; } var locationType = LocationType; return locationType != LocationType.Remote && locationType != LocationType.Virtual; } public virtual bool IsAuthorizedToDelete(User user, List allCollectionFolders) { if (user.Policy.EnableContentDeletion) { return true; } var allowed = user.Policy.EnableContentDeletionFromFolders; var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); foreach (var folder in collectionFolders) { if (allowed.Contains(folder.Id.ToString("N"), 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.Policy.EnableContentDownloading; } public bool CanDownload(User user) { return CanDownload() && IsAuthorizedToDownload(user); } /// /// Gets or sets the date created. /// /// The date created. [IgnoreDataMember] public DateTime DateCreated { get; set; } /// /// Gets or sets the date modified. /// /// The date modified. [IgnoreDataMember] public DateTime DateModified { get; set; } [IgnoreDataMember] public DateTime DateLastSaved { get; set; } [IgnoreDataMember] public DateTime DateLastRefreshed { get; set; } [IgnoreDataMember] public virtual bool EnableRefreshOnDateModifiedChange { get { return false; } } /// /// 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 ILiveTvManager LiveTvManager { get; set; } public static IChannelManager ChannelManager { get; set; } public static ICollectionManager CollectionManager { get; set; } public static IImageProcessor ImageProcessor { get; set; } public static IMediaSourceManager MediaSourceManager { get; set; } public static IMediaEncoder MediaEncoder { get; set; } /// /// Returns a that represents this instance. /// /// A that represents this instance. public override string ToString() { return Name; } [IgnoreDataMember] public bool IsLocked { get; set; } /// /// Gets or sets the locked fields. /// /// The locked fields. [IgnoreDataMember] public MetadataFields[] LockedFields { get; set; } /// /// Gets the type of the media. /// /// The type of the media. [IgnoreDataMember] public virtual string MediaType { get { return null; } } [IgnoreDataMember] public virtual string[] PhysicalLocations { get { var locationType = LocationType; if (locationType == LocationType.Remote || locationType == LocationType.Virtual) { return new string[] { }; } return new[] { Path }; } } private string _forcedSortName; /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. [IgnoreDataMember] public string ForcedSortName { get { return _forcedSortName; } set { _forcedSortName = value; _sortName = null; } } private string _sortName; /// /// Gets the name of the sort. /// /// The name of the sort. [IgnoreDataMember] public string SortName { get { if (_sortName == null) { if (!string.IsNullOrWhiteSpace(ForcedSortName)) { // Need the ToLower because that's what CreateSortName does _sortName = ModifySortChunks(ForcedSortName).ToLower(); } 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, Id.ToString("N")); } var idString = Id.ToString("N"); 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().ToLower(); 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.Debug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); } [IgnoreDataMember] public Guid ParentId { get; set; } /// /// Gets or sets the parent. /// /// The parent. [IgnoreDataMember] public Folder Parent { get { return GetParent() as Folder; } set { } } public void SetParent(Folder parent) { ParentId = parent == null ? Guid.Empty : parent.Id; } public BaseItem GetParent() { var parentId = ParentId; if (parentId != 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; } [IgnoreDataMember] public virtual Guid? DisplayParentId { get { var parentId = ParentId; if (parentId == Guid.Empty) { return null; } return parentId; } } [IgnoreDataMember] public BaseItem DisplayParent { get { var id = DisplayParentId; if (!id.HasValue || id.Value == Guid.Empty) { return null; } return LibraryManager.GetItemById(id.Value); } } /// /// When the item first debuted. For movies this could be premiere date, episodes would be first aired /// /// The premiere date. [IgnoreDataMember] public DateTime? PremiereDate { get; set; } /// /// Gets or sets the end date. /// /// The end date. [IgnoreDataMember] public DateTime? EndDate { get; set; } /// /// Gets or sets the official rating. /// /// The official rating. [IgnoreDataMember] public string OfficialRating { get; set; } [IgnoreDataMember] public int InheritedParentalRatingValue { get; set; } /// /// Gets or sets the critic rating. /// /// The critic rating. [IgnoreDataMember] public float? CriticRating { get; set; } /// /// Gets or sets the custom rating. /// /// The custom rating. [IgnoreDataMember] public string CustomRating { get; set; } /// /// Gets or sets the overview. /// /// The overview. [IgnoreDataMember] public string Overview { get; set; } /// /// Gets or sets the studios. /// /// The studios. [IgnoreDataMember] public string[] Studios { get; set; } /// /// Gets or sets the genres. /// /// The genres. [IgnoreDataMember] public List Genres { get; set; } /// /// Gets or sets the tags. /// /// The tags. [IgnoreDataMember] public string[] Tags { get; set; } [IgnoreDataMember] public string[] ProductionLocations { get; set; } /// /// Gets or sets the home page URL. /// /// The home page URL. [IgnoreDataMember] public string HomePageUrl { get; set; } /// /// Gets or sets the community rating. /// /// The community rating. [IgnoreDataMember] public float? CommunityRating { get; set; } /// /// Gets or sets the run time ticks. /// /// The run time ticks. [IgnoreDataMember] public long? RunTimeTicks { get; set; } /// /// Gets or sets the production year. /// /// The production year. [IgnoreDataMember] 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. [IgnoreDataMember] 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. [IgnoreDataMember] public int? ParentIndexNumber { get; set; } [IgnoreDataMember] public string OfficialRatingForComparison { get { var officialRating = OfficialRating; if (!string.IsNullOrWhiteSpace(officialRating)) { return officialRating; } var parent = DisplayParent; if (parent != null) { return parent.OfficialRatingForComparison; } return null; } } [IgnoreDataMember] public string CustomRatingForComparison { get { var customRating = CustomRating; if (!string.IsNullOrWhiteSpace(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.Policy.EnableMediaPlayback) { return PlayAccess.None; } //if (!user.IsParentalScheduleAllowed()) //{ // return PlayAccess.None; //} return PlayAccess.Full; } /// /// 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