using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities { /// /// Class BaseItem /// public abstract class BaseItem : IHasProviderIds { /// /// MusicAlbums in the library that are the soundtrack for this item /// public List SoundtrackIds { get; set; } protected BaseItem() { Genres = new List(); RemoteTrailers = new List(); Studios = new List(); People = new List(); Taglines = new List(); ScreenshotImagePaths = new List(); BackdropImagePaths = new List(); ProductionLocations = new List(); Images = new Dictionary(); ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); Tags = new List(); ThemeSongIds = new List(); ThemeVideoIds = new List(); SoundtrackIds = new List(); LocalTrailerIds = new List(); LockedFields = new List(); } /// /// The supported image extensions /// public static readonly string[] SupportedImageExtensions = new[] { ".png", ".jpg", ".jpeg" }; /// /// 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 XbmcTrailerFileSuffix = "-trailer"; public bool IsInMixedFolder { get; set; } private string _name; /// /// Gets or sets the name. /// /// The name. public string Name { get { return _name; } set { _name = value; // lazy load this again _sortName = null; } } /// /// Gets or sets the id. /// /// The id. public Guid Id { 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. public virtual string Path { get; set; } [IgnoreDataMember] protected internal bool IsOffline { get; set; } /// /// Gets or sets the type of the location. /// /// The type of the location. public virtual LocationType LocationType { get { if (IsOffline) { return LocationType.Offline; } if (string.IsNullOrEmpty(Path)) { return LocationType.Virtual; } return System.IO.Path.IsPathRooted(Path) ? LocationType.FileSystem : LocationType.Remote; } } /// /// This is just a helper for convenience /// /// The primary image path. [IgnoreDataMember] public string PrimaryImagePath { get { return GetImage(ImageType.Primary); } set { SetImage(ImageType.Primary, value); } } /// /// Gets or sets the images. /// /// The images. public Dictionary Images { get; set; } /// /// Gets or sets the date created. /// /// The date created. public DateTime DateCreated { get; set; } /// /// Gets or sets the date modified. /// /// The date modified. public DateTime DateModified { 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; } /// /// Returns a that represents this instance. /// /// A that represents this instance. public override string ToString() { return Name; } /// /// Returns true if this item should not attempt to fetch metadata /// /// true if [dont fetch meta]; otherwise, false. public bool DontFetchMeta { get; set; } /// /// Gets or sets the locked fields. /// /// The locked fields. public List LockedFields { get; set; } /// /// Determines whether the item has a saved local image of the specified name (jpg or png). /// /// The name. /// true if [has local image] [the specified item]; otherwise, false. /// name public bool HasLocalImage(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } return ResolveArgs.ContainsMetaFileByName(name + ".jpg") || ResolveArgs.ContainsMetaFileByName(name + ".png"); } /// /// Should be overridden to return the proper folder where metadata lives /// /// The meta location. [IgnoreDataMember] public virtual string MetaLocation { get { return Path ?? ""; } } /// /// The _provider data /// private Dictionary _providerData; /// /// Holds persistent data for providers like last refresh date. /// Providers can use this to determine if they need to refresh. /// The BaseProviderInfo class can be extended to hold anything a provider may need. /// Keyed by a unique provider ID. /// /// The provider data. public Dictionary ProviderData { get { return _providerData ?? (_providerData = new Dictionary()); } set { _providerData = value; } } /// /// Gets the type of the media. /// /// The type of the media. [IgnoreDataMember] public virtual string MediaType { get { return null; } } /// /// The _resolve args /// private ItemResolveArgs _resolveArgs; /// /// The _resolve args initialized /// private bool _resolveArgsInitialized; /// /// The _resolve args sync lock /// private object _resolveArgsSyncLock = new object(); /// /// We attach these to the item so that we only ever have to hit the file system once /// (this includes the children of the containing folder) /// /// The resolve args. [IgnoreDataMember] public ItemResolveArgs ResolveArgs { get { try { LazyInitializer.EnsureInitialized(ref _resolveArgs, ref _resolveArgsInitialized, ref _resolveArgsSyncLock, () => CreateResolveArgs()); } catch (IOException ex) { Logger.ErrorException("Error creating resolve args for {0}", ex, Path); IsOffline = true; throw; } return _resolveArgs; } set { _resolveArgs = value; _resolveArgsInitialized = value != null; } } /// /// Resets the resolve args. /// /// The path info. public void ResetResolveArgs(FileSystemInfo pathInfo) { ResolveArgs = CreateResolveArgs(pathInfo); } /// /// Creates ResolveArgs on demand /// /// The path info. /// ItemResolveArgs. /// Unable to retrieve file system info for + path protected internal virtual ItemResolveArgs CreateResolveArgs(FileSystemInfo pathInfo = null) { var path = Path; if (LocationType == LocationType.Remote || LocationType == LocationType.Virtual) { return new ItemResolveArgs(ConfigurationManager.ApplicationPaths); } var isDirectory = false; if (UseParentPathToCreateResolveArgs) { path = System.IO.Path.GetDirectoryName(path); isDirectory = true; } pathInfo = pathInfo ?? (isDirectory ? new DirectoryInfo(path) : FileSystem.GetFileSystemInfo(path)); if (pathInfo == null || !pathInfo.Exists) { throw new IOException("Unable to retrieve file system info for " + path); } var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths) { FileInfo = pathInfo, Path = path, Parent = Parent }; // Gather child folder and files if (args.IsDirectory) { var isPhysicalRoot = args.IsPhysicalRoot; // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { var paths = args.FileSystemDictionary.Keys.ToList(); foreach (var subPath in paths .Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && paths.Any(i => subPath.StartsWith(i.TrimEnd(System.IO.Path.DirectorySeparatorChar) + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))) { Logger.Info("Ignoring duplicate path: {0}", subPath); args.FileSystemDictionary.Remove(subPath); } } } //update our dates EntityResolutionHelper.EnsureDates(this, args, false); IsOffline = false; return args; } /// /// Some subclasses will stop resolving at a directory and point their Path to a file within. This will help ensure the on-demand resolve args are identical to the /// original ones. /// /// true if [use parent path to create resolve args]; otherwise, false. [IgnoreDataMember] protected virtual bool UseParentPathToCreateResolveArgs { get { return false; } } /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. public string ForcedSortName { get; set; } private string _sortName; /// /// Gets or sets the name of the sort. /// /// The name of the sort. [IgnoreDataMember] public string SortName { get { if (!string.IsNullOrEmpty(ForcedSortName)) { return ForcedSortName; } return _sortName ?? (_sortName = CreateSortName()); } } /// /// 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 var sortable = Name.Trim().ToLower(); sortable = ConfigurationManager.Configuration.SortRemoveCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), string.Empty)); sortable = ConfigurationManager.Configuration.SortReplaceCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), " ")); foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { var searchLower = search.ToLower(); // Remove from beginning if a space follows if (sortable.StartsWith(searchLower + " ")) { sortable = sortable.Remove(0, searchLower.Length + 1); } // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + searchLower + " ", " "); // Remove from end if followed by a space if (sortable.EndsWith(" " + searchLower)) { sortable = sortable.Remove(sortable.Length - (searchLower.Length + 1)); } } return sortable; } /// /// Gets or sets the parent. /// /// The parent. [IgnoreDataMember] public Folder Parent { get; set; } /// /// When the item first debuted. For movies this could be premiere date, episodes would be first aired /// /// The premiere date. public DateTime? PremiereDate { get; set; } /// /// Gets or sets the end date. /// /// The end date. public DateTime? EndDate { get; set; } /// /// Gets or sets the display type of the media. /// /// The display type of the media. public string DisplayMediaType { get; set; } /// /// Gets or sets the backdrop image paths. /// /// The backdrop image paths. public List BackdropImagePaths { get; set; } /// /// Gets or sets the screenshot image paths. /// /// The screenshot image paths. public List ScreenshotImagePaths { get; set; } /// /// Gets or sets the official rating. /// /// The official rating. public string OfficialRating { get; set; } /// /// Gets or sets the official rating description. /// /// The official rating description. public string OfficialRatingDescription { get; set; } /// /// Gets or sets the custom rating. /// /// The custom rating. public string CustomRating { get; set; } /// /// Gets or sets the language. /// /// The language. public string Language { get; set; } /// /// Gets or sets the overview. /// /// The overview. public string Overview { get; set; } /// /// Gets or sets the taglines. /// /// The taglines. public List Taglines { get; set; } /// /// Gets or sets the people. /// /// The people. public List People { get; set; } /// /// Gets or sets the tags. /// /// The tags. public List Tags { get; set; } /// /// Override this if you need to combine/collapse person information /// /// All people. [IgnoreDataMember] public virtual IEnumerable AllPeople { get { return People; } } [IgnoreDataMember] public virtual IEnumerable AllStudios { get { return Studios; } } [IgnoreDataMember] public virtual IEnumerable AllGenres { get { return Genres; } } /// /// Gets or sets the studios. /// /// The studios. public List Studios { get; set; } /// /// Gets or sets the genres. /// /// The genres. public List Genres { get; set; } /// /// Gets or sets the home page URL. /// /// The home page URL. public string HomePageUrl { get; set; } /// /// Gets or sets the budget. /// /// The budget. public double? Budget { get; set; } /// /// Gets or sets the revenue. /// /// The revenue. public double? Revenue { get; set; } /// /// Gets or sets the production locations. /// /// The production locations. public List ProductionLocations { get; set; } /// /// Gets or sets the critic rating. /// /// The critic rating. public float? CriticRating { get; set; } /// /// Gets or sets the critic rating summary. /// /// The critic rating summary. public string CriticRatingSummary { get; set; } /// /// Gets or sets the community rating. /// /// The community rating. public float? CommunityRating { get; set; } /// /// Gets or sets the community rating vote count. /// /// The community rating vote count. public int? VoteCount { get; set; } /// /// Gets or sets the run time ticks. /// /// The run time ticks. public long? RunTimeTicks { get; set; } /// /// Gets or sets the original run time ticks. /// /// The original run time ticks. public long? OriginalRunTimeTicks { get; set; } /// /// Gets or sets the aspect ratio. /// /// The aspect ratio. public string AspectRatio { get; set; } /// /// Gets or sets the production year. /// /// The production year. 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. 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. public int? ParentIndexNumber { get; set; } public List ThemeSongIds { get; set; } public List ThemeVideoIds { get; set; } public List LocalTrailerIds { get; set; } [IgnoreDataMember] public virtual string OfficialRatingForComparison { get { return OfficialRating; } } [IgnoreDataMember] public virtual string CustomRatingForComparison { get { return CustomRating; } } /// /// Loads local trailers from the file system /// /// List{Video}. private IEnumerable LoadLocalTrailers() { ItemResolveArgs resolveArgs; try { resolveArgs = ResolveArgs; if (!resolveArgs.IsDirectory) { return new List(); } } catch (IOException ex) { Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); return new List(); } var files = new List(); var folder = resolveArgs.GetFileSystemEntryByName(TrailerFolderName); // Path doesn't exist. No biggie if (folder != null) { try { files.AddRange(new DirectoryInfo(folder.FullName).EnumerateFiles()); } catch (IOException ex) { Logger.ErrorException("Error loading trailers for {0}", ex, Name); } } // Support xbmc trailers (-trailer suffix on video file names) files.AddRange(resolveArgs.FileSystemChildren.Where(i => { try { if ((i.Attributes & FileAttributes.Directory) != FileAttributes.Directory) { if (System.IO.Path.GetFileNameWithoutExtension(i.Name).EndsWith(XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) && !string.Equals(Path, i.FullName, StringComparison.OrdinalIgnoreCase)) { return true; } } } catch (IOException ex) { Logger.ErrorException("Error accessing path {0}", ex, i.FullName); } return false; })); return LibraryManager.ResolvePaths(files, null).Select(video => { // Try to retrieve it from the db. If we don't find it, use the resolved version var dbItem = LibraryManager.RetrieveItem(video.Id) as Trailer; if (dbItem != null) { dbItem.ResolveArgs = video.ResolveArgs; video = dbItem; } return video; }).ToList(); } /// /// Loads the theme songs. /// /// List{Audio.Audio}. private IEnumerable LoadThemeSongs() { ItemResolveArgs resolveArgs; try { resolveArgs = ResolveArgs; if (!resolveArgs.IsDirectory) { return new List(); } } catch (IOException ex) { Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); return new List(); } var files = new List(); var folder = resolveArgs.GetFileSystemEntryByName(ThemeSongsFolderName); // Path doesn't exist. No biggie if (folder != null) { try { files.AddRange(new DirectoryInfo(folder.FullName).EnumerateFiles()); } catch (IOException ex) { Logger.ErrorException("Error loading theme songs for {0}", ex, Name); } } // Support plex/xbmc convention files.AddRange(resolveArgs.FileSystemChildren .Where(i => string.Equals(System.IO.Path.GetFileNameWithoutExtension(i.Name), ThemeSongFilename, StringComparison.OrdinalIgnoreCase) && EntityResolutionHelper.IsAudioFile(i.Name)) ); return LibraryManager.ResolvePaths(files, null).Select(audio => { // Try to retrieve it from the db. If we don't find it, use the resolved version var dbItem = LibraryManager.RetrieveItem(audio.Id) as Audio.Audio; if (dbItem != null) { dbItem.ResolveArgs = audio.ResolveArgs; audio = dbItem; } return audio; }).ToList(); } /// /// Loads the video backdrops. /// /// List{Video}. private IEnumerable