diff options
Diffstat (limited to 'MediaBrowser.Controller/Entities')
33 files changed, 1320 insertions, 585 deletions
diff --git a/MediaBrowser.Controller/Entities/AdultVideo.cs b/MediaBrowser.Controller/Entities/AdultVideo.cs index 475d7bc54..fc7632152 100644 --- a/MediaBrowser.Controller/Entities/AdultVideo.cs +++ b/MediaBrowser.Controller/Entities/AdultVideo.cs @@ -1,7 +1,8 @@ - +using System.Collections.Generic; + namespace MediaBrowser.Controller.Entities { - public class AdultVideo : Video, IHasPreferredMetadataLanguage + public class AdultVideo : Video, IHasPreferredMetadataLanguage, IHasTaglines { /// <summary> /// Gets or sets the preferred metadata language. @@ -14,5 +15,12 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value>The preferred metadata country code.</value> public string PreferredMetadataCountryCode { get; set; } + + public List<string> Taglines { get; set; } + + public AdultVideo() + { + Taglines = new List<string>(); + } } } diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index ef455846e..5cabe1cfe 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; +using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { @@ -56,12 +57,12 @@ namespace MediaBrowser.Controller.Entities public List<string> PhysicalLocationsList { get; set; } - protected override IEnumerable<FileSystemInfo> GetFileSystemChildren() + protected override IEnumerable<FileSystemInfo> GetFileSystemChildren(IDirectoryService directoryService) { - return CreateResolveArgs().FileSystemChildren; + return CreateResolveArgs(directoryService).FileSystemChildren; } - private ItemResolveArgs CreateResolveArgs() + private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService) { var path = ContainingFolderPath; @@ -80,7 +81,7 @@ namespace MediaBrowser.Controller.Entities // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; - var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, FileSystem, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); + var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, 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 @@ -118,9 +119,9 @@ namespace MediaBrowser.Controller.Entities /// Get the children of this folder from the actual file system /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - protected override IEnumerable<BaseItem> GetNonCachedChildren() + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { - return base.GetNonCachedChildren().Concat(_virtualChildren); + return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); } /// <summary> diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 6f4a0c4d2..836874db9 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class Audio /// </summary> - public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres + public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<SongInfo>, IHasSeries { public Audio() { @@ -50,6 +51,15 @@ namespace MediaBrowser.Controller.Entities.Audio } } + [IgnoreDataMember] + public string SeriesName + { + get + { + return Album; + } + } + /// <summary> /// Gets or sets the artist. /// </summary> @@ -127,5 +137,16 @@ namespace MediaBrowser.Controller.Entities.Audio { return config.BlockUnratedMusic; } + + public SongInfo GetLookupInfo() + { + var info = GetItemLookupInfo<SongInfo>(); + + info.AlbumArtist = AlbumArtist; + info.Album = Album; + info.Artists = Artists; + + return info; + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index b3bf0d2b6..51c8a8727 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; @@ -10,10 +11,10 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicAlbum /// </summary> - public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasTags + public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasTags, IHasLookupInfo<AlbumInfo>, IHasSeries { public List<Guid> SoundtrackIds { get; set; } - + public MusicAlbum() { Artists = new List<string>(); @@ -21,6 +22,15 @@ namespace MediaBrowser.Controller.Entities.Audio Tags = new List<string>(); } + [IgnoreDataMember] + public MusicArtist MusicArtist + { + get + { + return Parents.OfType<MusicArtist>().FirstOrDefault(); + } + } + /// <summary> /// Gets or sets the tags. /// </summary> @@ -40,6 +50,15 @@ namespace MediaBrowser.Controller.Entities.Audio } } + [IgnoreDataMember] + public string SeriesName + { + get + { + return AlbumArtist; + } + } + /// <summary> /// Override this to true if class should be grouped under a container in indicies /// The container class should be defined via IndexContainer @@ -98,7 +117,7 @@ namespace MediaBrowser.Controller.Entities.Audio return "MusicAlbum-MusicBrainzReleaseGroup-" + id; } - id = this.GetProviderId(MetadataProviders.Musicbrainz); + id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum); if (!string.IsNullOrEmpty(id)) { @@ -112,6 +131,26 @@ namespace MediaBrowser.Controller.Entities.Audio { return config.BlockUnratedMusic; } + + public AlbumInfo GetLookupInfo() + { + var id = GetItemLookupInfo<AlbumInfo>(); + + id.AlbumArtist = AlbumArtist; + + var artist = Parents.OfType<MusicArtist>().FirstOrDefault(); + + if (artist != null) + { + id.ArtistProviderIds = artist.ProviderIds; + } + + id.SongInfos = RecursiveChildren.OfType<Audio>() + .Select(i => i.GetLookupInfo()) + .ToList(); + + return id; + } } public class MusicAlbumDisc : Folder diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 9b4e3a736..2b5570a80 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,8 +1,11 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; @@ -12,7 +15,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicArtist /// </summary> - public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasTags, IHasProductionLocations + public class MusicArtist : Folder, IMetadataContainer, IItemByName, IHasMusicGenres, IHasDualAccess, IHasTags, IHasProductionLocations, IHasLookupInfo<ArtistInfo> { [IgnoreDataMember] public List<ItemByNameCounts> UserItemCountList { get; set; } @@ -49,7 +52,7 @@ namespace MediaBrowser.Controller.Entities.Audio } private readonly Task _cachedTask = Task.FromResult(true); - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { if (IsAccessedByName) { @@ -57,17 +60,7 @@ namespace MediaBrowser.Controller.Entities.Audio return _cachedTask; } - return base.ValidateChildrenInternal(progress, cancellationToken, recursive, forceRefreshMetadata); - } - - public override string GetClientTypeName() - { - if (IsAccessedByName) - { - //return "Artist"; - } - - return base.GetClientTypeName(); + return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); } public MusicArtist() @@ -87,13 +80,38 @@ namespace MediaBrowser.Controller.Entities.Audio } /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } + + /// <summary> /// Gets the user data key. /// </summary> /// <param name="item">The item.</param> /// <returns>System.String.</returns> private static string GetUserDataKey(MusicArtist item) { - var id = item.GetProviderId(MetadataProviders.Musicbrainz); + var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); if (!string.IsNullOrEmpty(id)) { @@ -107,5 +125,107 @@ namespace MediaBrowser.Controller.Entities.Audio { return config.BlockUnratedMusic; } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + var items = RecursiveChildren.ToList(); + + var songs = items.OfType<Audio>().ToList(); + + var others = items.Except(songs).ToList(); + + var totalItems = songs.Count + others.Count; + var percentages = new Dictionary<Guid, double>(totalItems); + + var tasks = new List<Task>(); + + // Refresh songs + foreach (var item in songs) + { + if (tasks.Count >= 2) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); + } + + cancellationToken.ThrowIfCancellationRequested(); + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentChild = item; + innerProgress.RegisterAction(p => + { + lock (percentages) + { + percentages[currentChild.Id] = p / 100; + + var percent = percentages.Values.Sum(); + percent /= totalItems; + percent *= 100; + progress.Report(percent); + } + }); + + tasks.Add(RefreshItem(item, refreshOptions, innerProgress, cancellationToken)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); + + // Refresh current item + await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + // Refresh all non-songs + foreach (var item in others) + { + if (tasks.Count > 3) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); + } + + cancellationToken.ThrowIfCancellationRequested(); + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentChild = item; + innerProgress.RegisterAction(p => + { + lock (percentages) + { + percentages[currentChild.Id] = p / 100; + + var percent = percentages.Values.Sum(); + percent /= totalItems; + percent *= 100; + progress.Report(percent); + } + }); + + tasks.Add(RefreshItem(item, refreshOptions, innerProgress, cancellationToken)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + progress.Report(100); + } + + private async Task RefreshItem(BaseItem item, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + progress.Report(100); + } + + public ArtistInfo GetLookupInfo() + { + var info = GetItemLookupInfo<ArtistInfo>(); + + info.SongInfos = RecursiveChildren.OfType<Audio>() + .Select(i => i.GetLookupInfo()) + .ToList(); + + return info; + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index b54e14f2d..5e1d4c3c9 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -26,5 +26,30 @@ namespace MediaBrowser.Controller.Entities.Audio [IgnoreDataMember] public List<ItemByNameCounts> UserItemCountList { get; set; } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } } } 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; + } } } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 28ccf687c..0405fc484 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,9 +1,11 @@ -using MediaBrowser.Model.Configuration; +using System.Linq; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { - public class Book : BaseItem, IHasTags, IHasPreferredMetadataLanguage + public class Book : BaseItem, IHasTags, IHasPreferredMetadataLanguage, IHasLookupInfo<BookInfo>, IHasSeries { public override string MediaType { @@ -38,5 +40,21 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedBooks; } + + public BookInfo GetLookupInfo() + { + var info = GetItemLookupInfo<BookInfo>(); + + if (string.IsNullOrEmpty(SeriesName)) + { + info.SeriesName = Parents.Select(i => i.Name).FirstOrDefault(); + } + else + { + info.SeriesName = SeriesName; + } + + return info; + } } } diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 9c6b60969..416796b69 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using System; using System.Collections.Generic; using System.IO; @@ -60,12 +61,12 @@ namespace MediaBrowser.Controller.Entities public List<string> PhysicalLocationsList { get; set; } - protected override IEnumerable<FileSystemInfo> GetFileSystemChildren() + protected override IEnumerable<FileSystemInfo> GetFileSystemChildren(IDirectoryService directoryService) { - return CreateResolveArgs().FileSystemChildren; + return CreateResolveArgs(directoryService).FileSystemChildren; } - private ItemResolveArgs CreateResolveArgs() + private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService) { var path = ContainingFolderPath; @@ -84,7 +85,7 @@ namespace MediaBrowser.Controller.Entities // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; - var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, FileSystem, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); + var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, 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 @@ -116,11 +117,13 @@ namespace MediaBrowser.Controller.Entities /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> - /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param> + /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="directoryService">The directory service.</param> /// <returns>Task.</returns> - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { - CreateResolveArgs(); + CreateResolveArgs(directoryService); ResetDynamicChildren(); return NullTaskResult; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 63a1c2bab..cb14ed099 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,11 +1,9 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MoreLinq; using System; @@ -301,15 +299,27 @@ namespace MediaBrowser.Controller.Entities /// <value>The current validation cancellation token source.</value> private CancellationTokenSource CurrentValidationCancellationTokenSource { get; set; } + public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) + { + return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions()); + } + /// <summary> /// Validates that the children of the folder still exist /// </summary> /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="metadataRefreshOptions">The metadata refresh options.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> - /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param> /// <returns>Task.</returns> - public async Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) + public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) + { + metadataRefreshOptions.DirectoryService = metadataRefreshOptions.DirectoryService ?? new DirectoryService(Logger); + + return ValidateChildrenWithCancellationSupport(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); + } + + private async Task ValidateChildrenWithCancellationSupport(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { cancellationToken.ThrowIfCancellationRequested(); @@ -329,7 +339,7 @@ namespace MediaBrowser.Controller.Entities var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken); - await ValidateChildrenInternal(progress, linkedCancellationTokenSource.Token, recursive, forceRefreshMetadata).ConfigureAwait(false); + await ValidateChildrenInternal(progress, linkedCancellationTokenSource.Token, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false); } catch (OperationCanceledException ex) { @@ -354,21 +364,22 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes - /// ***Currently does not contain logic to maintain items that are unavailable in the file system*** + /// Validates the children internal. /// </summary> /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> - /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param> + /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="directoryService">The directory service.</param> /// <returns>Task.</returns> - protected async virtual Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) + protected async virtual Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { var locationType = LocationType; cancellationToken.ThrowIfCancellationRequested(); - var validChildren = new List<Tuple<BaseItem, bool>>(); + var validChildren = new List<BaseItem>(); if (locationType != LocationType.Remote && locationType != LocationType.Virtual) { @@ -376,7 +387,7 @@ namespace MediaBrowser.Controller.Entities try { - nonCachedChildren = GetNonCachedChildren(); + nonCachedChildren = GetNonCachedChildren(directoryService); } catch (IOException ex) { @@ -403,43 +414,30 @@ namespace MediaBrowser.Controller.Entities if (currentChildren.TryGetValue(child.Id, out currentChild)) { - //existing item - check if it has changed - if (currentChild.HasChanged(child)) - { - var currentChildLocationType = currentChild.LocationType; - if (currentChildLocationType != LocationType.Remote && - currentChildLocationType != LocationType.Virtual) - { - currentChild.DateModified = child.DateModified; - } - - currentChild.IsInMixedFolder = child.IsInMixedFolder; - validChildren.Add(new Tuple<BaseItem, bool>(currentChild, true)); - } - else + var currentChildLocationType = currentChild.LocationType; + if (currentChildLocationType != LocationType.Remote && + currentChildLocationType != LocationType.Virtual) { - validChildren.Add(new Tuple<BaseItem, bool>(currentChild, false)); + currentChild.DateModified = child.DateModified; } + currentChild.IsInMixedFolder = child.IsInMixedFolder; currentChild.IsOffline = false; } else { //brand new item - needs to be added newItems.Add(child); - - validChildren.Add(new Tuple<BaseItem, bool>(child, true)); } + + validChildren.Add(currentChild); } // If any items were added or removed.... if (newItems.Count > 0 || currentChildren.Count != validChildren.Count) { - var newChildren = validChildren.Select(c => c.Item1).ToList(); - // That's all the new and changed ones - now see if there are any that are missing - var itemsRemoved = currentChildren.Values.Except(newChildren).ToList(); - + var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var actualRemovals = new List<BaseItem>(); foreach (var item in itemsRemoved) @@ -448,14 +446,13 @@ namespace MediaBrowser.Controller.Entities item.LocationType == LocationType.Remote) { // Don't remove these because there's no way to accurately validate them. - validChildren.Add(new Tuple<BaseItem, bool>(item, false)); + validChildren.Add(item); } else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path)) { item.IsOffline = true; - - validChildren.Add(new Tuple<BaseItem, bool>(item, false)); + validChildren.Add(item); } else { @@ -481,88 +478,134 @@ namespace MediaBrowser.Controller.Entities await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); } } - else - { - validChildren.AddRange(ActualChildren.Select(i => new Tuple<BaseItem, bool>(i, false))); - } progress.Report(10); cancellationToken.ThrowIfCancellationRequested(); - await RefreshChildren(validChildren, progress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false); + if (recursive) + { + await ValidateSubFolders(ActualChildren.OfType<Folder>().ToList(), directoryService, progress, cancellationToken).ConfigureAwait(false); + } + + progress.Report(20); + + if (refreshChildMetadata) + { + var container = this as IMetadataContainer; + + var innerProgress = new ActionableProgress<double>(); + + innerProgress.RegisterAction(p => progress.Report((.80 * p) + 20)); + + if (container != null) + { + await container.RefreshAllMetadata(refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false); + } + else + { + await RefreshMetadataRecursive(refreshOptions, recursive, innerProgress, cancellationToken); + } + } progress.Report(100); } - /// <summary> - /// Refreshes the children. - /// </summary> - /// <param name="children">The children.</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="recursive">if set to <c>true</c> [recursive].</param> - /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param> - /// <returns>Task.</returns> - private async Task RefreshChildren(IList<Tuple<BaseItem, bool>> children, IProgress<double> progress, CancellationToken cancellationToken, bool? recursive, bool forceRefreshMetadata = false) + private async Task RefreshMetadataRecursive(MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - var list = children; + var children = ActualChildren.ToList(); - var percentages = new Dictionary<Guid, double>(list.Count); + var percentages = new Dictionary<Guid, double>(children.Count); var tasks = new List<Task>(); - foreach (var tuple in list) + foreach (var child in children) { - if (tasks.Count > 10) + if (tasks.Count >= 8) { await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); } - tasks.Add(RefreshChild(tuple, progress, percentages, list.Count, cancellationToken, recursive, forceRefreshMetadata)); - } + cancellationToken.ThrowIfCancellationRequested(); + var innerProgress = new ActionableProgress<double>(); - cancellationToken.ThrowIfCancellationRequested(); + // Avoid implicitly captured closure + var currentChild = child; + innerProgress.RegisterAction(p => + { + lock (percentages) + { + percentages[currentChild.Id] = p / 100; + + var percent = percentages.Values.Sum(); + percent /= children.Count; + percent *= 100; + progress.Report(percent); + } + }); + + if (child.IsFolder) + { + await RefreshChildMetadata(child, refreshOptions, recursive, innerProgress, cancellationToken) + .ConfigureAwait(false); + } + else + { + tasks.Add(RefreshChildMetadata(child, refreshOptions, false, innerProgress, cancellationToken)); + } + } await Task.WhenAll(tasks).ConfigureAwait(false); + progress.Report(100); } - private async Task RefreshChild(Tuple<BaseItem, bool> currentTuple, IProgress<double> progress, Dictionary<Guid, double> percentages, int childCount, CancellationToken cancellationToken, bool? recursive, bool forceRefreshMetadata = false) + private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + var container = child as IMetadataContainer; - var child = currentTuple.Item1; - try + if (container != null) { - //refresh it - await child.RefreshMetadata(new MetadataRefreshOptions - { - ForceSave = currentTuple.Item2, - ReplaceAllMetadata = forceRefreshMetadata - - }, cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); } - catch (IOException ex) + else { - Logger.ErrorException("Error refreshing {0}", ex, child.Path ?? child.Name); - } - - // Refresh children if a folder and the item changed or recursive is set to true - var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value)); + await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - if (refreshChildren) - { - // Don't refresh children if explicitly set to false - if (recursive.HasValue && recursive.Value == false) + if (recursive) { - refreshChildren = false; + var folder = child as Folder; + + if (folder != null) + { + await folder.RefreshMetadataRecursive(refreshOptions, true, progress, cancellationToken); + } } } + progress.Report(100); + } + + /// <summary> + /// Refreshes the children. + /// </summary> + /// <param name="children">The children.</param> + /// <param name="directoryService">The directory service.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) + { + var list = children; + var childCount = list.Count; + + var percentages = new Dictionary<Guid, double>(list.Count); - if (refreshChildren) + foreach (var item in list) { cancellationToken.ThrowIfCancellationRequested(); + var child = item; + var innerProgress = new ActionableProgress<double>(); innerProgress.RegisterAction(p => @@ -574,23 +617,12 @@ namespace MediaBrowser.Controller.Entities var percent = percentages.Values.Sum(); percent /= childCount; - progress.Report((90 * percent) + 10); + progress.Report((10 * percent) + 10); } }); - await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false); - } - else - { - lock (percentages) - { - percentages[child.Id] = 1; - - var percent = percentages.Values.Sum(); - percent /= childCount; - - progress.Report((90 * percent) + 10); - } + await child.ValidateChildrenWithCancellationSupport(innerProgress, cancellationToken, true, false, null, directoryService) + .ConfigureAwait(false); } } @@ -647,9 +679,9 @@ namespace MediaBrowser.Controller.Entities /// Get the children of this folder from the actual file system /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - protected virtual IEnumerable<BaseItem> GetNonCachedChildren() + protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { - return LibraryManager.ResolvePaths<BaseItem>(GetFileSystemChildren(), this); + return LibraryManager.ResolvePaths<BaseItem>(GetFileSystemChildren(directoryService), this); } /// <summary> @@ -895,17 +927,21 @@ namespace MediaBrowser.Controller.Entities return item; } - protected override Task BeforeRefreshMetadata(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { + var changesFound = false; + if (SupportsShortcutChildren && LocationType == LocationType.FileSystem) { if (RefreshLinkedChildren(fileSystemChildren)) { - options.ForceSave = true; + changesFound = true; } } - return base.BeforeRefreshMetadata(options, fileSystemChildren, cancellationToken); + var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + return baseHasChanges || changesFound; } /// <summary> @@ -967,11 +1003,11 @@ namespace MediaBrowser.Controller.Entities /// <returns>Task.</returns> public override async Task ChangedExternally() { - await base.ChangedExternally().ConfigureAwait(false); - var progress = new Progress<double>(); await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false); + + await base.ChangedExternally().ConfigureAwait(false); } /// <summary> @@ -1016,50 +1052,17 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentNullException(); } - try - { - var locationType = LocationType; - - if (locationType == LocationType.Remote && string.Equals(Path, path, StringComparison.OrdinalIgnoreCase)) - { - return this; - } - - if (locationType != LocationType.Virtual && PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)) - { - return this; - } - } - catch (IOException ex) + if (string.Equals(Path, path, StringComparison.OrdinalIgnoreCase)) { - Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); + return this; } - return RecursiveChildren.Where(i => i.LocationType != LocationType.Virtual).FirstOrDefault(i => + if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)) { - try - { - if (string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (i.LocationType != LocationType.Remote) - { - if (i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - } + return this; + } - return false; - } - catch (IOException ex) - { - Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); - return false; - } - }); + return RecursiveChildren.FirstOrDefault(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)); } public override bool IsPlayed(User user) diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs index 1b5176362..cc9d9a1a4 100644 --- a/MediaBrowser.Controller/Entities/Game.cs +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -1,11 +1,12 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { - public class Game : BaseItem, IHasSoundtracks, IHasTrailers, IHasThemeMedia, IHasTags, IHasScreenshots, IHasPreferredMetadataLanguage + public class Game : BaseItem, IHasSoundtracks, IHasTrailers, IHasThemeMedia, IHasTags, IHasScreenshots, IHasPreferredMetadataLanguage, IHasLookupInfo<GameInfo> { public List<Guid> SoundtrackIds { get; set; } @@ -29,18 +30,11 @@ namespace MediaBrowser.Controller.Entities ThemeSongIds = new List<Guid>(); ThemeVideoIds = new List<Guid>(); Tags = new List<string>(); - ScreenshotImagePaths = new List<string>(); } public List<Guid> LocalTrailerIds { get; set; } /// <summary> - /// Gets or sets the screenshot image paths. - /// </summary> - /// <value>The screenshot image paths.</value> - public List<string> ScreenshotImagePaths { get; set; } - - /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> @@ -115,5 +109,14 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedGames; } + + public GameInfo GetLookupInfo() + { + var id = GetItemLookupInfo<GameInfo>(); + + id.GameSystem = GameSystem; + + return id; + } } } diff --git a/MediaBrowser.Controller/Entities/GameGenre.cs b/MediaBrowser.Controller/Entities/GameGenre.cs index ffe62ba03..3a3c575cd 100644 --- a/MediaBrowser.Controller/Entities/GameGenre.cs +++ b/MediaBrowser.Controller/Entities/GameGenre.cs @@ -1,5 +1,4 @@ using MediaBrowser.Model.Dto; -using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -23,5 +22,30 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public List<ItemByNameCounts> UserItemCountList { get; set; } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } } } diff --git a/MediaBrowser.Controller/Entities/GameSystem.cs b/MediaBrowser.Controller/Entities/GameSystem.cs index 69cb5e974..f2fec4397 100644 --- a/MediaBrowser.Controller/Entities/GameSystem.cs +++ b/MediaBrowser.Controller/Entities/GameSystem.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using System; @@ -7,7 +8,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class GameSystem /// </summary> - public class GameSystem : Folder + public class GameSystem : Folder, IHasLookupInfo<GameSystemInfo> { /// <summary> /// Return the id that should be used to key display prefs for this item. @@ -47,5 +48,14 @@ namespace MediaBrowser.Controller.Entities // Don't block. Determine by game return false; } + + public GameSystemInfo GetLookupInfo() + { + var id = GetItemLookupInfo<GameSystemInfo>(); + + id.Path = Path; + + return id; + } } } diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 53bc64194..c15ca0aa2 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -25,5 +25,30 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public List<ItemByNameCounts> UserItemCountList { get; set; } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } } } diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs index dd6194bc7..d53eba11a 100644 --- a/MediaBrowser.Controller/Entities/IHasImages.cs +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -1,6 +1,8 @@ -using MediaBrowser.Model.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities @@ -32,6 +34,13 @@ namespace MediaBrowser.Controller.Entities LocationType LocationType { get; } /// <summary> + /// Gets the images. + /// </summary> + /// <param name="imageType">Type of the image.</param> + /// <returns>IEnumerable{ItemImageInfo}.</returns> + IEnumerable<ItemImageInfo> GetImages(ImageType imageType); + + /// <summary> /// Gets the image path. /// </summary> /// <param name="imageType">Type of the image.</param> @@ -40,19 +49,20 @@ namespace MediaBrowser.Controller.Entities string GetImagePath(ImageType imageType, int imageIndex); /// <summary> - /// Gets the image date modified. + /// Gets the image information. /// </summary> - /// <param name="imagePath">The image path.</param> - /// <returns>DateTime.</returns> - DateTime GetImageDateModified(string imagePath); + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>ItemImageInfo.</returns> + ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex); /// <summary> /// Sets the image. /// </summary> /// <param name="type">The type.</param> /// <param name="index">The index.</param> - /// <param name="path">The path.</param> - void SetImagePath(ImageType type, int index, string path); + /// <param name="file">The file.</param> + void SetImagePath(ImageType type, int index, FileInfo file); /// <summary> /// Determines whether the specified type has image. @@ -63,6 +73,13 @@ namespace MediaBrowser.Controller.Entities bool HasImage(ImageType type, int imageIndex); /// <summary> + /// Allowses the multiple images. + /// </summary> + /// <param name="type">The type.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool AllowsMultipleImages(ImageType type); + + /// <summary> /// Swaps the images. /// </summary> /// <param name="type">The type.</param> @@ -92,13 +109,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Validates the images and returns true or false indicating if any were removed. /// </summary> - bool ValidateImages(); - - /// <summary> - /// Gets or sets the backdrop image paths. - /// </summary> - /// <value>The backdrop image paths.</value> - List<string> BackdropImagePaths { get; set; } + bool ValidateImages(IDirectoryService directoryService); /// <summary> /// Gets a value indicating whether this instance is owned item. @@ -111,6 +122,26 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value>The containing folder path.</value> string ContainingFolderPath { get; } + + /// <summary> + /// Adds the images. + /// </summary> + /// <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> + bool AddImages(ImageType imageType, IEnumerable<FileInfo> images); + + /// <summary> + /// Determines whether [is save local metadata enabled]. + /// </summary> + /// <returns><c>true</c> if [is save local metadata enabled]; otherwise, <c>false</c>.</returns> + bool IsSaveLocalMetadataEnabled(); + + /// <summary> + /// Gets a value indicating whether [supports local metadata]. + /// </summary> + /// <value><c>true</c> if [supports local metadata]; otherwise, <c>false</c>.</value> + bool SupportsLocalMetadata { get; } } public static class HasImagesExtensions @@ -136,10 +167,21 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="item">The item.</param> /// <param name="imageType">Type of the image.</param> - /// <param name="path">The path.</param> - public static void SetImagePath(this IHasImages item, ImageType imageType, string path) + /// <param name="file">The file.</param> + public static void SetImagePath(this IHasImages item, ImageType imageType, FileInfo file) + { + item.SetImagePath(imageType, 0, file); + } + + /// <summary> + /// Sets the image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <param name="file">The file.</param> + public static void SetImagePath(this IHasImages item, ImageType imageType, string file) { - item.SetImagePath(imageType, 0, path); + item.SetImagePath(imageType, new FileInfo(file)); } } } diff --git a/MediaBrowser.Controller/Entities/IHasMetadata.cs b/MediaBrowser.Controller/Entities/IHasMetadata.cs new file mode 100644 index 000000000..0285b6749 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasMetadata.cs @@ -0,0 +1,59 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasMetadata + /// </summary> + public interface IHasMetadata : IHasImages + { + /// <summary> + /// Gets the preferred metadata country code. + /// </summary> + /// <returns>System.String.</returns> + string GetPreferredMetadataCountryCode(); + + /// <summary> + /// Gets the date modified. + /// </summary> + /// <value>The date modified.</value> + DateTime DateModified { get; } + + /// <summary> + /// Gets the locked fields. + /// </summary> + /// <value>The locked fields.</value> + List<MetadataFields> LockedFields { get; } + + /// <summary> + /// Gets or sets the date last saved. + /// </summary> + /// <value>The date last saved.</value> + DateTime DateLastSaved { 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> + bool IsInMixedFolder { get; } + + /// <summary> + /// Updates to repository. + /// </summary> + /// <param name="updateReason">The update reason.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken); + + /// <summary> + /// This is called before any metadata refresh and returns ItemUpdateType indictating if changes were made, and what. + /// </summary> + /// <returns>ItemUpdateType.</returns> + ItemUpdateType BeforeMetadataRefresh(); + } +} diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs index 341d6403f..2fd402bc2 100644 --- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; - + namespace MediaBrowser.Controller.Entities { /// <summary> @@ -7,10 +6,5 @@ namespace MediaBrowser.Controller.Entities /// </summary> public interface IHasScreenshots { - /// <summary> - /// Gets or sets the screenshot image paths. - /// </summary> - /// <value>The screenshot image paths.</value> - List<string> ScreenshotImagePaths { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs new file mode 100644 index 000000000..64c33a376 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -0,0 +1,12 @@ + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasSeries + { + /// <summary> + /// Gets the name of the series. + /// </summary> + /// <value>The name of the series.</value> + string SeriesName { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/IMetadataContainer.cs b/MediaBrowser.Controller/Entities/IMetadataContainer.cs new file mode 100644 index 000000000..33aa08425 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IMetadataContainer.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Controller.Providers; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + public interface IMetadataContainer + { + /// <summary> + /// Refreshes all metadata. + /// </summary> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs new file mode 100644 index 000000000..80aec6482 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -0,0 +1,14 @@ +using MediaBrowser.Model.Entities; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public class ItemImageInfo + { + public string Path { get; set; } + + public ImageType Type { get; set; } + + public DateTime DateModified { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 19d0d6682..a00bda772 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,15 +1,20 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities.Movies { /// <summary> /// Class BoxSet /// </summary> - public class BoxSet : Folder, IHasTrailers, IHasTags, IHasKeywords, IHasPreferredMetadataLanguage, IHasDisplayOrder + public class BoxSet : Folder, IHasTrailers, IHasTags, IHasKeywords, IHasPreferredMetadataLanguage, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo>, IMetadataContainer { public BoxSet() { @@ -74,5 +79,67 @@ namespace MediaBrowser.Controller.Entities.Movies // Default sorting return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending); } + + public BoxSetInfo GetLookupInfo() + { + return GetItemLookupInfo<BoxSetInfo>(); + } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + // Refresh bottom up, children first, then the boxset + // By then hopefully the movies within will have Tmdb collection values + var items = RecursiveChildren.ToList(); + + var totalItems = items.Count; + var percentages = new Dictionary<Guid, double>(totalItems); + + var tasks = new List<Task>(); + + // Refresh songs + foreach (var item in items) + { + if (tasks.Count >= 2) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); + } + + cancellationToken.ThrowIfCancellationRequested(); + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentChild = item; + innerProgress.RegisterAction(p => + { + lock (percentages) + { + percentages[currentChild.Id] = p / 100; + + var percent = percentages.Values.Sum(); + percent /= totalItems; + percent *= 100; + progress.Report(percent); + } + }); + + tasks.Add(RefreshItem(item, refreshOptions, innerProgress, cancellationToken)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + tasks.Clear(); + + // Refresh current item + await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + progress.Report(100); + } + + private async Task RefreshItem(BaseItem item, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + progress.Report(100); + } } } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 41a1969d6..8eba21df0 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Entities.Movies /// <summary> /// Class Movie /// </summary> - public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasKeywords, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasTags, IHasPreferredMetadataLanguage, IHasAwards, IHasMetascore + public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasKeywords, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasPreferredMetadataLanguage, IHasAwards, IHasMetascore, IHasLookupInfo<MovieInfo> { public List<Guid> SpecialFeatureIds { get; set; } @@ -39,7 +39,6 @@ namespace MediaBrowser.Controller.Entities.Movies ThemeSongIds = new List<Guid>(); ThemeVideoIds = new List<Guid>(); Taglines = new List<string>(); - Tags = new List<string>(); Keywords = new List<string>(); } @@ -53,12 +52,6 @@ namespace MediaBrowser.Controller.Entities.Movies public List<MediaUrl> RemoteTrailers { get; set; } /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public List<string> Tags { get; set; } - - /// <summary> /// Gets or sets the taglines. /// </summary> /// <value>The taglines.</value> @@ -103,9 +96,9 @@ namespace MediaBrowser.Controller.Entities.Movies return this.GetProviderId(MetadataProviders.Tmdb) ?? this.GetProviderId(MetadataProviders.Imdb) ?? base.GetUserDataKey(); } - protected override async Task BeforeRefreshMetadata(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { - await base.BeforeRefreshMetadata(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); // Must have a parent to have special features // In other words, it must be part of the Parent/Child tree @@ -115,12 +108,14 @@ namespace MediaBrowser.Controller.Entities.Movies if (specialFeaturesChanged) { - options.ForceSave = true; + hasChanges = true; } } + + return hasChanges; } - private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { var newItems = LoadSpecialFeatures(fileSystemChildren).ToList(); var newItemIds = newItems.Select(i => i.Id).ToList(); @@ -157,12 +152,19 @@ namespace MediaBrowser.Controller.Entities.Movies } return video; - }); + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } protected override bool GetBlockUnratedValue(UserConfiguration config) { return config.BlockUnratedMovies; } + + public MovieInfo GetLookupInfo() + { + return GetItemLookupInfo<MovieInfo>(); + } } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index d9eff8fbe..56cd71d49 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,11 +1,12 @@ using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; namespace MediaBrowser.Controller.Entities { - public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasBudget + public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasBudget, IHasLookupInfo<MusicVideoInfo> { /// <summary> /// Gets or sets the artist. @@ -54,5 +55,10 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedMusic; } + + public MusicVideoInfo GetLookupInfo() + { + return GetItemLookupInfo<MusicVideoInfo>(); + } } } diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 832586ab9..c1dc81136 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; using System.Collections.Generic; using System.Runtime.Serialization; @@ -7,7 +8,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// This is the full Person object that can be retrieved with all of it's data. /// </summary> - public class Person : BaseItem, IItemByName + public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo> { public Person() { @@ -31,6 +32,36 @@ namespace MediaBrowser.Controller.Entities { return "Person-" + Name; } + + public PersonLookupInfo GetLookupInfo() + { + return GetItemLookupInfo<PersonLookupInfo>(); + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } } /// <summary> diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 7bc17549f..5c3946f9b 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -26,5 +26,30 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public List<ItemByNameCounts> UserItemCountList { get; set; } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 73726a4e2..daff3dd6c 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -1,4 +1,7 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +12,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Episode /// </summary> - public class Episode : Video + public class Episode : Video, IHasLookupInfo<EpisodeInfo>, IHasSeries { /// <summary> /// Gets the season in which it aired. @@ -41,7 +44,7 @@ namespace MediaBrowser.Controller.Entities.TV /// </summary> /// <value>The index number.</value> public int? IndexNumberEnd { get; set; } - + /// <summary> /// We want to group into series not show individually in an index /// </summary> @@ -98,9 +101,11 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> public override string GetUserDataKey() { - if (Series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) + var series = Series; + + if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) { - return Series.GetUserDataKey() + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"); + return series.GetUserDataKey() + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"); } return base.GetUserDataKey(); @@ -112,16 +117,11 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public override string OfficialRatingForComparison { - get { return Series != null ? Series.OfficialRatingForComparison : base.OfficialRatingForComparison; } - } - - /// <summary> - /// Our rating comes from our series - /// </summary> - [IgnoreDataMember] - public override string CustomRatingForComparison - { - get { return Series != null ? Series.CustomRatingForComparison : base.CustomRatingForComparison; } + get + { + var series = Series; + return series != null ? series.OfficialRatingForComparison : base.OfficialRatingForComparison; + } } /// <summary> @@ -140,6 +140,16 @@ namespace MediaBrowser.Controller.Entities.TV get { return FindParent<Season>(); } } + [IgnoreDataMember] + public string SeriesName + { + get + { + var series = Series; + return series == null ? null : series.Name; + } + } + /// <summary> /// Creates the name of the sort. /// </summary> @@ -175,7 +185,7 @@ namespace MediaBrowser.Controller.Entities.TV { get { - return LocationType == Model.Entities.LocationType.Virtual && PremiereDate.HasValue && PremiereDate.Value < DateTime.UtcNow; + return LocationType == LocationType.Virtual && PremiereDate.HasValue && PremiereDate.Value < DateTime.UtcNow; } } @@ -188,7 +198,7 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public bool IsVirtualUnaired { - get { return LocationType == Model.Entities.LocationType.Virtual && IsUnaired; } + get { return LocationType == LocationType.Virtual && IsUnaired; } } [IgnoreDataMember] @@ -236,5 +246,70 @@ namespace MediaBrowser.Controller.Entities.TV { return config.BlockUnratedSeries; } + + public EpisodeInfo GetLookupInfo() + { + var id = GetItemLookupInfo<EpisodeInfo>(); + + var series = Series; + + if (series != null) + { + id.SeriesProviderIds = series.ProviderIds; + } + + id.IndexNumberEnd = IndexNumberEnd; + + return id; + } + + public override ItemUpdateType BeforeMetadataRefresh() + { + var updateType = base.BeforeMetadataRefresh(); + + var locationType = LocationType; + if (locationType == LocationType.FileSystem || locationType == LocationType.Offline) + { + if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) + { + IndexNumber = IndexNumber ?? TVUtils.GetEpisodeNumberFromFile(Path, Parent is Season); + + // If a change was made record it + if (IndexNumber.HasValue) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + } + + if (!IndexNumberEnd.HasValue && !string.IsNullOrEmpty(Path)) + { + IndexNumberEnd = IndexNumberEnd ?? TVUtils.GetEndingEpisodeNumberFromFile(Path); + + // If a change was made record it + if (IndexNumberEnd.HasValue) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + } + } + + if (!ParentIndexNumber.HasValue) + { + var season = Season; + + if (season != null) + { + ParentIndexNumber = season.IndexNumber; + } + + // If a change was made record it + if (ParentIndexNumber.HasValue) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + } + + return updateType; + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 744416560..830ccb8a2 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,11 +1,10 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Runtime.Serialization; @@ -14,7 +13,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Season /// </summary> - public class Season : Folder + public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> { /// <summary> @@ -119,16 +118,11 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public override string OfficialRatingForComparison { - get { return Series != null ? Series.OfficialRatingForComparison : base.OfficialRatingForComparison; } - } - - /// <summary> - /// Our rating comes from our series - /// </summary> - [IgnoreDataMember] - public override string CustomRatingForComparison - { - get { return Series != null ? Series.CustomRatingForComparison : base.CustomRatingForComparison; } + get + { + var series = Series; + return series != null ? series.OfficialRatingForComparison : base.OfficialRatingForComparison; + } } /// <summary> @@ -223,7 +217,7 @@ namespace MediaBrowser.Controller.Entities.TV { episodes = episodes.Where(i => !i.IsVirtualUnaired); } - + return LibraryManager .Sort(episodes, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) .Cast<Episode>(); @@ -239,5 +233,51 @@ namespace MediaBrowser.Controller.Entities.TV // Don't block. Let either the entire series rating or episode rating determine it return false; } + + [IgnoreDataMember] + public string SeriesName + { + get + { + var series = Series; + return series == null ? null : series.Name; + } + } + + /// <summary> + /// Gets the lookup information. + /// </summary> + /// <returns>SeasonInfo.</returns> + public SeasonInfo GetLookupInfo() + { + return GetItemLookupInfo<SeasonInfo>(); + } + + /// <summary> + /// This is called before any metadata refresh and returns ItemUpdateType indictating if changes were made, and what. + /// </summary> + /// <returns>ItemUpdateType.</returns> + public override ItemUpdateType BeforeMetadataRefresh() + { + var updateType = base.BeforeMetadataRefresh(); + + var locationType = LocationType; + + if (locationType == LocationType.FileSystem || locationType == LocationType.Offline) + { + if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) + { + IndexNumber = IndexNumber ?? TVUtils.GetSeasonNumberFromPath(Path); + + // If a change was made record it + if (IndexNumber.HasValue) + { + updateType = updateType | ItemUpdateType.MetadataImport; + } + } + } + + return updateType; + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index efb3c393b..0e07654d6 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Series /// </summary> - public class Series : Folder, IHasSoundtracks, IHasTrailers, IHasTags, IHasPreferredMetadataLanguage, IHasDisplayOrder + public class Series : Folder, IHasSoundtracks, IHasTrailers, IHasTags, IHasPreferredMetadataLanguage, IHasDisplayOrder, IHasLookupInfo<SeriesInfo> { public List<Guid> SpecialFeatureIds { get; set; } public List<Guid> SoundtrackIds { get; set; } @@ -222,5 +223,10 @@ namespace MediaBrowser.Controller.Entities.TV } public string PreferredMetadataLanguage { get; set; } + + public SeriesInfo GetLookupInfo() + { + return GetItemLookupInfo<SeriesInfo>(); + } } } diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index d6d193442..d655c275d 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; @@ -9,7 +10,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Trailer /// </summary> - public class Trailer : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers, IHasKeywords, IHasTaglines, IHasTags, IHasPreferredMetadataLanguage, IHasMetascore + public class Trailer : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers, IHasKeywords, IHasTaglines, IHasPreferredMetadataLanguage, IHasMetascore, IHasLookupInfo<TrailerInfo> { public List<Guid> SoundtrackIds { get; set; } @@ -27,7 +28,6 @@ namespace MediaBrowser.Controller.Entities Taglines = new List<string>(); SoundtrackIds = new List<Guid>(); LocalTrailerIds = new List<Guid>(); - Tags = new List<string>(); Keywords = new List<string>(); } @@ -40,12 +40,6 @@ namespace MediaBrowser.Controller.Entities public List<string> Keywords { get; set; } /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public List<string> Tags { get; set; } - - /// <summary> /// Gets or sets the taglines. /// </summary> /// <value>The taglines.</value> @@ -105,5 +99,10 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedTrailers; } + + public TrailerInfo GetLookupInfo() + { + return GetItemLookupInfo<TrailerInfo>(); + } } } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 5feb000af..66ef8c7dc 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -73,6 +73,31 @@ namespace MediaBrowser.Controller.Entities } /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } + + /// <summary> /// The _root folder /// </summary> private UserRootFolder _rootFolder; @@ -215,12 +240,18 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadata(new MetadataRefreshOptions { - ForceSave = true, - ReplaceAllMetadata = true + ReplaceAllMetadata = true, + ImageRefreshMode = ImageRefreshMode.FullRefresh, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh }, CancellationToken.None); } + public override Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + return UserManager.UpdateUser(this); + } + /// <summary> /// Gets the path to the user's configuration directory /// </summary> diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 8fe5f43f1..dc3d4c384 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using System.Collections.Generic; using System.Linq; namespace MediaBrowser.Controller.Entities @@ -13,9 +15,22 @@ namespace MediaBrowser.Controller.Entities /// Get the children of this folder from the actual file system /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - protected override IEnumerable<BaseItem> GetNonCachedChildren() + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { - return base.GetNonCachedChildren().Concat(LibraryManager.RootFolder.VirtualChildren); + return base.GetNonCachedChildren(directoryService).Concat(LibraryManager.RootFolder.VirtualChildren); + } + + public override ItemUpdateType BeforeMetadataRefresh() + { + var updateType = base.BeforeMetadataRefresh(); + + if (string.Equals("default", Name, System.StringComparison.OrdinalIgnoreCase)) + { + Name = "Default Media Library"; + updateType = updateType | ItemUpdateType.MetadataEdit; + } + + return updateType; } } } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index de78068b3..e778b38bd 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Video /// </summary> - public class Video : BaseItem, IHasMediaStreams, IHasAspectRatio + public class Video : BaseItem, IHasMediaStreams, IHasAspectRatio, IHasTags { public bool IsMultiPart { get; set; } @@ -26,15 +26,29 @@ namespace MediaBrowser.Controller.Entities { PlayableStreamFileNames = new List<string>(); AdditionalPartIds = new List<Guid>(); + Tags = new List<string>(); + SubtitleFiles = new List<string>(); } /// <summary> + /// Gets or sets the subtitle paths. + /// </summary> + /// <value>The subtitle paths.</value> + public List<string> SubtitleFiles { get; set; } + + /// <summary> /// Gets or sets a value indicating whether this instance has subtitles. /// </summary> /// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value> public bool HasSubtitles { get; set; } /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + public List<string> Tags { get; set; } + + /// <summary> /// Gets or sets the video bit rate. /// </summary> /// <value>The video bit rate.</value> @@ -149,9 +163,9 @@ namespace MediaBrowser.Controller.Entities } } - protected override async Task BeforeRefreshMetadata(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { - await base.BeforeRefreshMetadata(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); // Must have a parent to have additional parts // In other words, it must be part of the Parent/Child tree @@ -162,9 +176,11 @@ namespace MediaBrowser.Controller.Entities if (additionalPartsChanged) { - options.ForceSave = true; + hasChanges = true; } } + + return hasChanges; } /// <summary> @@ -174,7 +190,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="fileSystemChildren">The file system children.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{System.Boolean}.</returns> - private async Task<bool> RefreshAdditionalParts(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + private async Task<bool> RefreshAdditionalParts(MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { var newItems = LoadAdditionalParts(fileSystemChildren).ToList(); @@ -238,7 +254,8 @@ namespace MediaBrowser.Controller.Entities return video; - }).ToList(); + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } public override IEnumerable<string> GetDeletePaths() diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index cd50a1c60..c6ca028ae 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -26,5 +26,30 @@ namespace MediaBrowser.Controller.Entities { return "Year-" + Name; } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is owned item. + /// </summary> + /// <value><c>true</c> if this instance is owned item; otherwise, <c>false</c>.</value> + public override bool IsOwnedItem + { + get + { + return false; + } + } } } |
