diff options
Diffstat (limited to 'MediaBrowser.Controller/Entities/BaseItem.cs')
| -rw-r--r-- | MediaBrowser.Controller/Entities/BaseItem.cs | 560 |
1 files changed, 258 insertions, 302 deletions
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index de5516e29..8dcf08642 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -21,17 +21,16 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class BaseItem /// </summary> - public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData, IHasMetadata + public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData, IHasMetadata, IHasLookupInfo<ItemLookupInfo> { protected BaseItem() { Genres = new List<string>(); Studios = new List<string>(); People = new List<PersonInfo>(); - BackdropImagePaths = new List<string>(); - Images = new Dictionary<ImageType, string>(); ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); LockedFields = new List<MetadataFields>(); + ImageInfos = new List<ItemImageInfo>(); } /// <summary> @@ -48,6 +47,12 @@ namespace MediaBrowser.Controller.Entities public const string ThemeVideosFolderName = "backdrops"; public const string XbmcTrailerFileSuffix = "-trailer"; + public List<ItemImageInfo> ImageInfos { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance is in mixed folder. + /// </summary> + /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value> public bool IsInMixedFolder { get; set; } private string _name; @@ -119,7 +124,7 @@ namespace MediaBrowser.Controller.Entities } [IgnoreDataMember] - public bool IsOwnedItem + public virtual bool IsOwnedItem { get { @@ -152,6 +157,16 @@ namespace MediaBrowser.Controller.Entities } } + public virtual bool SupportsLocalMetadata + { + get + { + var locationType = LocationType; + + return locationType == LocationType.FileSystem || locationType == LocationType.Offline; + } + } + /// <summary> /// This is just a helper for convenience /// </summary> @@ -160,16 +175,9 @@ namespace MediaBrowser.Controller.Entities public string PrimaryImagePath { get { return this.GetImagePath(ImageType.Primary); } - set { this.SetImagePath(ImageType.Primary, value); } } /// <summary> - /// Gets or sets the images. - /// </summary> - /// <value>The images.</value> - public Dictionary<ImageType, string> Images { get; set; } - - /// <summary> /// Gets or sets the date created. /// </summary> /// <value>The date created.</value> @@ -236,7 +244,7 @@ namespace MediaBrowser.Controller.Entities { var locationType = LocationType; - if (locationType != LocationType.Remote && locationType != LocationType.Virtual) + if (locationType == LocationType.Remote || locationType == LocationType.Virtual) { return new string[] { }; } @@ -258,7 +266,7 @@ namespace MediaBrowser.Controller.Entities private string _sortName; /// <summary> - /// Gets or sets the name of the sort. + /// Gets the name of the sort. /// </summary> /// <value>The name of the sort.</value> [IgnoreDataMember] @@ -350,12 +358,6 @@ namespace MediaBrowser.Controller.Entities public string DisplayMediaType { get; set; } /// <summary> - /// Gets or sets the backdrop image paths. - /// </summary> - /// <value>The backdrop image paths.</value> - public List<string> BackdropImagePaths { get; set; } - - /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> @@ -447,9 +449,23 @@ namespace MediaBrowser.Controller.Entities } [IgnoreDataMember] - public virtual string CustomRatingForComparison + public string CustomRatingForComparison { - get { return CustomRating; } + get + { + if (!string.IsNullOrEmpty(CustomRating)) + { + return CustomRating; + } + + var parent = Parent; + if (parent != null) + { + return parent.CustomRatingForComparison; + } + + return null; + } } /// <summary> @@ -458,75 +474,30 @@ namespace MediaBrowser.Controller.Entities /// <returns>List{Video}.</returns> private IEnumerable<Trailer> LoadLocalTrailers(List<FileSystemInfo> fileSystemChildren) { - return new List<Trailer>(); - //ItemResolveArgs resolveArgs; - - //try - //{ - // resolveArgs = ResolveArgs; - - // if (!resolveArgs.IsDirectory) - // { - // return new List<Trailer>(); - // } - //} - //catch (IOException ex) - //{ - // Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); - // return new List<Trailer>(); - //} - - //var files = new List<FileSystemInfo>(); - - //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<Trailer>(files, null).Select(video => - //{ - // // Try to retrieve it from the db. If we don't find it, use the resolved version - // var dbItem = LibraryManager.GetItemById(video.Id) as Trailer; - - // if (dbItem != null) - // { - // video = dbItem; - // } - - // return video; - - //}).ToList(); + var files = fileSystemChildren.OfType<DirectoryInfo>() + .Where(i => string.Equals(i.Name, TrailerFolderName, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + .ToList(); + + // Support plex/xbmc convention + files.AddRange(fileSystemChildren.OfType<FileInfo>() + .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.Name).EndsWith(XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) && !string.Equals(Path, i.FullName, StringComparison.OrdinalIgnoreCase)) + ); + + return LibraryManager.ResolvePaths<Trailer>(files, null).Select(video => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(video.Id) as Trailer; + + if (dbItem != null) + { + video = dbItem; + } + + return video; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } /// <summary> @@ -556,7 +527,9 @@ namespace MediaBrowser.Controller.Entities } return audio; - }).ToList(); + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } /// <summary> @@ -580,7 +553,9 @@ namespace MediaBrowser.Controller.Entities } return item; - }).ToList(); + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } public Task RefreshMetadata(CancellationToken cancellationToken) @@ -598,19 +573,44 @@ namespace MediaBrowser.Controller.Entities { var locationType = LocationType; + var requiresSave = false; + if (IsFolder || Parent != null) { + options.DirectoryService = options.DirectoryService ?? new DirectoryService(Logger); + var files = locationType == LocationType.FileSystem || locationType == LocationType.Offline ? - GetFileSystemChildren().ToList() : + GetFileSystemChildren(options.DirectoryService).ToList() : new List<FileSystemInfo>(); - await BeforeRefreshMetadata(options, files, cancellationToken).ConfigureAwait(false); + var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); + + if (ownedItemsChanged) + { + requiresSave = true; + } } + var dateLastSaved = DateLastSaved; + await ProviderManager.RefreshMetadata(this, options, cancellationToken).ConfigureAwait(false); + + // If it wasn't saved by the provider process, save now + if (requiresSave && dateLastSaved == DateLastSaved) + { + await UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } } - protected virtual async Task BeforeRefreshMetadata(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + /// <summary> + /// Refreshes owned items such as trailers, theme videos, special features, etc. + /// Returns true or false indicating if changes were found. + /// </summary> + /// <param name="options"></param> + /// <param name="fileSystemChildren"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { var themeSongsChanged = false; @@ -637,18 +637,15 @@ namespace MediaBrowser.Controller.Entities localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); } } - - if (themeSongsChanged || themeVideosChanged || localTrailersChanged) - { - options.ForceSave = true; - } + + return themeSongsChanged || themeVideosChanged || localTrailersChanged; } - protected virtual IEnumerable<FileSystemInfo> GetFileSystemChildren() + protected virtual IEnumerable<FileSystemInfo> GetFileSystemChildren(IDirectoryService directoryService) { var path = ContainingFolderPath; - return new DirectoryInfo(path).EnumerateFileSystemInfos("*", SearchOption.TopDirectoryOnly); + return directoryService.GetFileSystemEntries(path); } private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) @@ -670,6 +667,7 @@ namespace MediaBrowser.Controller.Entities private async Task<bool> RefreshThemeVideos(IHasThemeMedia item, MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { var newThemeVideos = LoadThemeVideos(fileSystemChildren).ToList(); + var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToList(); var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds); @@ -885,29 +883,6 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Finds the particular item by searching through our parents and, if not found there, loading from repo - /// </summary> - /// <param name="id">The id.</param> - /// <returns>BaseItem.</returns> - /// <exception cref="System.ArgumentException"></exception> - protected BaseItem FindParentItem(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentException(); - } - - var parent = Parent; - while (parent != null && !parent.IsRoot) - { - if (parent.Id == id) return parent; - parent = parent.Parent; - } - - return null; - } - - /// <summary> /// Gets a value indicating whether this instance is folder. /// </summary> /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> @@ -1188,43 +1163,31 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> public bool HasImage(ImageType type, int imageIndex) { - if (type == ImageType.Backdrop) - { - return BackdropImagePaths.Count > imageIndex; - } - if (type == ImageType.Screenshot) - { - var hasScreenshots = this as IHasScreenshots; - return hasScreenshots != null && hasScreenshots.ScreenshotImagePaths.Count > imageIndex; - } - - return !string.IsNullOrEmpty(this.GetImagePath(type)); + return GetImageInfo(type, imageIndex) != null; } - public void SetImagePath(ImageType type, int index, string path) + public void SetImagePath(ImageType type, int index, FileInfo file) { - if (type == ImageType.Backdrop) - { - throw new ArgumentException("Backdrops should be accessed using Item.Backdrops"); - } - if (type == ImageType.Screenshot) + if (type == ImageType.Chapter) { - throw new ArgumentException("Screenshots should be accessed using Item.Screenshots"); + throw new ArgumentException("Cannot set chapter images using SetImagePath"); } - var typeKey = type; + var image = GetImageInfo(type, index); - // If it's null remove the key from the dictionary - if (string.IsNullOrEmpty(path)) + if (image == null) { - if (Images.ContainsKey(typeKey)) + ImageInfos.Add(new ItemImageInfo { - Images.Remove(typeKey); - } + Path = file.FullName, + Type = type, + DateModified = FileSystem.GetLastWriteTimeUtc(file) + }); } else { - Images[typeKey] = path; + image.Path = file.FullName; + image.DateModified = FileSystem.GetLastWriteTimeUtc(file); } } @@ -1234,66 +1197,23 @@ namespace MediaBrowser.Controller.Entities /// <param name="type">The type.</param> /// <param name="index">The index.</param> /// <returns>Task.</returns> - public Task DeleteImage(ImageType type, int? index) + public Task DeleteImage(ImageType type, int index) { - if (type == ImageType.Backdrop) - { - if (!index.HasValue) - { - throw new ArgumentException("Please specify a backdrop image index to delete."); - } - - var file = BackdropImagePaths[index.Value]; + var info = GetImageInfo(type, index); - BackdropImagePaths.Remove(file); - - // Delete the source file - DeleteImagePath(file); - } - else if (type == ImageType.Screenshot) + if (info == null) { - if (!index.HasValue) - { - throw new ArgumentException("Please specify a screenshot image index to delete."); - } - - var hasScreenshots = (IHasScreenshots)this; - var file = hasScreenshots.ScreenshotImagePaths[index.Value]; - - hasScreenshots.ScreenshotImagePaths.Remove(file); - - // Delete the source file - DeleteImagePath(file); + // Nothing to do + return Task.FromResult(true); } - else - { - // Delete the source file - DeleteImagePath(this.GetImagePath(type)); - // Remove it from the item - this.SetImagePath(type, null); - } - - // Refresh metadata - // Need to disable slow providers or the image might get re-downloaded - return RefreshMetadata(new MetadataRefreshOptions - { - ForceSave = true, - ImageRefreshMode = ImageRefreshMode.ValidationOnly, - MetadataRefreshMode = MetadataRefreshMode.None + // Remove it from the item + ImageInfos.Remove(info); - }, CancellationToken.None); - } - - /// <summary> - /// Deletes the image path. - /// </summary> - /// <param name="path">The path.</param> - private void DeleteImagePath(string path) - { - var currentFile = new FileInfo(path); + // Delete the source file + var currentFile = new FileInfo(info.Path); - // This will fail if the file is hidden + // Deletion will fail if the file is hidden so remove the attribute first if (currentFile.Exists) { if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) @@ -1303,90 +1223,33 @@ namespace MediaBrowser.Controller.Entities currentFile.Delete(); } - } - - /// <summary> - /// Validates that images within the item are still on the file system - /// </summary> - public bool ValidateImages() - { - var changed = false; - - // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below - var deletedKeys = Images - .Where(image => !File.Exists(image.Value)) - .Select(i => i.Key) - .ToList(); - - // Now remove them from the dictionary - foreach (var key in deletedKeys) - { - Images.Remove(key); - changed = true; - } - - if (ValidateBackdrops()) - { - changed = true; - } - if (ValidateScreenshots()) - { - changed = true; - } - return changed; + return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } - /// <summary> - /// Validates that backdrops within the item are still on the file system - /// </summary> - private bool ValidateBackdrops() + public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) { - var changed = false; - - // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below - var deletedImages = BackdropImagePaths - .Where(path => !File.Exists(path)) - .ToList(); - - // Now remove them from the dictionary - foreach (var path in deletedImages) - { - BackdropImagePaths.Remove(path); - - changed = true; - } - - return changed; + return LibraryManager.UpdateItem(this, ItemUpdateType.ImageUpdate, cancellationToken); } /// <summary> - /// Validates the screenshots. + /// Validates that images within the item are still on the file system /// </summary> - private bool ValidateScreenshots() + public bool ValidateImages(IDirectoryService directoryService) { - var changed = false; + var allDirectories = ImageInfos.Select(i => System.IO.Path.GetDirectoryName(i.Path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var allFiles = allDirectories.SelectMany(directoryService.GetFiles).Select(i => i.FullName).ToList(); - var hasScreenshots = this as IHasScreenshots; - - if (hasScreenshots == null) - { - return changed; - } - - // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below - var deletedImages = hasScreenshots.ScreenshotImagePaths - .Where(path => !File.Exists(path)) + var deletedImages = ImageInfos + .Where(image => !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase)) .ToList(); - // Now remove them from the dictionary - foreach (var path in deletedImages) + if (deletedImages.Count > 0) { - hasScreenshots.ScreenshotImagePaths.Remove(path); - changed = true; + ImageInfos = ImageInfos.Except(deletedImages).ToList(); } - return changed; + return deletedImages.Count > 0; } /// <summary> @@ -1400,42 +1263,87 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="System.ArgumentNullException">item</exception> public string GetImagePath(ImageType imageType, int imageIndex) { - if (imageType == ImageType.Backdrop) - { - return BackdropImagePaths.Count > imageIndex ? BackdropImagePaths[imageIndex] : null; - } + var info = GetImageInfo(imageType, imageIndex); + + return info == null ? null : info.Path; + } - if (imageType == ImageType.Screenshot) + /// <summary> + /// Gets the image information. + /// </summary> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>ItemImageInfo.</returns> + public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex) + { + if (imageType == ImageType.Chapter) { - var hasScreenshots = (IHasScreenshots)this; - return hasScreenshots.ScreenshotImagePaths.Count > imageIndex ? hasScreenshots.ScreenshotImagePaths[imageIndex] : null; + var chapter = ItemRepository.GetChapter(Id, imageIndex); + + if (chapter == null) + { + return null; + } + + var path = chapter.ImagePath; + + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + return new ItemImageInfo + { + Path = path, + DateModified = FileSystem.GetLastWriteTimeUtc(path), + Type = imageType + }; } + return GetImages(imageType) + .ElementAtOrDefault(imageIndex); + } + + public IEnumerable<ItemImageInfo> GetImages(ImageType imageType) + { if (imageType == ImageType.Chapter) { - return ItemRepository.GetChapter(Id, imageIndex).ImagePath; + throw new ArgumentException("No image info for chapter images"); } - string val; - Images.TryGetValue(imageType, out val); - return val; + return ImageInfos.Where(i => i.Type == imageType); } /// <summary> - /// Gets the image date modified. + /// Adds the images. /// </summary> - /// <param name="imagePath">The image path.</param> - /// <returns>DateTime.</returns> - /// <exception cref="System.ArgumentNullException">item</exception> - public DateTime GetImageDateModified(string imagePath) + /// <param name="imageType">Type of the image.</param> + /// <param name="images">The images.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception> + public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images) { - if (string.IsNullOrEmpty(imagePath)) + if (imageType == ImageType.Chapter) { - throw new ArgumentNullException("imagePath"); + throw new ArgumentException("Cannot call AddImages with chapter images"); } - // See if we can avoid a file system lookup by looking for the file in ResolveArgs - return FileSystem.GetLastWriteTimeUtc(imagePath); + var existingImagePaths = GetImages(imageType) + .Select(i => i.Path) + .ToList(); + + var newImages = images + .Where(i => !existingImagePaths.Contains(i.FullName, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + ImageInfos.AddRange(newImages.Select(i => new ItemImageInfo + { + Path = i.FullName, + Type = imageType, + DateModified = FileSystem.GetLastWriteTimeUtc(i) + })); + + return newImages.Count > 0; } /// <summary> @@ -1447,25 +1355,37 @@ namespace MediaBrowser.Controller.Entities return new[] { Path }; } + public bool AllowsMultipleImages(ImageType type) + { + return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter; + } + public Task SwapImages(ImageType type, int index1, int index2) { - if (type != ImageType.Screenshot && type != ImageType.Backdrop) + if (!AllowsMultipleImages(type)) { throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); } - var file1 = GetImagePath(type, index1); - var file2 = GetImagePath(type, index2); - - FileSystem.SwapFiles(file1, file2); + var info1 = GetImageInfo(type, index1); + var info2 = GetImageInfo(type, index2); - // Directory watchers should repeat this, but do a quick refresh first - return RefreshMetadata(new MetadataRefreshOptions + if (info1 == null || info2 == null) { - ForceSave = true, - MetadataRefreshMode = MetadataRefreshMode.None + // Nothing to do + return Task.FromResult(true); + } - }, CancellationToken.None); + var path1 = info1.Path; + var path2 = info2.Path; + + FileSystem.SwapFiles(path1, path2); + + // Refresh these values + info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path); + info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path); + + return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } public virtual bool IsPlayed(User user) @@ -1481,5 +1401,41 @@ namespace MediaBrowser.Controller.Entities return userdata == null || !userdata.Played; } + + ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo() + { + return GetItemLookupInfo<ItemLookupInfo>(); + } + + protected T GetItemLookupInfo<T>() + where T : ItemLookupInfo, new() + { + return new T + { + MetadataCountryCode = GetPreferredMetadataCountryCode(), + MetadataLanguage = GetPreferredMetadataLanguage(), + Name = Name, + ProviderIds = ProviderIds, + IndexNumber = IndexNumber, + ParentIndexNumber = ParentIndexNumber + }; + } + + /// <summary> + /// This is called before any metadata refresh and returns ItemUpdateType indictating if changes were made, and what. + /// </summary> + /// <returns>ItemUpdateType.</returns> + public virtual ItemUpdateType BeforeMetadataRefresh() + { + var updateType = ItemUpdateType.None; + + if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path)) + { + Name = System.IO.Path.GetFileNameWithoutExtension(Path); + updateType = updateType | ItemUpdateType.MetadataEdit; + } + + return updateType; + } } } |
