diff options
| author | Andrew Rabert <ar@nullsum.net> | 2018-12-27 18:27:57 -0500 |
|---|---|---|
| committer | Andrew Rabert <ar@nullsum.net> | 2018-12-27 18:27:57 -0500 |
| commit | a86b71899ec52c44ddc6c3018e8cc5e9d7ff4d62 (patch) | |
| tree | a74f6ea4a8abfa1664a605d31d48bc38245ccf58 /MediaBrowser.Controller/Entities | |
| parent | 9bac3ac616b01f67db98381feb09d34ebe821f9a (diff) | |
Add GPL modules
Diffstat (limited to 'MediaBrowser.Controller/Entities')
60 files changed, 13032 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs new file mode 100644 index 000000000..4f4b3483c --- /dev/null +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -0,0 +1,219 @@ +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Specialized folder that can have items added to it's children by external entities. + /// Used for our RootFolder so plug-ins can add items. + /// </summary> + public class AggregateFolder : Folder + { + public AggregateFolder() + { + PhysicalLocationsList = new string[] { }; + } + + [IgnoreDataMember] + public override bool IsPhysicalRoot + { + get { return true; } + } + + public override bool CanDelete() + { + return false; + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + /// <summary> + /// The _virtual children + /// </summary> + private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>(); + + /// <summary> + /// Gets the virtual children. + /// </summary> + /// <value>The virtual children.</value> + public ConcurrentBag<BaseItem> VirtualChildren + { + get { return _virtualChildren; } + } + + [IgnoreDataMember] + public override string[] PhysicalLocations + { + get + { + return PhysicalLocationsList; + } + } + + public string[] PhysicalLocationsList { get; set; } + + protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) + { + return CreateResolveArgs(directoryService, true).FileSystemChildren; + } + + private Guid[] _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override List<BaseItem> LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null || _childrenIds.Length == 0) + { + var list = base.LoadChildren(); + _childrenIds = list.Select(i => i.Id).ToArray(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + + private bool _requiresRefresh; + public override bool RequiresRefresh() + { + var changed = base.RequiresRefresh() || _requiresRefresh; + + if (!changed) + { + var locations = PhysicalLocations; + + var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + + if (!locations.SequenceEqual(newLocations)) + { + changed = true; + } + } + + return changed; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + ClearCache(); + + var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + _requiresRefresh = false; + return changed; + } + + private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) + { + ClearCache(); + + var path = ContainingFolderPath; + + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + { + FileInfo = FileSystem.GetDirectoryInfo(path), + Path = path + }; + + // Gather child folder and files + if (args.IsDirectory) + { + // When resolving the root, we need it's grandchildren (children of user views) + var flattenFolderDepth = 2; + + var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, CollectionFolder.ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true); + + // Need to remove subpaths that may have been resolved from shortcuts + // Example: if \\server\movies exists, then strip out \\server\movies\action + files = LibraryManager.NormalizeRootPathList(files).ToArray(); + + args.FileSystemChildren = files; + } + + _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations); + if (setPhysicalLocations) + { + PhysicalLocationsList = args.PhysicalLocations; + } + + return args; + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); + } + + protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + ClearCache(); + + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + .ConfigureAwait(false); + + ClearCache(); + } + + /// <summary> + /// Adds the virtual child. + /// </summary> + /// <param name="child">The child.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddVirtualChild(BaseItem child) + { + if (child == null) + { + throw new ArgumentNullException(); + } + + _virtualChildren.Add(child); + } + + /// <summary> + /// Finds the virtual child. + /// </summary> + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + public BaseItem FindVirtualChild(Guid id) + { + if (id.Equals(Guid.Empty)) + { + throw new ArgumentNullException("id"); + } + + foreach (var child in _virtualChildren) + { + if (child.Id == id) + { + return child; + } + } + return null; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs new file mode 100644 index 000000000..d07e31d8a --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -0,0 +1,216 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class Audio + /// </summary> + public class Audio : BaseItem, + IHasAlbumArtist, + IHasArtist, + IHasMusicGenres, + IHasLookupInfo<SongInfo>, + IHasMediaSources + { + /// <summary> + /// Gets or sets the artist. + /// </summary> + /// <value>The artist.</value> + [IgnoreDataMember] + public string[] Artists { get; set; } + + [IgnoreDataMember] + public string[] AlbumArtists { get; set; } + + public Audio() + { + Artists = new string[] {}; + AlbumArtists = new string[] {}; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return false; } + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + protected override bool SupportsOwnedItems + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override Folder LatestItemsIndexContainer + { + get + { + return AlbumEntity; + } + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + [IgnoreDataMember] + public string[] AllArtists + { + get + { + var list = new string[AlbumArtists.Length + Artists.Length]; + + var index = 0; + foreach (var artist in AlbumArtists) + { + list[index] = artist; + index++; + } + foreach (var artist in Artists) + { + list[index] = artist; + index++; + } + + return list; + + } + } + + [IgnoreDataMember] + public MusicAlbum AlbumEntity + { + get { return FindParent<MusicAlbum>(); } + } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Audio; + } + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected override string CreateSortName() + { + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "") + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty; + + + if (ParentIndexNumber.HasValue) + { + songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey; + } + songKey += Name; + + if (!string.IsNullOrEmpty(Album)) + { + songKey = Album + "-" + songKey; + } + + var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0]; + if (!string.IsNullOrEmpty(albumArtist)) + { + songKey = albumArtist + "-" + songKey; + } + + list.Insert(0, songKey); + + return list; + } + + public override UnratedItem GetBlockUnratedType() + { + if (SourceType == SourceType.Library) + { + return UnratedItem.Music; + } + return base.GetBlockUnratedType(); + } + + public List<MediaStream> GetMediaStreams(MediaStreamType type) + { + return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + { + ItemId = Id, + Type = type + }); + } + + public SongInfo GetLookupInfo() + { + var info = GetItemLookupInfo<SongInfo>(); + + info.AlbumArtists = AlbumArtists; + info.Album = Album; + info.Artists = Artists; + + return info; + } + + protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources() + { + var list = new List<Tuple<BaseItem, MediaSourceType>>(); + list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default)); + return list; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs new file mode 100644 index 000000000..b2dedada4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -0,0 +1,15 @@ + +namespace MediaBrowser.Controller.Entities.Audio +{ + public interface IHasAlbumArtist + { + string[] AlbumArtists { get; set; } + } + + public interface IHasArtist + { + string[] AllArtists { get; } + + string[] Artists { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs new file mode 100644 index 000000000..2200d4b75 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities.Audio +{ + public interface IHasMusicGenres + { + string[] Genres { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs new file mode 100644 index 000000000..48b5c64b2 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -0,0 +1,272 @@ +using System; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Users; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class MusicAlbum + /// </summary> + public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer + { + public string[] AlbumArtists { get; set; } + public string[] Artists { get; set; } + + public MusicAlbum() + { + Artists = new string[] {}; + AlbumArtists = new string[] {}; + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + public MusicArtist MusicArtist + { + get { return GetMusicArtist(new DtoOptions(true)); } + } + + public MusicArtist GetMusicArtist(DtoOptions options) + { + var parents = GetParents(); + foreach (var parent in parents) + { + var artist = parent as MusicArtist; + if (artist != null) + { + return artist; + } + } + + var name = AlbumArtist; + if (!string.IsNullOrEmpty(name)) + { + return LibraryManager.GetArtist(name, options); + } + return null; + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsCumulativeRunTimeTicks + { + get + { + return true; + } + } + + [IgnoreDataMember] + public string[] AllArtists + { + get + { + var list = new string[AlbumArtists.Length + Artists.Length]; + + var index = 0; + foreach (var artist in AlbumArtists) + { + list[index] = artist; + index++; + } + foreach (var artist in Artists) + { + list[index] = artist; + index++; + } + + return list; + } + } + + [IgnoreDataMember] + public string AlbumArtist + { + get { return AlbumArtists.Length == 0 ? null : AlbumArtists[0]; } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return false; } + } + + /// <summary> + /// Gets the tracks. + /// </summary> + /// <value>The tracks.</value> + [IgnoreDataMember] + public IEnumerable<BaseItem> Tracks + { + get + { + return GetRecursiveChildren(i => i is Audio); + } + } + + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + return Tracks; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var albumArtist = AlbumArtist; + if (!string.IsNullOrEmpty(albumArtist)) + { + list.Insert(0, albumArtist + "-" + Name); + } + + var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum); + + if (!string.IsNullOrEmpty(id)) + { + list.Insert(0, "MusicAlbum-Musicbrainz-" + id); + } + + id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + + if (!string.IsNullOrEmpty(id)) + { + list.Insert(0, "MusicAlbum-MusicBrainzReleaseGroup-" + id); + } + + return list; + } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Music); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + + public AlbumInfo GetLookupInfo() + { + var id = GetItemLookupInfo<AlbumInfo>(); + + id.AlbumArtists = AlbumArtists; + + var artist = GetMusicArtist(new DtoOptions(false)); + + if (artist != null) + { + id.ArtistProviderIds = artist.ProviderIds; + } + + id.SongInfos = GetRecursiveChildren(i => i is Audio) + .Cast<Audio>() + .Select(i => i.GetLookupInfo()) + .ToList(); + + var album = id.SongInfos + .Select(i => i.Album) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + + if (!string.IsNullOrEmpty(album)) + { + id.Name = album; + } + + return id; + } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + var items = GetRecursiveChildren(); + + var totalItems = items.Count; + var numComplete = 0; + + var childUpdateType = ItemUpdateType.None; + + // Refresh songs + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var updateType = await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + childUpdateType = childUpdateType | updateType; + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 95); + } + + var parentRefreshOptions = refreshOptions; + if (childUpdateType > ItemUpdateType.None) + { + parentRefreshOptions = new MetadataRefreshOptions(refreshOptions); + parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh; + } + + // Refresh current item + await RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false); + + if (!refreshOptions.IsAutomated) + { + await RefreshArtists(refreshOptions, cancellationToken).ConfigureAwait(false); + } + } + + private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + { + var all = AllArtists; + foreach (var i in all) + { + // This should not be necessary but we're seeing some cases of it + if (string.IsNullOrEmpty(i)) + { + continue; + } + + var artist = LibraryManager.GetArtist(i); + + if (!artist.IsAccessedByName) + { + continue; + } + + await artist.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs new file mode 100644 index 000000000..82dece84b --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -0,0 +1,275 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class MusicArtist + /// </summary> + public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> + { + [IgnoreDataMember] + public bool IsAccessedByName + { + get { return ParentId.Equals(Guid.Empty); } + } + + [IgnoreDataMember] + public override bool IsFolder + { + get + { + return !IsAccessedByName; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsCumulativeRunTimeTicks + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool CanDelete() + { + return !IsAccessedByName; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name }; + query.ArtistIds = new[] { Id }; + } + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override IEnumerable<BaseItem> Children + { + get + { + if (IsAccessedByName) + { + return new List<BaseItem>(); + } + + return base.Children; + } + } + + public override int GetChildCount(User user) + { + if (IsAccessedByName) + { + return 0; + } + return base.GetChildCount(user); + } + + public override bool IsSaveLocalMetadataEnabled() + { + if (IsAccessedByName) + { + return true; + } + + return base.IsSaveLocalMetadataEnabled(); + } + + private readonly Task _cachedTask = Task.FromResult(true); + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + if (IsAccessedByName) + { + // Should never get in here anyway + return _cachedTask; + } + + return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.InsertRange(0, GetUserDataKeys(this)); + return list; + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets the user data key. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + private static List<string> GetUserDataKeys(MusicArtist item) + { + var list = new List<string>(); + var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); + + if (!string.IsNullOrEmpty(id)) + { + list.Add("Artist-Musicbrainz-" + id); + } + + list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); + } + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Music); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + + public ArtistInfo GetLookupInfo() + { + var info = GetItemLookupInfo<ArtistInfo>(); + + info.SongInfos = GetRecursiveChildren(i => i is Audio) + .Cast<Audio>() + .Select(i => i.GetLookupInfo()) + .ToList(); + + return info; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs new file mode 100644 index 000000000..d60ce83ad --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class MusicGenre + /// </summary> + public class MusicGenre : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.GenreIds = new[] { Id }; + query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + + return LibraryManager.GetItemList(query); + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs new file mode 100644 index 000000000..679facf64 --- /dev/null +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -0,0 +1,69 @@ +using System; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> + { + [IgnoreDataMember] + public override bool SupportsPositionTicksResume + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + [IgnoreDataMember] + public string SeriesName { get; set; } + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public string FindSeriesSortName() + { + return SeriesName; + } + public string FindSeriesName() + { + return SeriesName; + } + public string FindSeriesPresentationUniqueKey() + { + return SeriesPresentationUniqueKey; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 0; + } + + public Guid FindSeriesId() + { + return SeriesId; + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Book; + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs new file mode 100644 index 000000000..053ee1b96 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -0,0 +1,2960 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class BaseItem + /// </summary> + public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo> + { + protected static MetadataFields[] EmptyMetadataFieldsArray = new MetadataFields[] { }; + protected static MediaUrl[] EmptyMediaUrlArray = new MediaUrl[] { }; + protected static ItemImageInfo[] EmptyItemImageInfoArray = new ItemImageInfo[] { }; + public static readonly LinkedChild[] EmptyLinkedChildArray = new LinkedChild[] { }; + + protected BaseItem() + { + ThemeSongIds = new Guid[] {}; + ThemeVideoIds = new Guid[] {}; + Tags = new string[] {}; + Genres = new string[] {}; + Studios = new string[] {}; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + LockedFields = EmptyMetadataFieldsArray; + ImageInfos = EmptyItemImageInfoArray; + ProductionLocations = new string[] {}; + RemoteTrailers = new MediaUrl[] { }; + ExtraIds = new Guid[] {}; + } + + public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; + public static char SlugChar = '-'; + + /// <summary> + /// The supported image extensions + /// </summary> + public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList(); + + /// <summary> + /// The trailer folder name + /// </summary> + public static string TrailerFolderName = "trailers"; + public static string ThemeSongsFolderName = "theme-music"; + public static string ThemeSongFilename = "theme"; + public static string ThemeVideosFolderName = "backdrops"; + + [IgnoreDataMember] + public Guid[] ThemeSongIds { get; set; } + [IgnoreDataMember] + public Guid[] ThemeVideoIds { get; set; } + + [IgnoreDataMember] + public string PreferredMetadataCountryCode { get; set; } + [IgnoreDataMember] + public string PreferredMetadataLanguage { get; set; } + + public long? Size { get; set; } + public string Container { get; set; } + + [IgnoreDataMember] + public string Tagline { get; set; } + + [IgnoreDataMember] + public virtual ItemImageInfo[] ImageInfos { get; set; } + + [IgnoreDataMember] + public bool IsVirtualItem { get; set; } + + /// <summary> + /// Gets or sets the album. + /// </summary> + /// <value>The album.</value> + [IgnoreDataMember] + public string Album { get; set; } + + /// <summary> + /// Gets or sets the channel identifier. + /// </summary> + /// <value>The channel identifier.</value> + [IgnoreDataMember] + public Guid ChannelId { get; set; } + + [IgnoreDataMember] + public virtual bool SupportsAddingToPlaylist + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool AlwaysScanInternalMetadataPath + { + get { return false; } + } + + /// <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> + [IgnoreDataMember] + public bool IsInMixedFolder { get; set; } + + [IgnoreDataMember] + public virtual bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsPositionTicksResume + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsRemoteImageDownloading + { + get + { + return true; + } + } + + private string _name; + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + [IgnoreDataMember] + public virtual string Name + { + get + { + return _name; + } + set + { + _name = value; + + // lazy load this again + _sortName = null; + } + } + + [IgnoreDataMember] + public bool IsUnaired + { + get { return PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; } + } + + [IgnoreDataMember] + public int? TotalBitrate { get; set; } + [IgnoreDataMember] + public ExtraType? ExtraType { get; set; } + + [IgnoreDataMember] + public bool IsThemeMedia + { + get + { + return ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); + } + } + + [IgnoreDataMember] + public string OriginalTitle { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [IgnoreDataMember] + public Guid Id { get; set; } + + [IgnoreDataMember] + public Guid OwnerId { get; set; } + + /// <summary> + /// Gets or sets the audio. + /// </summary> + /// <value>The audio.</value> + [IgnoreDataMember] + public ProgramAudio? Audio { get; set; } + + /// <summary> + /// Return the id that should be used to key display prefs for this item. + /// Default is based on the type for everything except actual generic folders. + /// </summary> + /// <value>The display prefs id.</value> + [IgnoreDataMember] + public virtual Guid DisplayPreferencesId + { + get + { + var thisType = GetType(); + return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5(); + } + } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + [IgnoreDataMember] + public virtual string Path { get; set; } + + [IgnoreDataMember] + public virtual SourceType SourceType + { + get + { + if (!ChannelId.Equals(Guid.Empty)) + { + return SourceType.Channel; + } + + return SourceType.Library; + } + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + [IgnoreDataMember] + public virtual string ContainingFolderPath + { + get + { + if (IsFolder) + { + return Path; + } + + return FileSystem.GetDirectoryName(Path); + } + } + + /// <summary> + /// Gets or sets the name of the service. + /// </summary> + /// <value>The name of the service.</value> + [IgnoreDataMember] + public string ServiceName { get; set; } + + /// <summary> + /// If this content came from an external service, the id of the content on that service + /// </summary> + [IgnoreDataMember] + public string ExternalId { get; set; } + + [IgnoreDataMember] + public string ExternalSeriesId { get; set; } + + /// <summary> + /// Gets or sets the etag. + /// </summary> + /// <value>The etag.</value> + [IgnoreDataMember] + public string ExternalEtag { get; set; } + + [IgnoreDataMember] + public virtual bool IsHidden + { + get + { + return false; + } + } + + public BaseItem GetOwner() + { + var ownerId = OwnerId; + return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); + } + + /// <summary> + /// Gets or sets the type of the location. + /// </summary> + /// <value>The type of the location.</value> + [IgnoreDataMember] + public virtual LocationType LocationType + { + get + { + //if (IsOffline) + //{ + // return LocationType.Offline; + //} + + var path = Path; + if (string.IsNullOrEmpty(path)) + { + if (SourceType == SourceType.Channel) + { + return LocationType.Remote; + } + + return LocationType.Virtual; + } + + return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote; + } + } + + [IgnoreDataMember] + public MediaProtocol? PathProtocol + { + get + { + var path = Path; + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + return MediaSourceManager.GetPathProtocol(path); + } + } + + public bool IsPathProtocol(MediaProtocol protocol) + { + var current = PathProtocol; + + return current.HasValue && current.Value == protocol; + } + + [IgnoreDataMember] + public bool IsFileProtocol + { + get + { + return IsPathProtocol(MediaProtocol.File); + } + } + + [IgnoreDataMember] + public bool HasPathProtocol + { + get + { + return PathProtocol.HasValue; + } + } + + [IgnoreDataMember] + public virtual bool SupportsLocalMetadata + { + get + { + if (SourceType == SourceType.Channel) + { + return false; + } + + return IsFileProtocol; + } + } + + [IgnoreDataMember] + public virtual string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileNameWithoutExtension(Path); + } + + return null; + } + } + + [IgnoreDataMember] + public virtual bool EnableAlphaNumericSorting + { + get + { + return true; + } + } + + private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) + { + var list = new List<Tuple<StringBuilder, bool>>(); + + int thisMarker = 0, thisNumericChunk = 0; + + while (thisMarker < s1.Length) + { + if (thisMarker >= s1.Length) + { + break; + } + char thisCh = s1[thisMarker]; + + StringBuilder thisChunk = new StringBuilder(); + + while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) + { + thisChunk.Append(thisCh); + thisMarker++; + + if (thisMarker < s1.Length) + { + thisCh = s1[thisMarker]; + } + } + + var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); + list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); + } + + return list; + } + + /// <summary> + /// This is just a helper for convenience + /// </summary> + /// <value>The primary image path.</value> + [IgnoreDataMember] + public string PrimaryImagePath + { + get { return this.GetImagePath(ImageType.Primary); } + } + + public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name) + { + if (SourceType == SourceType.Channel) + { + // hack alert + return !EnableMediaSourceDisplay; + } + + var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); + if (typeOptions != null) + { + return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + if (!libraryOptions.EnableInternetProviders) + { + return false; + } + + var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + + return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name) + { + if (this is Channel) + { + // hack alert + return true; + } + if (SourceType == SourceType.Channel) + { + // hack alert + return !EnableMediaSourceDisplay; + } + + var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); + if (typeOptions != null) + { + return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + if (!libraryOptions.EnableInternetProviders) + { + return false; + } + + var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + + return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + public virtual bool CanDelete() + { + if (SourceType == SourceType.Channel) + { + return ChannelManager.CanDelete(this); + } + + return IsFileProtocol; + } + + public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) + { + if (user.Policy.EnableContentDeletion) + { + return true; + } + + var allowed = user.Policy.EnableContentDeletionFromFolders; + + if (SourceType == SourceType.Channel) + { + return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase); + } + else + { + var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); + + foreach (var folder in collectionFolders) + { + if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + public bool CanDelete(User user, List<Folder> allCollectionFolders) + { + return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); + } + + public bool CanDelete(User user) + { + var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); + + return CanDelete(user, allCollectionFolders); + } + + public virtual bool CanDownload() + { + return false; + } + + public virtual bool IsAuthorizedToDownload(User user) + { + return user.Policy.EnableContentDownloading; + } + + public bool CanDownload(User user) + { + return CanDownload() && IsAuthorizedToDownload(user); + } + + /// <summary> + /// Gets or sets the date created. + /// </summary> + /// <value>The date created.</value> + [IgnoreDataMember] + public DateTime DateCreated { get; set; } + + /// <summary> + /// Gets or sets the date modified. + /// </summary> + /// <value>The date modified.</value> + [IgnoreDataMember] + public DateTime DateModified { get; set; } + + [IgnoreDataMember] + public DateTime DateLastSaved { get; set; } + + [IgnoreDataMember] + public DateTime DateLastRefreshed { get; set; } + + /// <summary> + /// The logger + /// </summary> + public static ILogger Logger { get; set; } + public static ILibraryManager LibraryManager { get; set; } + public static IServerConfigurationManager ConfigurationManager { get; set; } + public static IProviderManager ProviderManager { get; set; } + public static ILocalizationManager LocalizationManager { get; set; } + public static IItemRepository ItemRepository { get; set; } + public static IFileSystem FileSystem { get; set; } + public static IUserDataManager UserDataManager { get; set; } + public static IChannelManager ChannelManager { get; set; } + public static IMediaSourceManager MediaSourceManager { get; set; } + + /// <summary> + /// Returns a <see cref="System.String" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="System.String" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + [IgnoreDataMember] + public bool IsLocked { get; set; } + + /// <summary> + /// Gets or sets the locked fields. + /// </summary> + /// <value>The locked fields.</value> + [IgnoreDataMember] + public MetadataFields[] LockedFields { get; set; } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public virtual string MediaType + { + get + { + return null; + } + } + + [IgnoreDataMember] + public virtual string[] PhysicalLocations + { + get + { + if (!IsFileProtocol) + { + return new string[] { }; + } + + return new[] { Path }; + } + } + + private string _forcedSortName; + /// <summary> + /// Gets or sets the name of the forced sort. + /// </summary> + /// <value>The name of the forced sort.</value> + [IgnoreDataMember] + public string ForcedSortName + { + get { return _forcedSortName; } + set { _forcedSortName = value; _sortName = null; } + } + + private string _sortName; + /// <summary> + /// Gets the name of the sort. + /// </summary> + /// <value>The name of the sort.</value> + [IgnoreDataMember] + public string SortName + { + get + { + if (_sortName == null) + { + if (!string.IsNullOrEmpty(ForcedSortName)) + { + // Need the ToLower because that's what CreateSortName does + _sortName = ModifySortChunks(ForcedSortName).ToLower(); + } + else + { + _sortName = CreateSortName(); + } + } + return _sortName; + } + set + { + _sortName = value; + } + } + + public string GetInternalMetadataPath() + { + var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; + + return GetInternalMetadataPath(basePath); + } + + protected virtual string GetInternalMetadataPath(string basePath) + { + if (SourceType == SourceType.Channel) + { + return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N")); + } + + var idString = Id.ToString("N"); + + basePath = System.IO.Path.Combine(basePath, "library"); + + return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected virtual string CreateSortName() + { + if (Name == null) return null; //some items may not have name filled in properly + + if (!EnableAlphaNumericSorting) + { + return Name.TrimStart(); + } + + var sortable = Name.Trim().ToLower(); + + foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) + { + sortable = sortable.Replace(removeChar, string.Empty); + } + + foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) + { + sortable = sortable.Replace(replaceChar, " "); + } + + foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) + { + // Remove from beginning if a space follows + if (sortable.StartsWith(search + " ")) + { + sortable = sortable.Remove(0, search.Length + 1); + } + // Remove from middle if surrounded by spaces + sortable = sortable.Replace(" " + search + " ", " "); + + // Remove from end if followed by a space + if (sortable.EndsWith(" " + search)) + { + sortable = sortable.Remove(sortable.Length - (search.Length + 1)); + } + } + + return ModifySortChunks(sortable); + } + + private string ModifySortChunks(string name) + { + var chunks = GetSortChunks(name); + + var builder = new StringBuilder(); + + foreach (var chunk in chunks) + { + var chunkBuilder = chunk.Item1; + + // This chunk is numeric + if (chunk.Item2) + { + while (chunkBuilder.Length < 10) + { + chunkBuilder.Insert(0, '0'); + } + } + + builder.Append(chunkBuilder); + } + //Logger.Debug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); + return builder.ToString().RemoveDiacritics(); + } + + [IgnoreDataMember] + public bool EnableMediaSourceDisplay + { + get + { + if (SourceType == SourceType.Channel) + { + return ChannelManager.EnableMediaSourceDisplay(this); + } + + return true; + } + } + + [IgnoreDataMember] + public Guid ParentId { get; set; } + + /// <summary> + /// Gets or sets the parent. + /// </summary> + /// <value>The parent.</value> + [IgnoreDataMember] + public Folder Parent + { + get { return GetParent() as Folder; } + set + { + + } + } + + public void SetParent(Folder parent) + { + ParentId = parent == null ? Guid.Empty : parent.Id; + } + + public BaseItem GetParent() + { + var parentId = ParentId; + if (!parentId.Equals(Guid.Empty)) + { + return LibraryManager.GetItemById(parentId); + } + + return null; + } + + public IEnumerable<BaseItem> GetParents() + { + var parent = GetParent(); + + while (parent != null) + { + yield return parent; + + parent = parent.GetParent(); + } + } + + /// <summary> + /// Finds a parent of a given type + /// </summary> + /// <typeparam name="T"></typeparam> + /// <returns>``0.</returns> + public T FindParent<T>() + where T : Folder + { + foreach (var parent in GetParents()) + { + var item = parent as T; + if (item != null) + { + return item; + } + } + return null; + } + + [IgnoreDataMember] + public virtual Guid DisplayParentId + { + get + { + var parentId = ParentId; + return parentId; + } + } + + [IgnoreDataMember] + public BaseItem DisplayParent + { + get + { + var id = DisplayParentId; + if (id.Equals(Guid.Empty)) + { + return null; + } + return LibraryManager.GetItemById(id); + } + } + + /// <summary> + /// When the item first debuted. For movies this could be premiere date, episodes would be first aired + /// </summary> + /// <value>The premiere date.</value> + [IgnoreDataMember] + public DateTime? PremiereDate { get; set; } + + /// <summary> + /// Gets or sets the end date. + /// </summary> + /// <value>The end date.</value> + [IgnoreDataMember] + public DateTime? EndDate { get; set; } + + /// <summary> + /// Gets or sets the official rating. + /// </summary> + /// <value>The official rating.</value> + [IgnoreDataMember] + public string OfficialRating { get; set; } + + [IgnoreDataMember] + public int InheritedParentalRatingValue { get; set; } + + /// <summary> + /// Gets or sets the critic rating. + /// </summary> + /// <value>The critic rating.</value> + [IgnoreDataMember] + public float? CriticRating { get; set; } + + /// <summary> + /// Gets or sets the custom rating. + /// </summary> + /// <value>The custom rating.</value> + [IgnoreDataMember] + public string CustomRating { get; set; } + + /// <summary> + /// Gets or sets the overview. + /// </summary> + /// <value>The overview.</value> + [IgnoreDataMember] + public string Overview { get; set; } + + /// <summary> + /// Gets or sets the studios. + /// </summary> + /// <value>The studios.</value> + [IgnoreDataMember] + public string[] Studios { get; set; } + + /// <summary> + /// Gets or sets the genres. + /// </summary> + /// <value>The genres.</value> + [IgnoreDataMember] + public string[] Genres { get; set; } + + /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + [IgnoreDataMember] + public string[] Tags { get; set; } + + [IgnoreDataMember] + public string[] ProductionLocations { get; set; } + + /// <summary> + /// Gets or sets the home page URL. + /// </summary> + /// <value>The home page URL.</value> + [IgnoreDataMember] + public string HomePageUrl { get; set; } + + /// <summary> + /// Gets or sets the community rating. + /// </summary> + /// <value>The community rating.</value> + [IgnoreDataMember] + public float? CommunityRating { get; set; } + + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + [IgnoreDataMember] + public long? RunTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the production year. + /// </summary> + /// <value>The production year.</value> + [IgnoreDataMember] + public int? ProductionYear { get; set; } + + /// <summary> + /// If the item is part of a series, this is it's number in the series. + /// This could be episode number, album track number, etc. + /// </summary> + /// <value>The index number.</value> + [IgnoreDataMember] + public int? IndexNumber { get; set; } + + /// <summary> + /// For an episode this could be the season number, or for a song this could be the disc number. + /// </summary> + /// <value>The parent index number.</value> + [IgnoreDataMember] + public int? ParentIndexNumber { get; set; } + + [IgnoreDataMember] + public virtual bool HasLocalAlternateVersions + { + get { return false; } + } + + [IgnoreDataMember] + public string OfficialRatingForComparison + { + get + { + var officialRating = OfficialRating; + if (!string.IsNullOrEmpty(officialRating)) + { + return officialRating; + } + + var parent = DisplayParent; + if (parent != null) + { + return parent.OfficialRatingForComparison; + } + + return null; + } + } + + [IgnoreDataMember] + public string CustomRatingForComparison + { + get + { + var customRating = CustomRating; + if (!string.IsNullOrEmpty(customRating)) + { + return customRating; + } + + var parent = DisplayParent; + if (parent != null) + { + return parent.CustomRatingForComparison; + } + + return null; + } + } + + /// <summary> + /// Gets the play access. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>PlayAccess.</returns> + public PlayAccess GetPlayAccess(User user) + { + if (!user.Policy.EnableMediaPlayback) + { + return PlayAccess.None; + } + + //if (!user.IsParentalScheduleAllowed()) + //{ + // return PlayAccess.None; + //} + + return PlayAccess.Full; + } + + public virtual List<MediaStream> GetMediaStreams() + { + return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + { + ItemId = Id + }); + } + + protected virtual bool IsActiveRecording() + { + return false; + } + + public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + { + if (SourceType == SourceType.Channel) + { + var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None) + .ToList(); + + if (sources.Count > 0) + { + return sources; + } + } + + var list = GetAllItemsForMediaSources(); + var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item1, i.Item2)).ToList(); + + if (IsActiveRecording()) + { + foreach (var mediaSource in result) + { + mediaSource.Type = MediaSourceType.Placeholder; + } + } + + return result.OrderBy(i => + { + if (i.VideoType == VideoType.VideoFile) + { + return 0; + } + + return 1; + + }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) + .ThenByDescending(i => + { + var stream = i.VideoStream; + + return stream == null || stream.Width == null ? 0 : stream.Width.Value; + }) + .ToList(); + } + + protected virtual List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources() + { + return new List<Tuple<BaseItem, MediaSourceType>>(); + } + + private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type) + { + if (item == null) + { + throw new ArgumentNullException("media"); + } + + var protocol = item.PathProtocol; + + var info = new MediaSourceInfo + { + Id = item.Id.ToString("N"), + Protocol = protocol ?? MediaProtocol.File, + MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), + Name = GetMediaSourceName(item), + Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + RunTimeTicks = item.RunTimeTicks, + Container = item.Container, + Size = item.Size, + Type = type + }; + + if (string.IsNullOrEmpty(info.Path)) + { + info.Type = MediaSourceType.Placeholder; + } + + if (info.Protocol == MediaProtocol.File) + { + info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + } + + var video = item as Video; + if (video != null) + { + info.IsoType = video.IsoType; + info.VideoType = video.VideoType; + info.Video3DFormat = video.Video3DFormat; + info.Timestamp = video.Timestamp; + + if (video.IsShortcut) + { + info.IsRemote = true; + info.Path = video.ShortcutPath; + info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); + } + + if (string.IsNullOrEmpty(info.Container)) + { + if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) + { + if (protocol.HasValue && protocol.Value == MediaProtocol.File) + { + info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); + } + } + } + } + + if (string.IsNullOrEmpty(info.Container)) + { + if (protocol.HasValue && protocol.Value == MediaProtocol.File) + { + info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); + } + } + + if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path)) + { + info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol); + } + + if (video != null && video.VideoType != VideoType.VideoFile) + { + info.SupportsDirectStream = false; + } + + info.Bitrate = item.TotalBitrate; + info.InferTotalBitrate(); + + return info; + } + + private string GetMediaSourceName(BaseItem item) + { + var terms = new List<string>(); + + var path = item.Path; + if (item.IsFileProtocol && !string.IsNullOrEmpty(path)) + { + if (HasLocalAlternateVersions) + { + var displayName = System.IO.Path.GetFileNameWithoutExtension(path) + .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) + .TrimStart(new char[] { ' ', '-' }); + + if (!string.IsNullOrEmpty(displayName)) + { + terms.Add(displayName); + } + } + + if (terms.Count == 0) + { + var displayName = System.IO.Path.GetFileNameWithoutExtension(path); + terms.Add(displayName); + } + } + + if (terms.Count == 0) + { + terms.Add(item.Name); + } + + var video = item as Video; + if (video != null) + { + if (video.Video3DFormat.HasValue) + { + terms.Add("3D"); + } + + if (video.VideoType == VideoType.BluRay) + { + terms.Add("Bluray"); + } + else if (video.VideoType == VideoType.Dvd) + { + terms.Add("DVD"); + } + else if (video.VideoType == VideoType.Iso) + { + if (video.IsoType.HasValue) + { + if (video.IsoType.Value == Model.Entities.IsoType.BluRay) + { + terms.Add("Bluray"); + } + else if (video.IsoType.Value == Model.Entities.IsoType.Dvd) + { + terms.Add("DVD"); + } + } + else + { + terms.Add("ISO"); + } + } + } + + return string.Join("/", terms.ToArray(terms.Count)); + } + + /// <summary> + /// Loads the theme songs. + /// </summary> + /// <returns>List{Audio.Audio}.</returns> + private static Audio.Audio[] LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + { + var files = fileSystemChildren.Where(i => i.IsDirectory) + .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => FileSystem.GetFiles(i.FullName)) + .ToList(); + + // Support plex/xbmc convention + files.AddRange(fileSystemChildren + .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) + ); + + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + .OfType<Audio.Audio>() + .Select(audio => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio; + + if (dbItem != null) + { + audio = dbItem; + } + else + { + // item is new + audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong; + } + + return audio; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToArray(); + } + + /// <summary> + /// Loads the video backdrops. + /// </summary> + /// <returns>List{Video}.</returns> + private static Video[] LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + { + var files = fileSystemChildren.Where(i => i.IsDirectory) + .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => FileSystem.GetFiles(i.FullName)); + + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + .OfType<Video>() + .Select(item => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(item.Id) as Video; + + if (dbItem != null) + { + item = dbItem; + } + else + { + // item is new + item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; + } + + return item; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToArray(); + } + + public Task RefreshMetadata(CancellationToken cancellationToken) + { + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); + } + + protected virtual void TriggerOnRefreshStart() + { + + } + + protected virtual void TriggerOnRefreshComplete() + { + + } + + /// <summary> + /// Overrides the base implementation to refresh metadata for local trailers + /// </summary> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>true if a provider reports we changed</returns> + public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) + { + TriggerOnRefreshStart(); + + var requiresSave = false; + + if (SupportsOwnedItems) + { + try + { + var files = IsFileProtocol ? + GetFileSystemChildren(options.DirectoryService).ToList() : + new List<FileSystemMetadata>(); + + var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); + + if (ownedItemsChanged) + { + requiresSave = true; + } + } + catch (Exception ex) + { + Logger.ErrorException("Error refreshing owned items for {0}", ex, Path ?? Name); + } + } + + try + { + var refreshOptions = requiresSave + ? new MetadataRefreshOptions(options) + { + ForceSave = true + } + : options; + + return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); + } + finally + { + TriggerOnRefreshComplete(); + } + } + + [IgnoreDataMember] + protected virtual bool SupportsOwnedItems + { + get { return !ParentId.Equals(Guid.Empty) && IsFileProtocol; } + } + + [IgnoreDataMember] + public virtual bool SupportsPeople + { + get { return false; } + } + + [IgnoreDataMember] + public virtual bool SupportsThemeMedia + { + get { return false; } + } + + /// <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<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var themeSongsChanged = false; + + var themeVideosChanged = false; + + var localTrailersChanged = false; + + if (IsFileProtocol && SupportsOwnedItems) + { + if (SupportsThemeMedia) + { + if (!IsInMixedFolder) + { + themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + } + } + + var hasTrailers = this as IHasTrailers; + if (hasTrailers != null) + { + localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + } + } + + return themeSongsChanged || themeVideosChanged || localTrailersChanged; + } + + protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) + { + var path = ContainingFolderPath; + + return directoryService.GetFileSystemEntries(path); + } + + private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService).ToList(); + + var newItemIds = newItems.Select(i => i.Id).ToArray(); + + var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds); + var ownerId = item.Id; + + var tasks = newItems.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (!i.ExtraType.HasValue || + i.ExtraType.Value != Model.Entities.ExtraType.Trailer || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.ExtraType = Model.Entities.ExtraType.Trailer; + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.LocalTrailerIds = newItemIds; + + return itemsChanged; + } + + private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); + + var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray(newThemeVideos.Length); + + var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds); + + var ownerId = item.Id; + + var tasks = newThemeVideos.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (!i.ExtraType.HasValue || + i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.ExtraType = Model.Entities.ExtraType.ThemeVideo; + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.ThemeVideoIds = newThemeVideoIds; + + return themeVideosChanged; + } + + /// <summary> + /// Refreshes the theme songs. + /// </summary> + private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService); + var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray(newThemeSongs.Length); + + var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds); + + var ownerId = item.Id; + + var tasks = newThemeSongs.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (!i.ExtraType.HasValue || + i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.ExtraType = Model.Entities.ExtraType.ThemeSong; + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.ThemeSongIds = newThemeSongIds; + + return themeSongsChanged; + } + + /// <summary> + /// Gets or sets the provider ids. + /// </summary> + /// <value>The provider ids.</value> + [IgnoreDataMember] + public Dictionary<string, string> ProviderIds { get; set; } + + [IgnoreDataMember] + public virtual Folder LatestItemsIndexContainer + { + get { return null; } + } + + public virtual double GetDefaultPrimaryImageAspectRatio() + { + return 0; + } + + public virtual string CreatePresentationUniqueKey() + { + return Id.ToString("N"); + } + + [IgnoreDataMember] + public string PresentationUniqueKey { get; set; } + + public string GetPresentationUniqueKey() + { + return PresentationUniqueKey ?? CreatePresentationUniqueKey(); + } + + public virtual bool RequiresRefresh() + { + return false; + } + + public virtual List<string> GetUserDataKeys() + { + var list = new List<string>(); + + if (SourceType == SourceType.Channel) + { + if (!string.IsNullOrEmpty(ExternalId)) + { + list.Add(ExternalId); + } + } + + list.Add(Id.ToString()); + return list; + } + + internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) + { + var updateType = ItemUpdateType.None; + + if (IsInMixedFolder != newItem.IsInMixedFolder) + { + IsInMixedFolder = newItem.IsInMixedFolder; + updateType |= ItemUpdateType.MetadataImport; + } + + return updateType; + } + + public void AfterMetadataRefresh() + { + _sortName = null; + } + + /// <summary> + /// Gets the preferred metadata language. + /// </summary> + /// <returns>System.String.</returns> + public string GetPreferredMetadataLanguage() + { + string lang = PreferredMetadataLanguage; + + if (string.IsNullOrEmpty(lang)) + { + lang = GetParents() + .Select(i => i.PreferredMetadataLanguage) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetCollectionFolders(this) + .Select(i => i.PreferredMetadataLanguage) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage; + } + + if (string.IsNullOrEmpty(lang)) + { + lang = ConfigurationManager.Configuration.PreferredMetadataLanguage; + } + + return lang; + } + + /// <summary> + /// Gets the preferred metadata language. + /// </summary> + /// <returns>System.String.</returns> + public string GetPreferredMetadataCountryCode() + { + string lang = PreferredMetadataCountryCode; + + if (string.IsNullOrEmpty(lang)) + { + lang = GetParents() + .Select(i => i.PreferredMetadataCountryCode) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetCollectionFolders(this) + .Select(i => i.PreferredMetadataCountryCode) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode; + } + + if (string.IsNullOrEmpty(lang)) + { + lang = ConfigurationManager.Configuration.MetadataCountryCode; + } + + return lang; + } + + public virtual bool IsSaveLocalMetadataEnabled() + { + if (SourceType == SourceType.Channel) + { + return false; + } + + var libraryOptions = LibraryManager.GetLibraryOptions(this); + + return libraryOptions.SaveLocalMetadata; + } + + /// <summary> + /// Determines if a given user has access to this item + /// </summary> + /// <param name="user">The user.</param> + /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + public bool IsParentalAllowed(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (!IsVisibleViaTags(user)) + { + return false; + } + + var maxAllowedRating = user.Policy.MaxParentalRating; + + if (maxAllowedRating == null) + { + return true; + } + + var rating = CustomRatingForComparison; + + if (string.IsNullOrEmpty(rating)) + { + rating = OfficialRatingForComparison; + } + + if (string.IsNullOrEmpty(rating)) + { + return !GetBlockUnratedValue(user.Policy); + } + + var value = LocalizationManager.GetRatingLevel(rating); + + // Could not determine the integer value + if (!value.HasValue) + { + var isAllowed = !GetBlockUnratedValue(user.Policy); + + if (!isAllowed) + { + Logger.Debug("{0} has an unrecognized parental rating of {1}.", Name, rating); + } + + return isAllowed; + } + + return value.Value <= maxAllowedRating.Value; + } + + public int? GetParentalRatingValue() + { + var rating = CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = OfficialRating; + } + + if (string.IsNullOrEmpty(rating)) + { + return null; + } + + return LocalizationManager.GetRatingLevel(rating); + } + + public int? GetInheritedParentalRatingValue() + { + var rating = CustomRatingForComparison; + + if (string.IsNullOrEmpty(rating)) + { + rating = OfficialRatingForComparison; + } + + if (string.IsNullOrEmpty(rating)) + { + return null; + } + + return LocalizationManager.GetRatingLevel(rating); + } + + public List<string> GetInheritedTags() + { + var list = new List<string>(); + list.AddRange(Tags); + + foreach (var parent in GetParents()) + { + list.AddRange(parent.Tags); + } + + return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + } + + private bool IsVisibleViaTags(User user) + { + var policy = user.Policy; + if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + + return true; + } + + protected virtual bool IsAllowTagFilterEnforced() + { + return true; + } + + public virtual UnratedItem GetBlockUnratedType() + { + if (SourceType == SourceType.Channel) + { + return UnratedItem.ChannelContent; + } + + return UnratedItem.Other; + } + + /// <summary> + /// Gets the block unrated value. + /// </summary> + /// <param name="config">The configuration.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + protected virtual bool GetBlockUnratedValue(UserPolicy config) + { + // Don't block plain folders that are unrated. Let the media underneath get blocked + // Special folders like series and albums will override this method. + if (IsFolder) + { + return false; + } + if (this is IItemByName) + { + return false; + } + + return config.BlockUnratedItems.Contains(GetBlockUnratedType()); + } + + /// <summary> + /// Determines if this folder should be visible to a given user. + /// Default is just parental allowed. Can be overridden for more functionality. + /// </summary> + /// <param name="user">The user.</param> + /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + public virtual bool IsVisible(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + return IsParentalAllowed(user); + } + + public virtual bool IsVisibleStandalone(User user) + { + if (SourceType == SourceType.Channel) + { + return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user); + } + + return IsVisibleStandaloneInternal(user, true); + } + + [IgnoreDataMember] + public virtual bool SupportsInheritedParentImages + { + get { return false; } + } + + protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) + { + if (!IsVisible(user)) + { + return false; + } + + if (GetParents().Any(i => !i.IsVisible(user))) + { + return false; + } + + if (checkFolders) + { + var topParent = GetParents().LastOrDefault() ?? this; + + if (string.IsNullOrEmpty(topParent.Path)) + { + return true; + } + + var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList(); + + if (itemCollectionFolders.Count > 0) + { + var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList(); + if (!itemCollectionFolders.Any(userCollectionFolders.Contains)) + { + return false; + } + } + } + + return true; + } + + /// <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> + [IgnoreDataMember] + public virtual bool IsFolder + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool IsDisplayedAsFolder + { + get + { + return false; + } + } + + public virtual string GetClientTypeName() + { + if (IsFolder && SourceType == SourceType.Channel && !(this is Channel)) + { + return "ChannelFolderItem"; + } + + return GetType().Name; + } + + /// <summary> + /// Gets the linked child. + /// </summary> + /// <param name="info">The info.</param> + /// <returns>BaseItem.</returns> + protected BaseItem GetLinkedChild(LinkedChild info) + { + // First get using the cached Id + if (info.ItemId.HasValue) + { + if (info.ItemId.Value.Equals(Guid.Empty)) + { + return null; + } + + var itemById = LibraryManager.GetItemById(info.ItemId.Value); + + if (itemById != null) + { + return itemById; + } + } + + var item = FindLinkedChild(info); + + // If still null, log + if (item == null) + { + // Don't keep searching over and over + info.ItemId = Guid.Empty; + } + else + { + // Cache the id for next time + info.ItemId = item.Id; + } + + return item; + } + + private BaseItem FindLinkedChild(LinkedChild info) + { + var path = info.Path; + + if (!string.IsNullOrEmpty(path)) + { + path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path); + + var itemByPath = LibraryManager.FindByPath(path, null); + + if (itemByPath == null) + { + //Logger.Warn("Unable to find linked item at path {0}", info.Path); + } + + return itemByPath; + } + + if (!string.IsNullOrEmpty(info.LibraryItemId)) + { + var item = LibraryManager.GetItemById(info.LibraryItemId); + + if (item == null) + { + //Logger.Warn("Unable to find linked item at path {0}", info.Path); + } + + return item; + } + + return null; + } + + [IgnoreDataMember] + public virtual bool EnableRememberingTrackSelections + { + get + { + return true; + } + } + + /// <summary> + /// Adds a studio to the item + /// </summary> + /// <param name="name">The name.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddStudio(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + var current = Studios; + + if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + if (current.Length == 0) + { + Studios = new[] { name }; + } + else + { + var list = current.ToArray(current.Length + 1); + list[list.Length - 1] = name; + Studios = list; + } + } + } + + public void SetStudios(IEnumerable<string> names) + { + Studios = names.Distinct().ToArray(); + } + + /// <summary> + /// Adds a genre to the item + /// </summary> + /// <param name="name">The name.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddGenre(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + var genres = Genres; + if (!genres.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + var list = genres.ToList(); + list.Add(name); + Genres = list.ToArray(); + } + } + + /// <summary> + /// Marks the played. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="datePlayed">The date played.</param> + /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public virtual void MarkPlayed(User user, + DateTime? datePlayed, + bool resetPosition) + { + if (user == null) + { + throw new ArgumentNullException(); + } + + var data = UserDataManager.GetUserData(user, this); + + if (datePlayed.HasValue) + { + // Increment + data.PlayCount++; + } + + // Ensure it's at least one + data.PlayCount = Math.Max(data.PlayCount, 1); + + if (resetPosition) + { + data.PlaybackPositionTicks = 0; + } + + data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow; + data.Played = true; + + UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None); + } + + /// <summary> + /// Marks the unplayed. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public virtual void MarkUnplayed(User user) + { + if (user == null) + { + throw new ArgumentNullException(); + } + + var data = UserDataManager.GetUserData(user, this); + + //I think it is okay to do this here. + // if this is only called when a user is manually forcing something to un-played + // then it probably is what we want to do... + data.PlayCount = 0; + data.PlaybackPositionTicks = 0; + data.LastPlayedDate = null; + data.Played = false; + + UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None); + } + + /// <summary> + /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. + /// </summary> + /// <returns>Task.</returns> + public virtual void ChangedExternally() + { + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(FileSystem) + { + + }, RefreshPriority.High); + } + + /// <summary> + /// Gets an image + /// </summary> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> + public bool HasImage(ImageType type, int imageIndex) + { + return GetImageInfo(type, imageIndex) != null; + } + + public void SetImage(ItemImageInfo image, int index) + { + if (image.Type == ImageType.Chapter) + { + throw new ArgumentException("Cannot set chapter images using SetImagePath"); + } + + var existingImage = GetImageInfo(image.Type, index); + + if (existingImage != null) + { + existingImage.Path = image.Path; + existingImage.DateModified = image.DateModified; + existingImage.Width = image.Width; + existingImage.Height = image.Height; + } + + else + { + var currentCount = ImageInfos.Length; + var newList = ImageInfos.ToArray(currentCount + 1); + newList[currentCount] = image; + ImageInfos = newList; + } + } + + public void SetImagePath(ImageType type, int index, FileSystemMetadata file) + { + if (type == ImageType.Chapter) + { + throw new ArgumentException("Cannot set chapter images using SetImagePath"); + } + + var image = GetImageInfo(type, index); + + if (image == null) + { + var currentCount = ImageInfos.Length; + var newList = ImageInfos.ToArray(currentCount + 1); + newList[currentCount] = GetImageInfo(file, type); + ImageInfos = newList; + } + else + { + var imageInfo = GetImageInfo(file, type); + + image.Path = file.FullName; + image.DateModified = imageInfo.DateModified; + + // reset these values + image.Width = 0; + image.Height = 0; + } + } + + /// <summary> + /// Deletes the image. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="index">The index.</param> + /// <returns>Task.</returns> + public void DeleteImage(ImageType type, int index) + { + var info = GetImageInfo(type, index); + + if (info == null) + { + // Nothing to do + return; + } + + // Remove it from the item + RemoveImage(info); + + if (info.IsLocalFile) + { + FileSystem.DeleteFile(info.Path); + } + + UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + } + + public void RemoveImage(ItemImageInfo image) + { + RemoveImages(new List<ItemImageInfo> { image }); + } + + public void RemoveImages(List<ItemImageInfo> deletedImages) + { + ImageInfos = ImageInfos.Except(deletedImages).ToArray(); + } + + public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken); + } + + /// <summary> + /// Validates that images within the item are still on the file system + /// </summary> + public bool ValidateImages(IDirectoryService directoryService) + { + var allFiles = ImageInfos + .Where(i => i.IsLocalFile) + .Select(i => FileSystem.GetDirectoryName(i.Path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .SelectMany(i => directoryService.GetFilePaths(i)) + .ToList(); + + var deletedImages = ImageInfos + .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + if (deletedImages.Count > 0) + { + ImageInfos = ImageInfos.Except(deletedImages).ToArray(); + } + + return deletedImages.Count > 0; + } + + /// <summary> + /// Gets the image path. + /// </summary> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>System.String.</returns> + /// <exception cref="System.InvalidOperationException"> + /// </exception> + /// <exception cref="System.ArgumentNullException">item</exception> + public string GetImagePath(ImageType imageType, int imageIndex) + { + var info = GetImageInfo(imageType, imageIndex); + + return info == null ? null : info.Path; + } + + /// <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 chapter = ItemRepository.GetChapter(this, imageIndex); + + if (chapter == null) + { + return null; + } + + var path = chapter.ImagePath; + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + return new ItemImageInfo + { + Path = path, + DateModified = chapter.ImageDateModified, + Type = imageType + }; + } + + return GetImages(imageType) + .ElementAtOrDefault(imageIndex); + } + + public IEnumerable<ItemImageInfo> GetImages(ImageType imageType) + { + if (imageType == ImageType.Chapter) + { + throw new ArgumentException("No image info for chapter images"); + } + + return ImageInfos.Where(i => i.Type == imageType); + } + + /// <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> + /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception> + public bool AddImages(ImageType imageType, List<FileSystemMetadata> images) + { + if (imageType == ImageType.Chapter) + { + throw new ArgumentException("Cannot call AddImages with chapter images"); + } + + var existingImages = GetImages(imageType) + .ToList(); + + var newImageList = new List<FileSystemMetadata>(); + var imageAdded = false; + var imageUpdated = false; + + foreach (var newImage in images) + { + if (newImage == null) + { + throw new ArgumentException("null image found in list"); + } + + var existing = existingImages + .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); + + if (existing == null) + { + newImageList.Add(newImage); + imageAdded = true; + } + else + { + if (existing.IsLocalFile) + { + var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage); + + // If date changed then we need to reset saved image dimensions + if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0)) + { + existing.Width = 0; + existing.Height = 0; + imageUpdated = true; + } + + existing.DateModified = newDateModified; + } + } + } + + if (imageAdded || images.Count != existingImages.Count) + { + var newImagePaths = images.Select(i => i.FullName).ToList(); + + var deleted = existingImages + .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !FileSystem.FileExists(i.Path)) + .ToList(); + + if (deleted.Count > 0) + { + ImageInfos = ImageInfos.Except(deleted).ToArray(); + } + } + + if (newImageList.Count > 0) + { + var currentCount = ImageInfos.Length; + var newList = ImageInfos.ToArray(currentCount + newImageList.Count); + + foreach (var image in newImageList) + { + newList[currentCount] = GetImageInfo(image, imageType); + currentCount++; + } + + ImageInfos = newList; + } + + return imageUpdated || newImageList.Count > 0; + } + + private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type) + { + return new ItemImageInfo + { + Path = file.FullName, + Type = type, + DateModified = FileSystem.GetLastWriteTimeUtc(file) + }; + } + + /// <summary> + /// Gets the file system path to delete when the item is to be deleted + /// </summary> + /// <returns></returns> + public virtual IEnumerable<FileSystemMetadata> GetDeletePaths() + { + return new[] { + new FileSystemMetadata + { + FullName = Path, + IsDirectory = IsFolder + } + }.Concat(GetLocalMetadataFilesToDelete()); + } + + protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete() + { + if (IsFolder || !IsInMixedFolder) + { + return new List<FileSystemMetadata>(); + } + + var filename = System.IO.Path.GetFileNameWithoutExtension(Path); + var extensions = new List<string> { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; + extensions.AddRange(SupportedImageExtensions); + + return FileSystem.GetFiles(FileSystem.GetDirectoryName(Path), extensions.ToArray(extensions.Count), false, false) + .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + public bool AllowsMultipleImages(ImageType type) + { + return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter; + } + + public void SwapImages(ImageType type, int index1, int index2) + { + if (!AllowsMultipleImages(type)) + { + throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); + } + + var info1 = GetImageInfo(type, index1); + var info2 = GetImageInfo(type, index2); + + if (info1 == null || info2 == null) + { + // Nothing to do + return; + } + + if (!info1.IsLocalFile || !info2.IsLocalFile) + { + // TODO: Not supported yet + return; + } + + 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); + + info1.Width = 0; + info1.Height = 0; + info2.Width = 0; + info2.Height = 0; + + UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + } + + public virtual bool IsPlayed(User user) + { + var userdata = UserDataManager.GetUserData(user, this); + + return userdata != null && userdata.Played; + } + + public bool IsFavoriteOrLiked(User user) + { + var userdata = UserDataManager.GetUserData(user, this); + + return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false)); + } + + public virtual bool IsUnplayed(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var userdata = UserDataManager.GetUserData(user, this); + + 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 = GetNameForMetadataLookup(), + ProviderIds = ProviderIds, + IndexNumber = IndexNumber, + ParentIndexNumber = ParentIndexNumber, + Year = ProductionYear, + PremiereDate = PremiereDate + }; + } + + protected virtual string GetNameForMetadataLookup() + { + return Name; + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + _sortName = null; + + var hasChanges = false; + + if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path)) + { + Name = FileSystem.GetFileNameWithoutExtension(Path); + hasChanges = true; + } + + return hasChanges; + } + + protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol) + { + if (protocol.HasValue && protocol.Value == MediaProtocol.File) + { + return LibraryManager.GetPathAfterNetworkSubstitution(path, item); + } + + return path; + } + + public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) + { + if (RunTimeTicks.HasValue) + { + double pct = RunTimeTicks.Value; + + if (pct > 0) + { + pct = userData.PlaybackPositionTicks / pct; + + if (pct > 0) + { + dto.PlayedPercentage = 100 * pct; + } + } + } + } + + protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var newOptions = new MetadataRefreshOptions(options); + newOptions.SearchResult = null; + + var item = this; + + if (copyTitleMetadata) + { + // Take some data from the main item, for querying purposes + if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal)) + { + newOptions.ForceSave = true; + ownedItem.Genres = item.Genres; + } + if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) + { + newOptions.ForceSave = true; + ownedItem.Studios = item.Studios; + } + if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) + { + newOptions.ForceSave = true; + ownedItem.ProductionLocations = item.ProductionLocations; + } + if (item.CommunityRating != ownedItem.CommunityRating) + { + ownedItem.CommunityRating = item.CommunityRating; + newOptions.ForceSave = true; + } + if (item.CriticRating != ownedItem.CriticRating) + { + ownedItem.CriticRating = item.CriticRating; + newOptions.ForceSave = true; + } + if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) + { + ownedItem.Overview = item.Overview; + newOptions.ForceSave = true; + } + if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) + { + ownedItem.OfficialRating = item.OfficialRating; + newOptions.ForceSave = true; + } + if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) + { + ownedItem.CustomRating = item.CustomRating; + newOptions.ForceSave = true; + } + } + + return ownedItem.RefreshMetadata(newOptions, cancellationToken); + } + + protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) + { + var newOptions = new MetadataRefreshOptions(options); + newOptions.SearchResult = null; + + var id = LibraryManager.GetNewItemId(path, typeof(Video)); + + // Try to retrieve it from the db. If we don't find it, use the resolved version + var video = LibraryManager.GetItemById(id) as Video; + + if (video == null) + { + video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; + + newOptions.ForceSave = true; + } + + //var parentId = Id; + //if (!video.IsOwnedItem || video.ParentId != parentId) + //{ + // video.IsOwnedItem = true; + // video.ParentId = parentId; + // newOptions.ForceSave = true; + //} + + if (video == null) + { + return Task.FromResult(true); + } + + return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); + } + + public string GetEtag(User user) + { + var list = GetEtagValues(user); + + return string.Join("|", list.ToArray(list.Count)).GetMD5().ToString("N"); + } + + protected virtual List<string> GetEtagValues(User user) + { + return new List<string> + { + DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) + }; + } + + public virtual IEnumerable<Guid> GetAncestorIds() + { + return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id)); + } + + public BaseItem GetTopParent() + { + if (IsTopParent) + { + return this; + } + + foreach (var parent in GetParents()) + { + if (parent.IsTopParent) + { + return parent; + } + } + return null; + } + + [IgnoreDataMember] + public virtual bool IsTopParent + { + get + { + if (this is BasePluginFolder || this is Channel) + { + return true; + } + + var view = this as IHasCollectionType; + if (view != null) + { + if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + if (GetParent() is AggregateFolder) + { + return true; + } + + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsAncestors + { + get + { + return true; + } + } + + [IgnoreDataMember] + public virtual bool StopRefreshIfLocalMetadataFound + { + get + { + return true; + } + } + + public virtual IEnumerable<Guid> GetIdsForAncestorQuery() + { + return new[] { Id }; + } + + public virtual List<ExternalUrl> GetRelatedUrls() + { + return new List<ExternalUrl>(); + } + + public virtual double? GetRefreshProgress() + { + return null; + } + + public virtual ItemUpdateType OnMetadataChanged() + { + var updateType = ItemUpdateType.None; + + var item = this; + + var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0; + if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) + { + item.InheritedParentalRatingValue = inheritedParentalRatingValue; + updateType |= ItemUpdateType.MetadataImport; + } + + return updateType; + } + + /// <summary> + /// Updates the official rating based on content and returns true or false indicating if it changed. + /// </summary> + /// <returns></returns> + public bool UpdateRatingToItems(IList<BaseItem> children) + { + var currentOfficialRating = OfficialRating; + + // Gather all possible ratings + var ratings = children + .Select(i => i.OfficialRating) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(i => new Tuple<string, int?>(i, LocalizationManager.GetRatingLevel(i))) + .OrderBy(i => i.Item2 ?? 1000) + .Select(i => i.Item1); + + OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; + + return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty, + StringComparison.OrdinalIgnoreCase); + } + + public IEnumerable<BaseItem> GetThemeSongs() + { + return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName); + } + + public IEnumerable<BaseItem> GetThemeVideos() + { + return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + } + + public MediaUrl[] RemoteTrailers { get; set; } + + public IEnumerable<BaseItem> GetExtras() + { + return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + } + + public IEnumerable<BaseItem> GetExtras(ExtraType[] unused) + { + return GetExtras(); + } + + public IEnumerable<BaseItem> GetDisplayExtras() + { + return GetExtras(); + } + + public virtual bool IsHD { + get{ + return Height >= 720; + } + } + public bool IsShortcut{ get; set;} + public string ShortcutPath{ get; set;} + public int Width { get; set; } + public int Height { get; set; } + public Guid[] ExtraIds { get; set; } + public virtual long GetRunTimeTicksForPlayState() { + return RunTimeTicks ?? 0; + } + // what does this do? + public static ExtraType[] DisplayExtraTypes = new[] {Model.Entities.ExtraType.ThemeSong, Model.Entities.ExtraType.ThemeVideo }; + public virtual bool SupportsExternalTransfer { + get { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs new file mode 100644 index 000000000..c56a370a8 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Entities +{ + public static class BaseItemExtensions + { + /// <summary> + /// Gets the image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <returns>System.String.</returns> + public static string GetImagePath(this BaseItem item, ImageType imageType) + { + return item.GetImagePath(imageType, 0); + } + + public static bool HasImage(this BaseItem item, ImageType imageType) + { + return item.HasImage(imageType, 0); + } + + /// <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 BaseItem item, ImageType imageType, FileSystemMetadata 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 BaseItem item, ImageType imageType, string file) + { + if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase)) + { + item.SetImage(new ItemImageInfo + { + Path = file, + Type = imageType + }, 0); + } + else + { + item.SetImagePath(imageType, BaseItem.FileSystem.GetFileInfo(file)); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs new file mode 100644 index 000000000..c06f1cef4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -0,0 +1,54 @@ + +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Plugins derive from and export this class to create a folder that will appear in the root along + /// with all the other actual physical folders in the system. + /// </summary> + public abstract class BasePluginFolder : Folder, ICollectionFolder + { + [IgnoreDataMember] + public virtual string CollectionType + { + get { return null; } + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + //public override double? GetDefaultPrimaryImageAspectRatio() + //{ + // double value = 16; + // value /= 9; + + // return value; + //} + } +} diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs new file mode 100644 index 000000000..6814570c3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries + { + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Book; + } + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + [IgnoreDataMember] + public string SeriesName { get; set; } + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public string FindSeriesSortName() + { + return SeriesName; + } + public string FindSeriesName() + { + return SeriesName; + } + public string FindSeriesPresentationUniqueKey() + { + return SeriesPresentationUniqueKey; + } + + public Guid FindSeriesId() + { + return SeriesId; + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Book; + } + + public BookInfo GetLookupInfo() + { + var info = GetItemLookupInfo<BookInfo>(); + + if (string.IsNullOrEmpty(SeriesName)) + { + info.SeriesName = GetParents().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 new file mode 100644 index 000000000..8240a68ff --- /dev/null +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -0,0 +1,405 @@ +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Specialized Folder class that points to a subset of the physical folders in the system. + /// It is created from the user-specific folders within the system root + /// </summary> + public class CollectionFolder : Folder, ICollectionFolder + { + public static IXmlSerializer XmlSerializer { get; set; } + public static IJsonSerializer JsonSerializer { get; set; } + public static IServerApplicationHost ApplicationHost { get; set; } + + public CollectionFolder() + { + PhysicalLocationsList = new string[] { }; + PhysicalFolderIds = new Guid[] { }; + } + + //public override double? GetDefaultPrimaryImageAspectRatio() + //{ + // double value = 16; + // value /= 9; + + // return value; + //} + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + public override bool CanDelete() + { + return false; + } + + public string CollectionType { get; set; } + + private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>(); + public LibraryOptions GetLibraryOptions() + { + return GetLibraryOptions(Path); + } + + private static LibraryOptions LoadLibraryOptions(string path) + { + try + { + var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; + + if (result == null) + { + return new LibraryOptions(); + } + + foreach (var mediaPath in result.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) + { + mediaPath.Path = ApplicationHost.ExpandVirtualPath(mediaPath.Path); + } + } + + return result; + } + catch (FileNotFoundException) + { + return new LibraryOptions(); + } + catch (IOException) + { + return new LibraryOptions(); + } + catch (Exception ex) + { + Logger.ErrorException("Error loading library options", ex); + + return new LibraryOptions(); + } + } + + private static string GetLibraryOptionsPath(string path) + { + return System.IO.Path.Combine(path, "options.xml"); + } + + public void UpdateLibraryOptions(LibraryOptions options) + { + SaveLibraryOptions(Path, options); + } + + public static LibraryOptions GetLibraryOptions(string path) + { + lock (LibraryOptions) + { + LibraryOptions options; + if (!LibraryOptions.TryGetValue(path, out options)) + { + options = LoadLibraryOptions(path); + LibraryOptions[path] = options; + } + + return options; + } + } + + public static void SaveLibraryOptions(string path, LibraryOptions options) + { + lock (LibraryOptions) + { + LibraryOptions[path] = options; + + var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options)); + foreach (var mediaPath in clone.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) + { + mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); + } + } + + XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); + } + } + + public static void OnCollectionFolderChange() + { + lock (LibraryOptions) + { + LibraryOptions.Clear(); + } + } + + /// <summary> + /// Allow different display preferences for each collection folder + /// </summary> + /// <value>The display prefs id.</value> + [IgnoreDataMember] + public override Guid DisplayPreferencesId + { + get + { + return Id; + } + } + + [IgnoreDataMember] + public override string[] PhysicalLocations + { + get + { + return PhysicalLocationsList; + } + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public string[] PhysicalLocationsList { get; set; } + public Guid[] PhysicalFolderIds { get; set; } + + protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) + { + return CreateResolveArgs(directoryService, true).FileSystemChildren; + } + + private bool _requiresRefresh; + public override bool RequiresRefresh() + { + var changed = base.RequiresRefresh() || _requiresRefresh; + + if (!changed) + { + var locations = PhysicalLocations; + + var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + + if (!locations.SequenceEqual(newLocations)) + { + changed = true; + } + } + + if (!changed) + { + var folderIds = PhysicalFolderIds; + + var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + } + } + + return changed; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + _requiresRefresh = false; + return changed; + } + + public override double? GetRefreshProgress() + { + var folders = GetPhysicalFolders(true).ToList(); + double totalProgresses = 0; + var foldersWithProgress = 0; + + foreach (var folder in folders) + { + var progress = ProviderManager.GetRefreshProgress(folder.Id); + if (progress.HasValue) + { + totalProgresses += progress.Value; + foldersWithProgress++; + } + } + + if (foldersWithProgress == 0) + { + return null; + } + + return (totalProgresses / foldersWithProgress); + } + + protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) + { + return RefreshLinkedChildrenInternal(true); + } + + private bool RefreshLinkedChildrenInternal(bool setFolders) + { + var physicalFolders = GetPhysicalFolders(false) + .ToList(); + + var linkedChildren = physicalFolders + .SelectMany(c => c.LinkedChildren) + .ToList(); + + var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer(FileSystem)); + + LinkedChildren = linkedChildren.ToArray(linkedChildren.Count); + + var folderIds = PhysicalFolderIds; + var newFolderIds = physicalFolders.Select(i => i.Id).ToArray(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + if (setFolders) + { + PhysicalFolderIds = newFolderIds; + } + } + + return changed; + } + + private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) + { + var path = ContainingFolderPath; + + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + { + FileInfo = FileSystem.GetDirectoryInfo(path), + Path = path, + Parent = GetParent() as Folder, + CollectionType = CollectionType + }; + + // Gather child folder and files + if (args.IsDirectory) + { + var flattenFolderDepth = 0; + + var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true); + + args.FileSystemChildren = files; + } + + _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations); + + if (setPhysicalLocations) + { + PhysicalLocationsList = args.PhysicalLocations; + } + + return args; + } + + /// <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*** + /// </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="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, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + return Task.FromResult(true); + } + + /// <summary> + /// Our children are actually just references to the ones in the physical root... + /// </summary> + /// <value>The actual children.</value> + [IgnoreDataMember] + public override IEnumerable<BaseItem> Children + { + get { return GetActualChildren(); } + } + + public IEnumerable<BaseItem> GetActualChildren() + { + return GetPhysicalFolders(true).SelectMany(c => c.Children); + } + + public IEnumerable<Folder> GetPhysicalFolders() + { + return GetPhysicalFolders(true); + } + + private IEnumerable<Folder> GetPhysicalFolders(bool enableCache) + { + if (enableCache) + { + return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType<Folder>(); + } + + var rootChildren = LibraryManager.RootFolder.Children + .OfType<Folder>() + .ToList(); + + return PhysicalLocations.Where(i => !FileSystem.AreEqual(i, Path)).SelectMany(i => GetPhysicalParents(i, rootChildren)).DistinctBy(i => i.Id); + } + + private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren) + { + var result = rootChildren + .Where(i => FileSystem.AreEqual(i.Path, path)) + .ToList(); + + if (result.Count == 0) + { + var folder = LibraryManager.FindByPath(path, true) as Folder; + + if (folder != null) + { + result.Add(folder); + } + } + + return result; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs new file mode 100644 index 000000000..166ef66d4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs @@ -0,0 +1,71 @@ +using MediaBrowser.Model.Configuration; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + public static class DayOfWeekHelper + { + public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day) + { + return GetDaysOfWeek(new List<DynamicDayOfWeek> { day }); + } + + public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days) + { + var list = new List<DayOfWeek>(); + + if (days.Contains(DynamicDayOfWeek.Sunday) || + days.Contains(DynamicDayOfWeek.Weekend) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Sunday); + } + + if (days.Contains(DynamicDayOfWeek.Saturday) || + days.Contains(DynamicDayOfWeek.Weekend) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Saturday); + } + + if (days.Contains(DynamicDayOfWeek.Monday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Monday); + } + + if (days.Contains(DynamicDayOfWeek.Tuesday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Tuesday + ); + } + + if (days.Contains(DynamicDayOfWeek.Wednesday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Wednesday); + } + + if (days.Contains(DynamicDayOfWeek.Thursday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Thursday); + } + + if (days.Contains(DynamicDayOfWeek.Friday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Friday); + } + + return list; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs new file mode 100644 index 000000000..c706cf36c --- /dev/null +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Linq; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Extensions + /// </summary> + public static class Extensions + { + /// <summary> + /// Adds the trailer URL. + /// </summary> + public static void AddTrailerUrl(this BaseItem item, string url) + { + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentNullException("url"); + } + + var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase)); + + if (current == null) + { + var mediaUrl = new MediaUrl + { + Url = url + }; + + if (item.RemoteTrailers.Length == 0) + { + item.RemoteTrailers = new[] { mediaUrl }; + } + else + { + var list = item.RemoteTrailers.ToArray(item.RemoteTrailers.Length + 1); + list[list.Length - 1] = mediaUrl; + + item.RemoteTrailers = list; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs new file mode 100644 index 000000000..8b9aa5fc3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -0,0 +1,1803 @@ +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Folder + /// </summary> + public class Folder : BaseItem + { + public static IUserManager UserManager { get; set; } + public static IUserViewManager UserViewManager { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is root. + /// </summary> + /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value> + public bool IsRoot { get; set; } + + public LinkedChild[] LinkedChildren { get; set; } + + [IgnoreDataMember] + public DateTime? DateLastMediaAdded { get; set; } + + public Folder() + { + LinkedChildren = EmptyLinkedChildArray; + } + + [IgnoreDataMember] + public override bool SupportsThemeMedia + { + get { return true; } + } + + [IgnoreDataMember] + public virtual bool IsPreSorted + { + get { return false; } + } + + [IgnoreDataMember] + public virtual bool IsPhysicalRoot + { + get { return false; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + /// <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> + [IgnoreDataMember] + public override bool IsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public virtual bool SupportsCumulativeRunTimeTicks + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsDateLastMediaAdded + { + get + { + return false; + } + } + + public override bool CanDelete() + { + if (IsRoot) + { + return false; + } + + return base.CanDelete(); + } + + public override bool RequiresRefresh() + { + var baseResult = base.RequiresRefresh(); + + if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue) + { + baseResult = true; + } + + return baseResult; + } + + [IgnoreDataMember] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileName(Path); + } + + return null; + } + } + + protected override bool IsAllowTagFilterEnforced() + { + if (this is ICollectionFolder) + { + return false; + } + if (this is UserView) + { + return false; + } + return true; + } + + [IgnoreDataMember] + protected virtual bool SupportsShortcutChildren + { + get { return false; } + } + + /// <summary> + /// Adds the child. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.InvalidOperationException">Unable to add + item.Name</exception> + public void AddChild(BaseItem item, CancellationToken cancellationToken) + { + item.SetParent(this); + + if (item.Id.Equals(Guid.Empty)) + { + item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType()); + } + + if (item.DateCreated == DateTime.MinValue) + { + item.DateCreated = DateTime.UtcNow; + } + if (item.DateModified == DateTime.MinValue) + { + item.DateModified = DateTime.UtcNow; + } + + LibraryManager.CreateItem(item, this); + } + + /// <summary> + /// Gets the actual children. + /// </summary> + /// <value>The actual children.</value> + [IgnoreDataMember] + public virtual IEnumerable<BaseItem> Children + { + get + { + return LoadChildren(); + } + } + + /// <summary> + /// thread-safe access to all recursive children of this folder - without regard to user + /// </summary> + /// <value>The recursive children.</value> + [IgnoreDataMember] + public IEnumerable<BaseItem> RecursiveChildren + { + get { return GetRecursiveChildren(); } + } + + public override bool IsVisible(User user) + { + if (this is ICollectionFolder && !(this is BasePluginFolder)) + { + if (user.Policy.BlockedMediaFolders != null) + { + if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || + + // Backwards compatibility + user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + else + { + if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + } + + return base.IsVisible(user); + } + + /// <summary> + /// Loads our children. Validation will occur externally. + /// We want this sychronous. + /// </summary> + protected virtual List<BaseItem> LoadChildren() + { + //Logger.Debug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); + //just load our children from the repo - the library will be validated and maintained in other processes + return GetCachedChildren(); + } + + public override double? GetRefreshProgress() + { + return ProviderManager.GetRefreshProgress(Id); + } + + public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) + { + return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); + } + + /// <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> + /// <returns>Task.</returns> + public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) + { + return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); + } + + private Dictionary<Guid, BaseItem> GetActualChildrenDictionary() + { + var dictionary = new Dictionary<Guid, BaseItem>(); + + var childrenList = Children.ToList(); + + foreach (var child in childrenList) + { + var id = child.Id; + if (dictionary.ContainsKey(id)) + { + Logger.Error("Found folder containing items with duplicate id. Path: {0}, Child Name: {1}", + Path ?? Name, + child.Path ?? child.Name); + } + else + { + dictionary[id] = child; + } + } + + return dictionary; + } + + protected override void TriggerOnRefreshStart() + { + } + + protected override void TriggerOnRefreshComplete() + { + } + + /// <summary> + /// 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="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 virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + if (recursive) + { + ProviderManager.OnRefreshStart(this); + } + + try + { + await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false); + } + finally + { + if (recursive) + { + ProviderManager.OnRefreshComplete(this); + } + } + } + + private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + cancellationToken.ThrowIfCancellationRequested(); + + var validChildren = new List<BaseItem>(); + var validChildrenNeedGeneration = false; + + if (IsFileProtocol) + { + IEnumerable<BaseItem> nonCachedChildren; + + try + { + nonCachedChildren = GetNonCachedChildren(directoryService); + } + catch (Exception ex) + { + return; + } + + progress.Report(5); + + if (recursive) + { + ProviderManager.OnRefreshProgress(this, 5); + } + + //build a dictionary of the current children we have now by Id so we can compare quickly and easily + var currentChildren = GetActualChildrenDictionary(); + + //create a list for our validated children + var newItems = new List<BaseItem>(); + + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var child in nonCachedChildren) + { + BaseItem currentChild; + + if (currentChildren.TryGetValue(child.Id, out currentChild)) + { + validChildren.Add(currentChild); + + if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None) + { + currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + } + + continue; + } + + // Brand new item - needs to be added + child.SetParent(this); + newItems.Add(child); + validChildren.Add(child); + } + + // If any items were added or removed.... + if (newItems.Count > 0 || currentChildren.Count != validChildren.Count) + { + // That's all the new and changed ones - now see if there are any that are missing + var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); + + foreach (var item in itemsRemoved) + { + if (!item.IsFileProtocol) + { + } + + else + { + Logger.Debug("Removed item: " + item.Path); + + item.SetParent(null); + LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); + } + } + + LibraryManager.CreateItems(newItems, this, cancellationToken); + } + } + else + { + validChildrenNeedGeneration = true; + } + + progress.Report(10); + + if (recursive) + { + ProviderManager.OnRefreshProgress(this, 10); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (recursive) + { + var innerProgress = new ActionableProgress<double>(); + + var folder = this; + innerProgress.RegisterAction(p => + { + double newPct = .80 * p + 10; + progress.Report(newPct); + ProviderManager.OnRefreshProgress(folder, newPct); + }); + + if (validChildrenNeedGeneration) + { + validChildren = Children.ToList(); + validChildrenNeedGeneration = false; + } + + await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cancellationToken).ConfigureAwait(false); + } + + if (refreshChildMetadata) + { + progress.Report(90); + + if (recursive) + { + ProviderManager.OnRefreshProgress(this, 90); + } + + var container = this as IMetadataContainer; + + var innerProgress = new ActionableProgress<double>(); + + var folder = this; + innerProgress.RegisterAction(p => + { + double newPct = .10 * p + 90; + progress.Report(newPct); + if (recursive) + { + ProviderManager.OnRefreshProgress(folder, newPct); + } + }); + + if (container != null) + { + await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false); + } + else + { + if (validChildrenNeedGeneration) + { + validChildren = Children.ToList(); + } + + await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken); + } + } + } + + private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + { + var numComplete = 0; + var count = children.Count; + double currentPercent = 0; + + foreach (var child in children) + { + cancellationToken.ThrowIfCancellationRequested(); + + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentInnerPercent = currentPercent; + + innerProgress.RegisterAction(p => + { + double innerPercent = currentInnerPercent; + innerPercent += p / (count); + progress.Report(innerPercent); + }); + + await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken) + .ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + currentPercent = percent; + + progress.Report(percent); + } + } + + private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + var series = container as Series; + if (series != null) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + } + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + } + + private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + { + var container = child as IMetadataContainer; + + if (container != null) + { + await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false); + } + else + { + if (refreshOptions.RefreshItem(child)) + { + await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + + if (recursive) + { + var folder = child as Folder; + + if (folder != null) + { + await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken); + } + } + } + } + + /// <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 numComplete = 0; + var count = children.Count; + double currentPercent = 0; + + foreach (var child in children) + { + cancellationToken.ThrowIfCancellationRequested(); + + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentInnerPercent = currentPercent; + + innerProgress.RegisterAction(p => + { + double innerPercent = currentInnerPercent; + innerPercent += p / (count); + progress.Report(innerPercent); + }); + + await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService) + .ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + currentPercent = percent; + + progress.Report(percent); + } + } + + /// <summary> + /// Get the children of this folder from the actual file system + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + var collectionType = LibraryManager.GetContentType(this); + var libraryOptions = LibraryManager.GetLibraryOptions(this); + + return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType); + } + + /// <summary> + /// Get our children from the repo - stubbed for now + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + protected List<BaseItem> GetCachedChildren() + { + return ItemRepository.GetItemList(new InternalItemsQuery + { + Parent = this, + GroupByPresentationUniqueKey = false, + DtoOptions = new DtoOptions(true) + }); + } + + public virtual int GetChildCount(User user) + { + if (LinkedChildren.Length > 0) + { + if (!(this is ICollectionFolder)) + { + return GetChildren(user, true).Count; + } + } + + var result = GetItems(new InternalItemsQuery(user) + { + Recursive = false, + Limit = 0, + Parent = this, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }); + + return result.TotalRecordCount; + } + + public virtual int GetRecursiveChildCount(User user) + { + return GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + IsVirtualItem = false, + EnableTotalRecordCount = true, + Limit = 0, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }).TotalRecordCount; + } + + public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query) + { + var user = query.User; + + if (!query.ForceDirect && RequiresPostFiltering(query)) + { + IEnumerable<BaseItem> items; + Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + + if (query.User == null) + { + items = GetRecursiveChildren(filter); + } + else + { + items = GetRecursiveChildren(user, query); + } + + return PostFilterAndSort(items, query, true); + } + + if (!(this is UserRootFolder) && !(this is AggregateFolder)) + { + if (!query.ParentId.Equals(Guid.Empty)) + { + query.Parent = this; + } + } + + if (RequiresPostFiltering2(query)) + { + return QueryWithPostFiltering2(query); + } + + return LibraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query) + { + var startIndex = query.StartIndex; + var limit = query.Limit; + + query.StartIndex = null; + query.Limit = null; + + var itemsList = LibraryManager.GetItemList(query); + var user = query.User; + + if (user != null) + { + // needed for boxsets + itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList(); + } + + BaseItem[] returnItems; + int totalCount = 0; + + if (query.EnableTotalRecordCount) + { + var itemsArray = itemsList.ToArray(); + totalCount = itemsArray.Length; + returnItems = itemsArray; + } + else + { + returnItems = itemsList.ToArray(); + } + + if (limit.HasValue) + { + returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray(); + } + else if (startIndex.HasValue) + { + returnItems = returnItems.Skip(startIndex.Value).ToArray(); + } + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = returnItems.ToArray() + }; + } + + private bool RequiresPostFiltering2(InternalItemsQuery query) + { + if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Query requires post-filtering due to BoxSet query"); + return true; + } + + return false; + } + + private bool RequiresPostFiltering(InternalItemsQuery query) + { + if (LinkedChildren.Length > 0) + { + if (!(this is ICollectionFolder)) + { + Logger.Debug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name); + return true; + } + } + + // Filter by Video3DFormat + if (query.Is3D.HasValue) + { + Logger.Debug("Query requires post-filtering due to Is3D"); + return true; + } + + if (query.HasOfficialRating.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasOfficialRating"); + return true; + } + + if (query.IsPlaceHolder.HasValue) + { + Logger.Debug("Query requires post-filtering due to IsPlaceHolder"); + return true; + } + + if (query.HasSpecialFeature.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasSpecialFeature"); + return true; + } + + if (query.HasSubtitles.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasSubtitles"); + return true; + } + + if (query.HasTrailer.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasTrailer"); + return true; + } + + // Filter by VideoType + if (query.VideoTypes.Length > 0) + { + Logger.Debug("Query requires post-filtering due to VideoTypes"); + return true; + } + + if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager)) + { + Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems"); + return true; + } + + if (!string.IsNullOrEmpty(query.AdjacentTo)) + { + Logger.Debug("Query requires post-filtering due to AdjacentTo"); + return true; + } + + if (query.SeriesStatuses.Length > 0) + { + Logger.Debug("Query requires post-filtering due to SeriesStatuses"); + return true; + } + + if (query.AiredDuringSeason.HasValue) + { + Logger.Debug("Query requires post-filtering due to AiredDuringSeason"); + return true; + } + + if (query.IsPlayed.HasValue) + { + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name)) + { + Logger.Debug("Query requires post-filtering due to IsPlayed"); + return true; + } + } + + return false; + } + + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) + { + if (query.ItemIds.Length > 0) + { + var result = LibraryManager.GetItemsResult(query); + + if (query.OrderBy.Length == 0) + { + var ids = query.ItemIds.ToList(); + + // Try to preserve order + result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + } + return result; + } + + return GetItemsInternal(query); + } + + public BaseItem[] GetItemList(InternalItemsQuery query) + { + query.EnableTotalRecordCount = false; + + if (query.ItemIds.Length > 0) + { + var result = LibraryManager.GetItemList(query); + + if (query.OrderBy.Length == 0) + { + var ids = query.ItemIds.ToList(); + + // Try to preserve order + return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + } + return result.ToArray(result.Count); + } + + return GetItemsInternal(query).Items; + } + + protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = this; + query.ChannelIds = new Guid[] { ChannelId }; + + // Don't blow up here because it could cause parent screens with other content to fail + return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result; + } + catch + { + // Already logged at lower levels + return new QueryResult<BaseItem>(); + } + } + + if (query.Recursive) + { + return QueryRecursive(query); + } + + var user = query.User; + + Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + + IEnumerable<BaseItem> items; + + if (query.User == null) + { + items = Children.Where(filter); + } + else + { + items = GetChildren(user, true).Where(filter); + } + + return PostFilterAndSort(items, query, true); + } + + public static ICollectionManager CollectionManager { get; set; } + + protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting) + { + var user = query.User; + + // Check recursive - don't substitute in plain folder views + if (user != null) + { + items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); + } + + if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) + { + items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1); + } + if (!string.IsNullOrEmpty(query.NameStartsWith)) + { + items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrEmpty(query.NameLessThan)) + { + items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1); + } + + // This must be the last filter + if (!string.IsNullOrEmpty(query.AdjacentTo)) + { + items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo); + } + + return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); + } + + private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items, + InternalItemsQuery query, + BaseItem queryParent, + User user, + IServerConfigurationManager configurationManager, ICollectionManager collectionManager) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) + { + items = collectionManager.CollapseItemsWithinBoxSets(items, user); + } + + return items; + } + + private static bool CollapseBoxSetItems(InternalItemsQuery query, + BaseItem queryParent, + User user, + IServerConfigurationManager configurationManager) + { + // Could end up stuck in a loop like this + if (queryParent is BoxSet) + { + return false; + } + if (queryParent is Series) + { + return false; + } + if (queryParent is Season) + { + return false; + } + if (queryParent is MusicAlbum) + { + return false; + } + if (queryParent is MusicArtist) + { + return false; + } + + var param = query.CollapseBoxSetItems; + + if (!param.HasValue) + { + if (user != null && !configurationManager.Configuration.EnableGroupingIntoCollections) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase)) + { + param = true; + } + } + + return param.HasValue && param.Value && AllowBoxSetCollapsing(query); + } + + private static bool AllowBoxSetCollapsing(InternalItemsQuery request) + { + if (request.IsFavorite.HasValue) + { + return false; + } + if (request.IsFavoriteOrLiked.HasValue) + { + return false; + } + if (request.IsLiked.HasValue) + { + return false; + } + if (request.IsPlayed.HasValue) + { + return false; + } + if (request.IsResumable.HasValue) + { + return false; + } + if (request.IsFolder.HasValue) + { + return false; + } + + if (request.Genres.Length > 0) + { + return false; + } + + if (request.GenreIds.Length > 0) + { + return false; + } + + if (request.HasImdbId.HasValue) + { + return false; + } + + if (request.HasOfficialRating.HasValue) + { + return false; + } + + if (request.HasOverview.HasValue) + { + return false; + } + + if (request.HasParentalRating.HasValue) + { + return false; + } + + if (request.HasSpecialFeature.HasValue) + { + return false; + } + + if (request.HasSubtitles.HasValue) + { + return false; + } + + if (request.HasThemeSong.HasValue) + { + return false; + } + + if (request.HasThemeVideo.HasValue) + { + return false; + } + + if (request.HasTmdbId.HasValue) + { + return false; + } + + if (request.HasTrailer.HasValue) + { + return false; + } + + if (request.ImageTypes.Length > 0) + { + return false; + } + + if (request.Is3D.HasValue) + { + return false; + } + + if (request.IsHD.HasValue) + { + return false; + } + + if (request.IsLocked.HasValue) + { + return false; + } + + if (request.IsPlaceHolder.HasValue) + { + return false; + } + + if (request.IsPlayed.HasValue) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(request.Person)) + { + return false; + } + + if (request.PersonIds.Length > 0) + { + return false; + } + + if (request.ItemIds.Length > 0) + { + return false; + } + + if (request.StudioIds.Length > 0) + { + return false; + } + + if (request.GenreIds.Length > 0) + { + return false; + } + + if (request.VideoTypes.Length > 0) + { + return false; + } + + if (request.Years.Length > 0) + { + return false; + } + + if (request.Tags.Length > 0) + { + return false; + } + + if (request.OfficialRatings.Length > 0) + { + return false; + } + + if (request.MinPlayers.HasValue) + { + return false; + } + + if (request.MaxPlayers.HasValue) + { + return false; + } + + if (request.MinCommunityRating.HasValue) + { + return false; + } + + if (request.MinCriticRating.HasValue) + { + return false; + } + + if (request.MinIndexNumber.HasValue) + { + return false; + } + + return true; + } + + public List<BaseItem> GetChildren(User user, bool includeLinkedChildren) + { + return GetChildren(user, includeLinkedChildren, null); + } + + public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + if (user == null) + { + throw new ArgumentNullException(); + } + + //the true root should return our users root folder children + if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + + var result = new Dictionary<Guid, BaseItem>(); + + AddChildren(user, includeLinkedChildren, result, false, query); + + return result.Values.ToList(); + } + + protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + return Children; + } + + /// <summary> + /// Adds the children to list. + /// </summary> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query) + { + foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) + { + bool? isVisibleToUser = null; + + if (query == null || UserViewBuilder.FilterItem(child, query)) + { + isVisibleToUser = child.IsVisible(user); + + if (isVisibleToUser.Value) + { + result[child.Id] = child; + } + } + + if (isVisibleToUser ?? child.IsVisible(user)) + { + if (recursive && child.IsFolder) + { + var folder = (Folder)child; + + folder.AddChildren(user, includeLinkedChildren, result, true, query); + } + } + } + + if (includeLinkedChildren) + { + foreach (var child in GetLinkedChildren(user)) + { + if (query == null || UserViewBuilder.FilterItem(child, query)) + { + if (child.IsVisible(user)) + { + result[child.Id] = child; + } + } + } + } + } + + /// <summary> + /// Gets allowed recursive children of an item + /// </summary> + /// <param name="user">The user.</param> + /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> + /// <returns>IEnumerable{BaseItem}.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true) + { + return GetRecursiveChildren(user, null); + } + + public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var result = new Dictionary<Guid, BaseItem>(); + + AddChildren(user, true, result, true, query); + + return result.Values; + } + + /// <summary> + /// Gets the recursive children. + /// </summary> + /// <returns>IList{BaseItem}.</returns> + public IList<BaseItem> GetRecursiveChildren() + { + return GetRecursiveChildren(true); + } + + public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren) + { + return GetRecursiveChildren(i => true, includeLinkedChildren); + } + + public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter) + { + return GetRecursiveChildren(filter, true); + } + + public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren) + { + var result = new Dictionary<Guid, BaseItem>(); + + AddChildrenToList(result, includeLinkedChildren, true, filter); + + return result.Values.ToList(); + } + + /// <summary> + /// Adds the children to list. + /// </summary> + private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Func<BaseItem, bool> filter) + { + foreach (var child in Children) + { + if (filter == null || filter(child)) + { + result[child.Id] = child; + } + + if (recursive && child.IsFolder) + { + var folder = (Folder)child; + + // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loop of linked items + folder.AddChildrenToList(result, false, true, filter); + } + } + + if (includeLinkedChildren) + { + foreach (var child in GetLinkedChildren()) + { + if (filter == null || filter(child)) + { + result[child.Id] = child; + } + } + } + } + + + /// <summary> + /// Gets the linked children. + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + public List<BaseItem> GetLinkedChildren() + { + var linkedChildren = LinkedChildren; + var list = new List<BaseItem>(linkedChildren.Length); + + foreach (var i in linkedChildren) + { + var child = GetLinkedChild(i); + + if (child != null) + { + list.Add(child); + } + } + return list; + } + + protected virtual bool FilterLinkedChildrenPerUser + { + get + { + return false; + } + } + + public bool ContainsLinkedChildByItemId(Guid itemId) + { + var linkedChildren = LinkedChildren; + foreach (var i in linkedChildren) + { + if (i.ItemId.HasValue && i.ItemId.Value == itemId) + { + return true; + } + + var child = GetLinkedChild(i); + + if (child != null && child.Id == itemId) + { + return true; + } + } + return false; + } + + public List<BaseItem> GetLinkedChildren(User user) + { + if (!FilterLinkedChildrenPerUser || user == null) + { + return GetLinkedChildren(); + } + + var linkedChildren = LinkedChildren; + var list = new List<BaseItem>(linkedChildren.Length); + + if (linkedChildren.Length == 0) + { + return list; + } + + var allUserRootChildren = LibraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .ToList(); + + var collectionFolderIds = allUserRootChildren + .Select(i => i.Id) + .ToList(); + + foreach (var i in linkedChildren) + { + var child = GetLinkedChild(i); + + if (child == null) + { + continue; + } + + var childOwner = child.GetOwner() ?? child; + + if (childOwner != null && !(child is IItemByName)) + { + var childProtocol = childOwner.PathProtocol; + if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File) + { + if (!childOwner.IsVisibleStandalone(user)) + { + continue; + } + } + else + { + var itemCollectionFolderIds = + LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id); + + if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains)) + { + continue; + } + } + } + + list.Add(child); + } + + return list; + } + + /// <summary> + /// Gets the linked children. + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos() + { + return LinkedChildren + .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i))) + .Where(i => i.Item2 != null); + } + + [IgnoreDataMember] + protected override bool SupportsOwnedItems + { + get + { + return base.SupportsOwnedItems || SupportsShortcutChildren; + } + } + + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var changesFound = false; + + if (IsFileProtocol) + { + if (RefreshLinkedChildren(fileSystemChildren)) + { + changesFound = true; + } + } + + var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + return baseHasChanges || changesFound; + } + + /// <summary> + /// Refreshes the linked children. + /// </summary> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) + { + if (SupportsShortcutChildren) + { + var newShortcutLinks = fileSystemChildren + .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName)) + .Select(i => + { + try + { + Logger.Debug("Found shortcut at {0}", i.FullName); + + var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShortcut(i.FullName)); + + if (!string.IsNullOrEmpty(resolvedPath)) + { + return new LinkedChild + { + Path = resolvedPath, + Type = LinkedChildType.Shortcut + }; + } + + Logger.Error("Error resolving shortcut {0}", i.FullName); + + return null; + } + catch (IOException ex) + { + Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName); + return null; + } + }) + .Where(i => i != null) + .ToList(); + + var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); + + if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem))) + { + Logger.Info("Shortcut links have changed for {0}", Path); + + newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual)); + LinkedChildren = newShortcutLinks.ToArray(newShortcutLinks.Count); + return true; + } + } + + foreach (var child in LinkedChildren) + { + // Reset the cached value + child.ItemId = null; + } + + return false; + } + + /// <summary> + /// Marks the played. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="datePlayed">The date played.</param> + /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> + /// <returns>Task.</returns> + public override void MarkPlayed(User user, + DateTime? datePlayed, + bool resetPosition) + { + var query = new InternalItemsQuery + { + User = user, + Recursive = true, + IsFolder = false, + EnableTotalRecordCount = false + }; + + if (!user.Configuration.DisplayMissingEpisodes) + { + query.IsVirtualItem = false; + } + + var itemsResult = GetItemList(query); + + // Sweep through recursively and update status + foreach (var item in itemsResult) + { + if (item.IsVirtualItem) + { + // The querying doesn't support virtual unaired + var episode = item as Episode; + if (episode != null && episode.IsUnaired) + { + continue; + } + } + + item.MarkPlayed(user, datePlayed, resetPosition); + } + } + + /// <summary> + /// Marks the unplayed. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + public override void MarkUnplayed(User user) + { + var itemsResult = GetItemList(new InternalItemsQuery + { + User = user, + Recursive = true, + IsFolder = false, + EnableTotalRecordCount = false + + }); + + // Sweep through recursively and update status + foreach (var item in itemsResult) + { + item.MarkUnplayed(user); + } + } + + public override bool IsPlayed(User user) + { + var itemsResult = GetItemList(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + IsVirtualItem = false, + EnableTotalRecordCount = false + + }); + + return itemsResult + .All(i => i.IsPlayed(user)); + } + + public override bool IsUnplayed(User user) + { + return !IsPlayed(user); + } + + [IgnoreDataMember] + public virtual bool SupportsUserDataFromChildren + { + get + { + // These are just far too slow. + if (this is ICollectionFolder) + { + return false; + } + if (this is UserView) + { + return false; + } + if (this is UserRootFolder) + { + return false; + } + if (this is Channel) + { + return false; + } + if (SourceType != SourceType.Library) + { + return false; + } + var iItemByName = this as IItemByName; + if (iItemByName != null) + { + var hasDualAccess = this as IHasDualAccess; + if (hasDualAccess == null || hasDualAccess.IsAccessedByName) + { + return false; + } + } + + return true; + } + } + + public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) + { + if (!SupportsUserDataFromChildren) + { + return; + } + + if (itemDto != null) + { + if (fields.ContainsField(ItemFields.RecursiveItemCount)) + { + itemDto.RecursiveItemCount = GetRecursiveChildCount(user); + } + } + + if (SupportsPlayedStatus) + { + var unplayedQueryResult = GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + IsVirtualItem = false, + EnableTotalRecordCount = true, + Limit = 0, + IsPlayed = false, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }); + + double unplayedCount = unplayedQueryResult.TotalRecordCount; + + dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; + + if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + { + if (itemDto.RecursiveItemCount.Value > 0) + { + var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; + } + } + else + { + dto.Played = (dto.UnplayedItemCount ?? 0) == 0; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs new file mode 100644 index 000000000..e4c417c8a --- /dev/null +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -0,0 +1,129 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public class Game : BaseItem, IHasTrailers, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo> + { + public Game() + { + MultiPartGameFiles = new string[] {}; + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + [IgnoreDataMember] + public override bool SupportsThemeMedia + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return false; } + } + + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public override string MediaType + { + get { return Model.Entities.MediaType.Game; } + } + + /// <summary> + /// Gets or sets the players supported. + /// </summary> + /// <value>The players supported.</value> + public int? PlayersSupported { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance is place holder. + /// </summary> + /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value> + public bool IsPlaceHolder { get; set; } + + /// <summary> + /// Gets or sets the game system. + /// </summary> + /// <value>The game system.</value> + public string GameSystem { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is multi part. + /// </summary> + /// <value><c>true</c> if this instance is multi part; otherwise, <c>false</c>.</value> + public bool IsMultiPart { get; set; } + + /// <summary> + /// Holds the paths to the game files in the event this is a multipart game + /// </summary> + public string[] MultiPartGameFiles { get; set; } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + var id = this.GetProviderId(MetadataProviders.Gamesdb); + + if (!string.IsNullOrEmpty(id)) + { + list.Insert(0, "Game-Gamesdb-" + id); + } + return list; + } + + public override IEnumerable<FileSystemMetadata> GetDeletePaths() + { + if (!IsInMixedFolder) + { + return new[] { + new FileSystemMetadata + { + FullName = FileSystem.GetDirectoryName(Path), + IsDirectory = true + } + }; + } + + return base.GetDeletePaths(); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Game; + } + + 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 new file mode 100644 index 000000000..63493ad4a --- /dev/null +++ b/MediaBrowser.Controller/Entities/GameGenre.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + public class GameGenre : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override bool CanDelete() + { + return false; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.GenreIds = new[] { Id }; + query.IncludeItemTypes = new[] { typeof(Game).Name }; + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GameGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/GameSystem.cs b/MediaBrowser.Controller/Entities/GameSystem.cs new file mode 100644 index 000000000..fb60ce83a --- /dev/null +++ b/MediaBrowser.Controller/Entities/GameSystem.cs @@ -0,0 +1,101 @@ +using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Users; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class GameSystem + /// </summary> + public class GameSystem : Folder, IHasLookupInfo<GameSystemInfo> + { + /// <summary> + /// Return the id that should be used to key display prefs for this item. + /// Default is based on the type for everything except actual generic folders. + /// </summary> + /// <value>The display prefs id.</value> + [IgnoreDataMember] + public override Guid DisplayPreferencesId + { + get + { + return Id; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 16; + value /= 9; + + return value; + } + + /// <summary> + /// Gets or sets the game system. + /// </summary> + /// <value>The game system.</value> + public string GameSystemName { get; set; } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (!string.IsNullOrEmpty(GameSystemName)) + { + list.Insert(0, "GameSystem-" + GameSystemName); + } + return list; + } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + // Don't block. Determine by game + return false; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Game; + } + + public GameSystemInfo GetLookupInfo() + { + var id = GetItemLookupInfo<GameSystemInfo>(); + + id.Path = Path; + + return id; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs new file mode 100644 index 000000000..94a5984df --- /dev/null +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -0,0 +1,140 @@ +using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Entities.Audio; +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Genre + /// </summary> + public class Genre : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override bool CanDelete() + { + return false; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.GenreIds = new[] { Id }; + query.ExcludeItemTypes = new[] { typeof(Game).Name, typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs new file mode 100644 index 000000000..b61e7b339 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is just a marker interface to denote top level folders + /// </summary> + public interface ICollectionFolder : IHasCollectionType + { + string Path { get; } + string Name { get; } + Guid Id { get; } + string[] PhysicalLocations { get; } + } + + public interface ISupportsUserSpecificView + { + bool EnableUserSpecificView { get; } + } + + public interface IHasCollectionType + { + string CollectionType { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs new file mode 100644 index 000000000..5aecf4eac --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasAspectRatio + /// </summary> + public interface IHasAspectRatio + { + /// <summary> + /// Gets or sets the aspect ratio. + /// </summary> + /// <value>The aspect ratio.</value> + string AspectRatio { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs new file mode 100644 index 000000000..5e1ae2179 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -0,0 +1,15 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasDisplayOrder + /// </summary> + public interface IHasDisplayOrder + { + /// <summary> + /// Gets or sets the display order. + /// </summary> + /// <value>The display order.</value> + string DisplayOrder { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs new file mode 100644 index 000000000..a13c95942 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Model.Dto; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasMediaSources + { + /// <summary> + /// Gets the media sources. + /// </summary> + List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); + List<MediaStream> GetMediaStreams(); + Guid Id { get; set; } + long? RunTimeTicks { get; set; } + string Path { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs new file mode 100644 index 000000000..0bc9ff81e --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasProgramAttributes + { + bool IsMovie { get; set; } + bool IsSports { get; } + bool IsNews { get; } + bool IsKids { get; } + bool IsRepeat { get; set; } + bool IsSeries { get; set; } + ProgramAudio? Audio { get; set; } + string EpisodeTitle { get; set; } + string ServiceName { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs new file mode 100644 index 000000000..2fd402bc2 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasScreenshots + /// </summary> + public interface IHasScreenshots + { + } +} diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs new file mode 100644 index 000000000..18d66452a --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -0,0 +1,20 @@ + +using System; + +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; set; } + string FindSeriesName(); + string FindSeriesSortName(); + Guid SeriesId { get; set; } + Guid FindSeriesId(); + string SeriesPresentationUniqueKey { get; set; } + string FindSeriesPresentationUniqueKey(); + } +} diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs new file mode 100644 index 000000000..f4905b7dc --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -0,0 +1,13 @@ +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasSpecialFeatures + { + /// <summary> + /// Gets or sets the special feature ids. + /// </summary> + /// <value>The special feature ids.</value> + Guid[] SpecialFeatureIds { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasStartDate.cs b/MediaBrowser.Controller/Entities/IHasStartDate.cs new file mode 100644 index 000000000..a6714fb96 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasStartDate.cs @@ -0,0 +1,9 @@ +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasStartDate + { + DateTime StartDate { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs new file mode 100644 index 000000000..8e7c4e007 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -0,0 +1,39 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasTrailers : IHasProviderIds + { + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets or sets the local trailer ids. + /// </summary> + /// <value>The local trailer ids.</value> + Guid[] LocalTrailerIds { get; set; } + Guid[] RemoteTrailerIds { get; set; } + Guid Id { get; set; } + } + + public static class HasTrailerExtensions + { + /// <summary> + /// Gets the trailer ids. + /// </summary> + /// <returns>List<Guid>.</returns> + public static List<Guid> GetTrailerIds(this IHasTrailers item) + { + var list = item.LocalTrailerIds.ToList(); + list.AddRange(item.RemoteTrailerIds); + return list; + } + + } +} diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs new file mode 100644 index 000000000..d21c6ae4d --- /dev/null +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Marker interface + /// </summary> + public interface IItemByName + { + IList<BaseItem> GetTaggedItems(InternalItemsQuery query); + } + + public interface IHasDualAccess : IItemByName + { + bool IsAccessedByName { 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/ISupportsBoxSetGrouping.cs b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs new file mode 100644 index 000000000..fbe5a06d0 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs @@ -0,0 +1,12 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Marker interface to denote a class that supports being hidden underneath it's boxset. + /// Just about anything can be placed into a boxset, + /// but movies should also only appear underneath and not outside separately (subject to configuration). + /// </summary> + public interface ISupportsBoxSetGrouping + { + } +} diff --git a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs new file mode 100644 index 000000000..2507c8ee6 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs @@ -0,0 +1,12 @@ + +namespace MediaBrowser.Controller.Entities +{ + public interface ISupportsPlaceHolders + { + /// <summary> + /// Gets a value indicating whether this instance is place holder. + /// </summary> + /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value> + bool IsPlaceHolder { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs new file mode 100644 index 000000000..ff57c2471 --- /dev/null +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -0,0 +1,261 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Configuration; +using System.Linq; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Entities +{ + public class InternalItemsQuery + { + public bool Recursive { get; set; } + + public int? StartIndex { get; set; } + + public int? Limit { get; set; } + + public User User { get; set; } + + public BaseItem SimilarTo { get; set; } + + public bool? IsFolder { get; set; } + public bool? IsFavorite { get; set; } + public bool? IsFavoriteOrLiked { get; set; } + public bool? IsLiked { get; set; } + public bool? IsPlayed { get; set; } + public bool? IsResumable { get; set; } + public bool? IncludeItemsByName { get; set; } + + public string[] MediaTypes { get; set; } + public string[] IncludeItemTypes { get; set; } + public string[] ExcludeItemTypes { get; set; } + public string[] ExcludeTags { get; set; } + public string[] ExcludeInheritedTags { get; set; } + public string[] Genres { get; set; } + + public bool? IsSpecialSeason { get; set; } + public bool? IsMissing { get; set; } + public bool? IsUnaired { get; set; } + public bool? CollapseBoxSetItems { get; set; } + + public string NameStartsWithOrGreater { get; set; } + public string NameStartsWith { get; set; } + public string NameLessThan { get; set; } + public string NameContains { get; set; } + public string MinSortName { get; set; } + + public string PresentationUniqueKey { get; set; } + public string Path { get; set; } + public string PathNotStartsWith { get; set; } + public string Name { get; set; } + + public string Person { get; set; } + public Guid[] PersonIds { get; set; } + public Guid[] ItemIds { get; set; } + public Guid[] ExcludeItemIds { get; set; } + public string AdjacentTo { get; set; } + public string[] PersonTypes { get; set; } + + public bool? Is3D { get; set; } + public bool? IsHD { get; set; } + public bool? IsLocked { get; set; } + public bool? IsPlaceHolder { get; set; } + + public bool? HasImdbId { get; set; } + public bool? HasOverview { get; set; } + public bool? HasTmdbId { get; set; } + public bool? HasOfficialRating { get; set; } + public bool? HasTvdbId { get; set; } + public bool? HasThemeSong { get; set; } + public bool? HasThemeVideo { get; set; } + public bool? HasSubtitles { get; set; } + public bool? HasSpecialFeature { get; set; } + public bool? HasTrailer { get; set; } + public bool? HasParentalRating { get; set; } + + public Guid[] StudioIds { get; set; } + public Guid[] GenreIds { get; set; } + public ImageType[] ImageTypes { get; set; } + public VideoType[] VideoTypes { get; set; } + public UnratedItem[] BlockUnratedItems { get; set; } + public int[] Years { get; set; } + public string[] Tags { get; set; } + public string[] OfficialRatings { get; set; } + + public DateTime? MinPremiereDate { get; set; } + public DateTime? MaxPremiereDate { get; set; } + public DateTime? MinStartDate { get; set; } + public DateTime? MaxStartDate { get; set; } + public DateTime? MinEndDate { get; set; } + public DateTime? MaxEndDate { get; set; } + public bool? IsAiring { get; set; } + + public bool? IsMovie { get; set; } + public bool? IsSports { get; set; } + public bool? IsKids { get; set; } + public bool? IsNews { get; set; } + public bool? IsSeries { get; set; } + + public int? MinPlayers { get; set; } + public int? MaxPlayers { get; set; } + public int? MinIndexNumber { get; set; } + public int? AiredDuringSeason { get; set; } + public double? MinCriticRating { get; set; } + public double? MinCommunityRating { get; set; } + + public Guid[] ChannelIds { get; set; } + + public int? ParentIndexNumber { get; set; } + public int? ParentIndexNumberNotEquals { get; set; } + public int? IndexNumber { get; set; } + public int? MinParentalRating { get; set; } + public int? MaxParentalRating { get; set; } + + public bool? HasDeadParentId { get; set; } + public bool? IsVirtualItem { get; set; } + + public Guid ParentId { get; set; } + public string ParentType { get; set; } + public Guid[] AncestorIds { get; set; } + public Guid[] TopParentIds { get; set; } + + public BaseItem Parent + { + set + { + if (value == null) + { + ParentId = Guid.Empty; + ParentType = null; + } + else + { + ParentId = value.Id; + ParentType = value.GetType().Name; + } + } + } + + public string[] PresetViews { get; set; } + public TrailerType[] TrailerTypes { get; set; } + public SourceType[] SourceTypes { get; set; } + + public SeriesStatus[] SeriesStatuses { get; set; } + public string ExternalSeriesId { get; set; } + public string ExternalId { get; set; } + + public Guid[] AlbumIds { get; set; } + public Guid[] ArtistIds { get; set; } + public Guid[] ExcludeArtistIds { get; set; } + public string AncestorWithPresentationUniqueKey { get; set; } + public string SeriesPresentationUniqueKey { get; set; } + + public bool GroupByPresentationUniqueKey { get; set; } + public bool GroupBySeriesPresentationUniqueKey { get; set; } + public bool EnableTotalRecordCount { get; set; } + public bool ForceDirect { get; set; } + public Dictionary<string, string> ExcludeProviderIds { get; set; } + public bool EnableGroupByMetadataKey { get; set; } + public bool? HasChapterImages { get; set; } + + // why tuple vs value tuple? + //public Tuple<string, SortOrder>[] OrderBy { get; set; } + public ValueTuple<string, SortOrder>[] OrderBy { get; set; } + + public DateTime? MinDateCreated { get; set; } + public DateTime? MinDateLastSaved { get; set; } + public DateTime? MinDateLastSavedForUser { get; set; } + + public DtoOptions DtoOptions { get; set; } + public int MinSimilarityScore { get; set; } + public string HasNoAudioTrackWithLanguage { get; set; } + public string HasNoInternalSubtitleTrackWithLanguage { get; set; } + public string HasNoExternalSubtitleTrackWithLanguage { get; set; } + public string HasNoSubtitleTrackWithLanguage { get; set; } + public bool? IsDeadArtist { get; set; } + public bool? IsDeadStudio { get; set; } + public bool? IsDeadPerson { get; set; } + + public InternalItemsQuery() + { + AlbumArtistIds = new Guid[] {}; + AlbumIds = new Guid[] {}; + AncestorIds = new Guid[] {}; + ArtistIds = new Guid[] {}; + BlockUnratedItems = new UnratedItem[] { }; + BoxSetLibraryFolders = new Guid[] {}; + ChannelIds = new Guid[] {}; + ContributingArtistIds = new Guid[] {}; + DtoOptions = new DtoOptions(); + EnableTotalRecordCount = true; + ExcludeArtistIds = new Guid[] {}; + ExcludeInheritedTags = new string[] {}; + ExcludeItemIds = new Guid[] {}; + ExcludeItemTypes = new string[] {}; + ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ExcludeTags = new string[] {}; + GenreIds = new Guid[] {}; + Genres = new string[] {}; + GroupByPresentationUniqueKey = true; + HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ImageTypes = new ImageType[] { }; + IncludeItemTypes = new string[] {}; + ItemIds = new Guid[] {}; + MediaTypes = new string[] {}; + MinSimilarityScore = 20; + OfficialRatings = new string[] {}; + OrderBy = Array.Empty<ValueTuple<string, SortOrder>>(); + PersonIds = new Guid[] {}; + PersonTypes = new string[] {}; + PresetViews = new string[] {}; + SeriesStatuses = new SeriesStatus[] { }; + SourceTypes = new SourceType[] { }; + StudioIds = new Guid[] {}; + Tags = new string[] {}; + TopParentIds = new Guid[] {}; + TrailerTypes = new TrailerType[] { }; + VideoTypes = new VideoType[] { }; + Years = new int[] { }; + } + + public InternalItemsQuery(User user) + : this() + { + SetUser(user); + } + + public void SetUser(User user) + { + if (user != null) + { + var policy = user.Policy; + MaxParentalRating = policy.MaxParentalRating; + + if (policy.MaxParentalRating.HasValue) + { + BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray(); + } + + ExcludeInheritedTags = policy.BlockedTags; + + User = user; + } + } + + public Dictionary<string, string> HasAnyProviderId { get; set; } + public Guid[] AlbumArtistIds { get; set; } + public Guid[] BoxSetLibraryFolders { get; set; } + public Guid[] ContributingArtistIds { get; set; } + public bool? HasAired { get; set; } + public bool? HasOwnerId { get; set; } + public bool? Is4K { get; set; } + public int? MaxHeight { get; set; } + public int? MaxWidth { get; set; } + public int? MinHeight { get; set; } + public int? MinWidth { get; set; } + public string SearchTerm { get; set; } + public string SeriesTimerId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs new file mode 100644 index 000000000..7e00834e3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + public class InternalPeopleQuery + { + public Guid ItemId { get; set; } + public string[] PersonTypes { get; set; } + public string[] ExcludePersonTypes { get; set; } + public int? MaxListOrder { get; set; } + public Guid AppearsInItemId { get; set; } + public string NameContains { get; set; } + + public InternalPeopleQuery() + { + PersonTypes = new string[] { }; + ExcludePersonTypes = new string[] { }; + } + } +} diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs new file mode 100644 index 000000000..bd0011c4b --- /dev/null +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Model.Entities; +using System; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class ItemImageInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public ImageType Type { get; set; } + + /// <summary> + /// Gets or sets the date modified. + /// </summary> + /// <value>The date modified.</value> + public DateTime DateModified { get; set; } + + public int Width { get; set; } + public int Height { get; set; } + + [IgnoreDataMember] + public bool IsLocalFile + { + get + { + if (Path != null) + { + if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + return true; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs new file mode 100644 index 000000000..363a3d6fd --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class LinkedChild + { + public string Path { get; set; } + public LinkedChildType Type { get; set; } + public string LibraryItemId { get; set; } + + [IgnoreDataMember] + public string Id { get; set; } + + /// <summary> + /// Serves as a cache + /// </summary> + public Guid? ItemId { get; set; } + + public static LinkedChild Create(BaseItem item) + { + var child = new LinkedChild + { + Path = item.Path, + Type = LinkedChildType.Manual + }; + + if (string.IsNullOrEmpty(child.Path)) + { + child.LibraryItemId = item.Id.ToString("N"); + } + + return child; + } + + public LinkedChild() + { + Id = Guid.NewGuid().ToString("N"); + } + } + + public enum LinkedChildType + { + Manual = 0, + Shortcut = 1 + } + + public class LinkedChildComparer : IEqualityComparer<LinkedChild> + { + private readonly IFileSystem _fileSystem; + + public LinkedChildComparer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public bool Equals(LinkedChild x, LinkedChild y) + { + if (x.Type == y.Type) + { + return _fileSystem.AreEqual(x.Path, y.Path); + } + return false; + } + + public int GetHashCode(LinkedChild obj) + { + return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs new file mode 100644 index 000000000..5918bf981 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -0,0 +1,263 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities.Movies +{ + /// <summary> + /// Class BoxSet + /// </summary> + public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo> + { + public BoxSet() + { + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] { }; + RemoteTrailerIds = new Guid[] { }; + + DisplayOrder = ItemSortBy.PremiereDate; + } + + [IgnoreDataMember] + protected override bool FilterLinkedChildrenPerUser + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets or sets the display order. + /// </summary> + /// <value>The display order.</value> + public string DisplayOrder { get; set; } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Movie); + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Movie; + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + if (IsLegacyBoxSet) + { + return base.GetNonCachedChildren(directoryService); + } + return new List<BaseItem>(); + } + + protected override List<BaseItem> LoadChildren() + { + if (IsLegacyBoxSet) + { + return base.LoadChildren(); + } + + // Save a trip to the database + return new List<BaseItem>(); + } + + [IgnoreDataMember] + private bool IsLegacyBoxSet + { + get + { + if (string.IsNullOrEmpty(Path)) + { + return false; + } + + if (LinkedChildren.Length > 0) + { + return false; + } + + return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); + } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) + { + return true; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + var children = base.GetChildren(user, includeLinkedChildren, query); + + if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)) + { + // Sort by name + return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + { + // Sort by release date + return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + // Default sorting + return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + var children = base.GetRecursiveChildren(user, query); + + if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + { + // Sort by release date + return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + return children; + } + + public BoxSetInfo GetLookupInfo() + { + return GetItemLookupInfo<BoxSetInfo>(); + } + + public override bool IsVisible(User user) + { + if (IsLegacyBoxSet) + { + return base.IsVisible(user); + } + + if (base.IsVisible(user)) + { + if (LinkedChildren.Length == 0) + { + return true; + } + + var userLibraryFolderIds = GetLibraryFolderIds(user); + var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds(); + + if (libraryFolderIds.Length == 0) + { + return true; + } + + return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i)); + } + + return false; + } + + public override bool IsVisibleStandalone(User user) + { + if (IsLegacyBoxSet) + { + return base.IsVisibleStandalone(user); + } + + return IsVisible(user); + } + + public Guid[] LibraryFolderIds { get; set; } + + private Guid[] GetLibraryFolderIds(User user) + { + return LibraryManager.GetUserRootFolder().GetChildren(user, true) + .Select(i => i.Id) + .ToArray(); + } + + public Guid[] GetLibraryFolderIds() + { + var expandedFolders = new List<Guid>() { }; + + return FlattenItems(this, expandedFolders) + .SelectMany(i => LibraryManager.GetCollectionFolders(i)) + .Select(i => i.Id) + .Distinct() + .ToArray(); + } + + private IEnumerable<BaseItem> FlattenItems(IEnumerable<BaseItem> items, List<Guid> expandedFolders) + { + return items + .SelectMany(i => FlattenItems(i, expandedFolders)); + } + + private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders) + { + var boxset = item as BoxSet; + if (boxset != null) + { + if (!expandedFolders.Contains(item.Id)) + { + expandedFolders.Add(item.Id); + + return FlattenItems(boxset.GetLinkedChildren(), expandedFolders); + } + + return new BaseItem[] { }; + } + + return new[] { item }; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs new file mode 100644 index 000000000..878b1b860 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -0,0 +1,203 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.Movies +{ + /// <summary> + /// Class Movie + /// </summary> + public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping + { + public Guid[] SpecialFeatureIds { get; set; } + + public Movie() + { + SpecialFeatureIds = new Guid[] {}; + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets or sets the name of the TMDB collection. + /// </summary> + /// <value>The name of the TMDB collection.</value> + public string TmdbCollectionName { get; set; } + + [IgnoreDataMember] + public string CollectionName + { + get { return TmdbCollectionName; } + set { TmdbCollectionName = value; } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + // hack for tv plugins + if (SourceType == SourceType.Channel) + { + return 0; + } + + double value = 2; + value /= 3; + + return value; + } + + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + 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 + if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder) + { + var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + if (specialFeaturesChanged) + { + hasChanges = true; + } + } + + return hasChanges; + } + + private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList(); + var newItemIds = newItems.Select(i => i.Id).ToArray(); + + var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds); + + var ownerId = Id; + + var tasks = newItems.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (i.OwnerId != ownerId) + { + i.OwnerId = ownerId; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + SpecialFeatureIds = newItemIds; + + return itemsChanged; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Movie; + } + + public MovieInfo GetLookupInfo() + { + var info = GetItemLookupInfo<MovieInfo>(); + + if (!IsInMixedFolder) + { + var name = System.IO.Path.GetFileName(ContainingFolderPath); + + if (VideoType == VideoType.VideoFile || VideoType == VideoType.Iso) + { + if (string.Equals(name, System.IO.Path.GetFileName(Path), StringComparison.OrdinalIgnoreCase)) + { + // if the folder has the file extension, strip it + name = System.IO.Path.GetFileNameWithoutExtension(name); + } + } + + info.Name = name; + } + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + else + { + // Try to get the year from the folder name + if (!IsInMixedFolder) + { + info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); + + yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + } + } + + return hasChanges; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + }); + } + + return list; + } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs new file mode 100644 index 000000000..78f9d0671 --- /dev/null +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -0,0 +1,79 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo> + { + [IgnoreDataMember] + public string[] Artists { get; set; } + + public MusicVideo() + { + Artists = new string[] {}; + } + + [IgnoreDataMember] + public string[] AllArtists + { + get + { + return Artists; + } + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + + public MusicVideoInfo GetLookupInfo() + { + var info = GetItemLookupInfo<MusicVideoInfo>(); + + info.Artists = Artists; + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + else + { + // Try to get the year from the folder name + if (!IsInMixedFolder) + { + info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); + + yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs new file mode 100644 index 000000000..9f85b2aea --- /dev/null +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -0,0 +1,119 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Entities +{ + public static class PeopleHelper + { + public static void AddPerson(List<PersonInfo> people, PersonInfo person) + { + if (person == null) + { + throw new ArgumentNullException("person"); + } + + if (string.IsNullOrEmpty(person.Name)) + { + throw new ArgumentNullException(); + } + + // Normalize + if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.GuestStar; + } + else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.Director; + } + else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.Producer; + } + else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.Writer; + } + + // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes + if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + { + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase)); + + if (existing != null) + { + existing.Type = PersonType.GuestStar; + MergeExisting(existing, person); + return; + } + } + + if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) + { + // If the actor already exists without a role and we have one, fill it in + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))); + if (existing == null) + { + // Wasn't there - add it + people.Add(person); + } + else + { + // Was there, if no role and we have one - fill it in + if (string.IsNullOrEmpty(existing.Role) && !string.IsNullOrEmpty(person.Role)) + { + existing.Role = person.Role; + } + + MergeExisting(existing, person); + } + } + else + { + var existing = people.FirstOrDefault(p => + string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) && + string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase)); + + // Check for dupes based on the combination of Name and Type + if (existing == null) + { + people.Add(person); + } + else + { + MergeExisting(existing, person); + } + } + } + + private static void MergeExisting(PersonInfo existing, PersonInfo person) + { + existing.SortOrder = person.SortOrder ?? existing.SortOrder; + existing.ImageUrl = person.ImageUrl ?? existing.ImageUrl; + + foreach (var id in person.ProviderIds) + { + existing.SetProviderId(id.Key, id.Value); + } + } + + public static bool ContainsPerson(List<PersonInfo> people, string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + foreach (var i in people) + { + if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs new file mode 100644 index 000000000..64d775094 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -0,0 +1,216 @@ +using MediaBrowser.Controller.Providers; +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Serialization; + +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, IHasLookupInfo<PersonLookupInfo> + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + public PersonLookupInfo GetLookupInfo() + { + return GetItemLookupInfo<PersonLookupInfo>(); + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.PersonIds = new[] { Id }; + + return LibraryManager.GetItemList(query); + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + [IgnoreDataMember] + public override bool EnableAlphaNumericSorting + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validFilename = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + string subFolderPrefix = null; + + foreach (char c in validFilename) + { + if (char.IsLetterOrDigit(c)) + { + subFolderPrefix = c.ToString(); + break; + } + } + + var path = ConfigurationManager.ApplicationPaths.PeoplePath; + + return string.IsNullOrEmpty(subFolderPrefix) ? + System.IO.Path.Combine(path, validFilename) : + System.IO.Path.Combine(path, subFolderPrefix, validFilename); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } + + /// <summary> + /// This is the small Person stub that is attached to BaseItems + /// </summary> + public class PersonInfo : IHasProviderIds + { + public PersonInfo() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets the role. + /// </summary> + /// <value>The role.</value> + public string Role { get; set; } + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the sort order - ascending + /// </summary> + /// <value>The sort order.</value> + public int? SortOrder { get; set; } + + public string ImageUrl { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + /// <summary> + /// Returns a <see cref="System.String" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="System.String" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public bool IsType(string type) + { + return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs new file mode 100644 index 000000000..01c10831d --- /dev/null +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -0,0 +1,104 @@ +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class Photo : BaseItem + { + [IgnoreDataMember] + public override bool SupportsLocalMetadata + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Photo; + } + } + + [IgnoreDataMember] + public override Folder LatestItemsIndexContainer + { + get + { + return AlbumEntity; + } + } + + + [IgnoreDataMember] + public PhotoAlbum AlbumEntity + { + get + { + var parents = GetParents(); + foreach (var parent in parents) + { + var photoAlbum = parent as PhotoAlbum; + if (photoAlbum != null) + { + return photoAlbum; + } + } + return null; + } + } + + public override bool CanDownload() + { + return true; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + if (Width.HasValue && Height.HasValue) + { + double width = Width.Value; + double height = Height.Value; + + if (Orientation.HasValue) + { + switch (Orientation.Value) + { + case ImageOrientation.LeftBottom: + case ImageOrientation.LeftTop: + case ImageOrientation.RightBottom: + case ImageOrientation.RightTop: + var temp = height; + height = width; + width = temp; + break; + } + } + + width /= Height.Value; + return width; + } + + return base.GetDefaultPrimaryImageAspectRatio(); + } + + public int? Width { get; set; } + public int? Height { get; set; } + public string CameraMake { get; set; } + public string CameraModel { get; set; } + public string Software { get; set; } + public double? ExposureTime { get; set; } + public double? FocalLength { get; set; } + public ImageOrientation? Orientation { get; set; } + public double? Aperture { get; set; } + public double? ShutterSpeed { get; set; } + + public double? Latitude { get; set; } + public double? Longitude { get; set; } + public double? Altitude { get; set; } + public int? IsoSpeedRating { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs new file mode 100644 index 000000000..af9d8c801 --- /dev/null +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class PhotoAlbum : Folder + { + [IgnoreDataMember] + public override bool AlwaysScanInternalMetadataPath + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs new file mode 100644 index 000000000..4ea0b1ea6 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasShares + { + Share[] Shares { get; set; } + } + + public class Share + { + public string UserId { get; set; } + public bool CanEdit { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/SourceType.cs b/MediaBrowser.Controller/Entities/SourceType.cs new file mode 100644 index 000000000..9c307b4e6 --- /dev/null +++ b/MediaBrowser.Controller/Entities/SourceType.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Entities +{ + public enum SourceType + { + Library = 0, + Channel = 1, + LiveTV = 2 + } +} diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs new file mode 100644 index 000000000..29f617539 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Studio + /// </summary> + public class Studio : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 16; + value /= 9; + + return value; + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.StudioIds = new[] { Id }; + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs new file mode 100644 index 000000000..201579731 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -0,0 +1,374 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.TV +{ + /// <summary> + /// Class Episode + /// </summary> + public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries + { + public Episode() + { + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets the season in which it aired. + /// </summary> + /// <value>The aired season.</value> + public int? AirsBeforeSeasonNumber { get; set; } + public int? AirsAfterSeasonNumber { get; set; } + public int? AirsBeforeEpisodeNumber { get; set; } + + /// <summary> + /// This is the ending episode number for double episodes. + /// </summary> + /// <value>The index number.</value> + public int? IndexNumberEnd { get; set; } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesName : series.SortName; + } + + [IgnoreDataMember] + protected override bool SupportsOwnedItems + { + get + { + return IsStacked || MediaSourceCount > 1; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + [IgnoreDataMember] + public int? AiredSeasonNumber + { + get + { + return AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber; + } + } + + [IgnoreDataMember] + public override Folder LatestItemsIndexContainer + { + get + { + return Series; + } + } + + [IgnoreDataMember] + public override Guid DisplayParentId + { + get + { + return SeasonId; + } + } + + [IgnoreDataMember] + protected override bool EnableDefaultVideoUserDataKeys + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + // hack for tv plugins + if (SourceType == SourceType.Channel) + { + return 0; + } + + double value = 16; + value /= 9; + + return value; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var series = Series; + if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) + { + var seriesUserDataKeys = series.GetUserDataKeys(); + var take = seriesUserDataKeys.Count; + if (seriesUserDataKeys.Count > 1) + { + take--; + } + list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"))); + } + + return list; + } + + /// <summary> + /// This Episode's Series Instance + /// </summary> + /// <value>The series.</value> + [IgnoreDataMember] + public Series Series + { + get + { + var seriesId = SeriesId; + if (seriesId.Equals(Guid.Empty)) { + seriesId = FindSeriesId(); + } + return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; + } + } + + [IgnoreDataMember] + public Season Season + { + get + { + var seasonId = SeasonId; + if (seasonId.Equals(Guid.Empty)) { + seasonId = FindSeasonId(); + } + return !seasonId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seasonId) as Season) : null; + } + } + + [IgnoreDataMember] + public bool IsInSeasonFolder + { + get + { + return FindParent<Season>() != null; + } + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + + [IgnoreDataMember] + public string SeriesName { get; set; } + + [IgnoreDataMember] + public string SeasonName { get; set; } + + public string FindSeriesPresentationUniqueKey() + { + var series = Series; + return series == null ? null : series.PresentationUniqueKey; + } + + public string FindSeasonName() + { + var season = Season; + + if (season == null) + { + if (ParentIndexNumber.HasValue) + { + return "Season " + ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture); + } + return "Season Unknown"; + } + + return season.Name; + } + + public string FindSeriesName() + { + var series = Series; + return series == null ? SeriesName : series.Name; + } + + public Guid FindSeasonId() + { + var season = FindParent<Season>(); + + // Episodes directly in series folder + if (season == null) + { + var series = Series; + + if (series != null && ParentIndexNumber.HasValue) + { + var findNumber = ParentIndexNumber.Value; + + season = series.Children + .OfType<Season>() + .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber); + } + } + + return season == null ? Guid.Empty : season.Id; + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected override string CreateSortName() + { + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "") + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + } + + /// <summary> + /// Determines whether [contains episode number] [the specified number]. + /// </summary> + /// <param name="number">The number.</param> + /// <returns><c>true</c> if [contains episode number] [the specified number]; otherwise, <c>false</c>.</returns> + public bool ContainsEpisodeNumber(int number) + { + if (IndexNumber.HasValue) + { + if (IndexNumberEnd.HasValue) + { + return number >= IndexNumber.Value && number <= IndexNumberEnd.Value; + } + + return IndexNumber.Value == number; + } + + return false; + } + + [IgnoreDataMember] + public override bool SupportsRemoteImageDownloading + { + get + { + if (IsMissingEpisode) + { + return false; + } + + return true; + } + } + + [IgnoreDataMember] + public bool IsMissingEpisode + { + get + { + return LocationType == LocationType.Virtual; + } + } + + [IgnoreDataMember] + public Guid SeasonId { get; set; } + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public Guid FindSeriesId() + { + var series = FindParent<Series>(); + return series == null ? Guid.Empty : series.Id; + } + + public override IEnumerable<Guid> GetAncestorIds() + { + var list = base.GetAncestorIds().ToList(); + + var seasonId = SeasonId; + + if (!seasonId.Equals(Guid.Empty) && !list.Contains(seasonId)) + { + list.Add(seasonId); + } + + return list; + } + + public override IEnumerable<FileSystemMetadata> GetDeletePaths() + { + return new[] { + new FileSystemMetadata + { + FullName = Path, + IsDirectory = IsFolder + } + }.Concat(GetLocalMetadataFilesToDelete()); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + + public EpisodeInfo GetLookupInfo() + { + var id = GetItemLookupInfo<EpisodeInfo>(); + + var series = Series; + + if (series != null) + { + id.SeriesProviderIds = series.ProviderIds; + id.SeriesDisplayOrder = series.DisplayOrder; + } + + id.IsMissingEpisode = IsMissingEpisode; + id.IndexNumberEnd = IndexNumberEnd; + + return id; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!IsLocked) + { + if (SourceType == SourceType.Library) + { + try + { + if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata)) + { + hasChanges = true; + } + } + catch (Exception ex) + { + Logger.ErrorException("Error in FillMissingEpisodeNumbersFromPath. Episode: {0}", ex, Path ?? Name ?? Id.ToString()); + } + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs new file mode 100644 index 000000000..b5f77df55 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -0,0 +1,273 @@ +using System; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Users; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.TV +{ + /// <summary> + /// Class Season + /// </summary> + public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> + { + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsDateLastMediaAdded + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + public override Guid DisplayParentId + { + get { return SeriesId; } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesName : series.SortName; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var series = Series; + if (series != null) + { + list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000"))); + } + + return list; + } + + public override int GetChildCount(User user) + { + var result = GetChildren(user, true).Count; + + return result; + } + + /// <summary> + /// This Episode's Series Instance + /// </summary> + /// <value>The series.</value> + [IgnoreDataMember] + public Series Series + { + get + { + var seriesId = SeriesId; + if (seriesId.Equals(Guid.Empty)) { + seriesId = FindSeriesId(); + } + return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; + } + } + + [IgnoreDataMember] + public string SeriesPath + { + get + { + var series = Series; + + if (series != null) + { + return series.Path; + } + + return FileSystem.GetDirectoryName(Path); + } + } + + public override string CreatePresentationUniqueKey() + { + if (IndexNumber.HasValue) + { + var series = Series; + if (series != null) + { + return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); + } + } + + return base.CreatePresentationUniqueKey(); + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected override string CreateSortName() + { + return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name; + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + if (query.User == null) + { + return base.GetItemsInternal(query); + } + + var user = query.User; + + Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + + var items = GetEpisodes(user, query.DtoOptions).Where(filter); + + return PostFilterAndSort(items, query, false); + } + + /// <summary> + /// Gets the episodes. + /// </summary> + public List<BaseItem> GetEpisodes(User user, DtoOptions options) + { + return GetEpisodes(Series, user, options); + } + + public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options) + { + return GetEpisodes(series, user, null, options); + } + + public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options) + { + return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options); + } + + public List<BaseItem> GetEpisodes() + { + return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true)); + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + return GetEpisodes(user, new DtoOptions(true)); + } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + // Don't block. Let either the entire series rating or episode rating determine it + return false; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + + [IgnoreDataMember] + public string SeriesName { get; set; } + + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public string FindSeriesPresentationUniqueKey() + { + var series = Series; + return series == null ? null : series.PresentationUniqueKey; + } + + public string FindSeriesName() + { + var series = Series; + return series == null ? SeriesName : series.Name; + } + + public Guid FindSeriesId() + { + var series = FindParent<Series>(); + return series == null ? Guid.Empty: series.Id; + } + + /// <summary> + /// Gets the lookup information. + /// </summary> + /// <returns>SeasonInfo.</returns> + public SeasonInfo GetLookupInfo() + { + var id = GetItemLookupInfo<SeasonInfo>(); + + var series = Series; + + if (series != null) + { + id.SeriesProviderIds = series.ProviderIds; + } + + return id; + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) + { + IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path); + + // If a change was made record it + if (IndexNumber.HasValue) + { + hasChanges = true; + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs new file mode 100644 index 000000000..88fde1760 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -0,0 +1,540 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.TV +{ + /// <summary> + /// Class Series + /// </summary> + public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer + { + public Series() + { + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + AirDays = new DayOfWeek[] { }; + } + + public DayOfWeek[] AirDays { get; set; } + public string AirTime { get; set; } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsDateLastMediaAdded + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// airdate, dvd or absolute + /// </summary> + public string DisplayOrder { get; set; } + + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public SeriesStatus? Status { get; set; } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public override string CreatePresentationUniqueKey() + { + if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping) + { + var userdatakeys = GetUserDataKeys(); + + if (userdatakeys.Count > 1) + { + return AddLibrariesToPresentationUniqueKey(userdatakeys[0]); + } + } + + return base.CreatePresentationUniqueKey(); + } + + private string AddLibrariesToPresentationUniqueKey(string key) + { + var lang = GetPreferredMetadataLanguage(); + if (!string.IsNullOrEmpty(lang)) + { + key += "-" + lang; + } + + var folders = LibraryManager.GetCollectionFolders(this) + .Select(i => i.Id.ToString("N")) + .ToArray(); + + if (folders.Length == 0) + { + return key; + } + + return key + "-" + string.Join("-", folders); + } + + private static string GetUniqueSeriesKey(BaseItem series) + { + return series.GetPresentationUniqueKey(); + } + + public override int GetChildCount(User user) + { + var seriesKey = GetUniqueSeriesKey(this); + + var result = LibraryManager.GetCount(new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] { typeof(Season).Name }, + IsVirtualItem = false, + Limit = 0, + DtoOptions = new Dto.DtoOptions(false) + { + EnableImages = false + } + }); + + return result; + } + + public override int GetRecursiveChildCount(User user) + { + var seriesKey = GetUniqueSeriesKey(this); + + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + DtoOptions = new Dto.DtoOptions(false) + { + EnableImages = false + } + }; + + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + } + query.IsVirtualItem = false; + query.Limit = 0; + var totalRecordCount = LibraryManager.GetCount(query); + + return totalRecordCount; + } + + /// <summary> + /// Gets the user data key. + /// </summary> + /// <returns>System.String.</returns> + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var key = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + key = this.GetProviderId(MetadataProviders.Tvdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + return list; + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + return GetSeasons(user, new DtoOptions(true)); + } + + public List<BaseItem> GetSeasons(User user, DtoOptions options) + { + var query = new InternalItemsQuery(user) + { + DtoOptions = options + }; + + SetSeasonQueryOptions(query, user); + + return LibraryManager.GetItemList(query); + } + + private void SetSeasonQueryOptions(InternalItemsQuery query, User user) + { + var seriesKey = GetUniqueSeriesKey(this); + + query.AncestorWithPresentationUniqueKey = null; + query.SeriesPresentationUniqueKey = seriesKey; + query.IncludeItemTypes = new[] { typeof(Season).Name }; + query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + + if (user != null) + { + var config = user.Configuration; + + if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + } + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + var user = query.User; + + if (query.Recursive) + { + var seriesKey = GetUniqueSeriesKey(this); + + query.AncestorWithPresentationUniqueKey = null; + query.SeriesPresentationUniqueKey = seriesKey; + if (query.OrderBy.Length == 0) + { + query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + } + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }; + } + query.IsVirtualItem = false; + return LibraryManager.GetItemsResult(query); + } + + SetSeasonQueryOptions(query, user); + + return LibraryManager.GetItemsResult(query); + } + + public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options) + { + var seriesKey = GetUniqueSeriesKey(this); + + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }, + OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + DtoOptions = options + }; + var config = user.Configuration; + if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + + var allItems = LibraryManager.GetItemList(query); + + var allSeriesEpisodes = allItems.OfType<Episode>().ToList(); + + var allEpisodes = allItems.OfType<Season>() + .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options)) + .Reverse(); + + // Specials could appear twice based on above - once in season 0, once in the aired season + // This depends on settings for that series + // When this happens, remove the duplicate from season 0 + + return allEpisodes.DistinctBy(i => i.Id).Reverse(); + } + + 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 = GetRecursiveChildren(); + + var totalItems = items.Count; + var numComplete = 0; + + // Refresh seasons + foreach (var item in items) + { + if (!(item is Season)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (refreshOptions.RefreshItem(item)) + { + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 100); + } + + // Refresh episodes and other children + foreach (var item in items) + { + if ((item is Season)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var skipItem = false; + + var episode = item as Episode; + + if (episode != null + && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh + && !refreshOptions.ReplaceAllMetadata + && episode.IsMissingEpisode + && episode.LocationType == LocationType.Virtual + && episode.PremiereDate.HasValue + && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30) + { + skipItem = true; + } + + if (!skipItem) + { + if (refreshOptions.RefreshItem(item)) + { + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + } + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 100); + } + + refreshOptions = new MetadataRefreshOptions(refreshOptions); + await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); + } + + public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options) + { + var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons; + + // add optimization when this setting is not enabled + var seriesKey = queryFromSeries ? + GetUniqueSeriesKey(this) : + GetUniqueSeriesKey(parentSeason); + + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey, + SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null, + IncludeItemTypes = new[] { typeof(Episode).Name }, + OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + DtoOptions = options + }; + if (user != null) + { + var config = user.Configuration; + if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + } + + var allItems = LibraryManager.GetItemList(query); + + return GetSeasonEpisodes(parentSeason, user, allItems, options); + } + + public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options) + { + if (allSeriesEpisodes == null) + { + return GetSeasonEpisodes(parentSeason, user, options); + } + + var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons); + + var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder; + + return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList(); + } + + /// <summary> + /// Filters the episodes by season. + /// </summary> + public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials) + { + var seasonNumber = parentSeason.IndexNumber; + var seasonPresentationKey = GetUniqueSeriesKey(parentSeason); + + var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0; + + return episodes.Where(episode => + { + var episodeItem = (Episode)episode; + + var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexNumber; + if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.Value) + { + return true; + } + + if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual) + { + return true; + } + + var season = episodeItem.Season; + return season != null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase); + }); + } + + /// <summary> + /// Filters the episodes by season. + /// </summary> + public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials) + { + if (!includeSpecials || seasonNumber < 1) + { + return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber); + } + + return episodes.Where(i => + { + var episode = i; + + if (episode != null) + { + var currentSeasonNumber = episode.AiredSeasonNumber; + + return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber; + } + + return false; + }); + } + + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Series); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + + public SeriesInfo GetLookupInfo() + { + var info = GetItemLookupInfo<SeriesInfo>(); + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + + return hasChanges; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/shows/{0}", imdbId) + }); + } + + return list; + } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs new file mode 100644 index 000000000..e5d8f35d9 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + public static class TagExtensions + { + public static void AddTag(this BaseItem item, string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + var current = item.Tags; + + if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + if (current.Length == 0) + { + item.Tags = new[] { name }; + } + else + { + var list = current.ToArray(current.Length + 1); + list[list.Length - 1] = name; + + item.Tags = list; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs new file mode 100644 index 000000000..4f2a5631b --- /dev/null +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -0,0 +1,111 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Trailer + /// </summary> + public class Trailer : Video, IHasLookupInfo<TrailerInfo> + { + public Trailer() + { + TrailerTypes = new TrailerType[] { }; + } + + public TrailerType[] TrailerTypes { get; set; } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Trailer; + } + + public TrailerInfo GetLookupInfo() + { + var info = GetItemLookupInfo<TrailerInfo>(); + + if (!IsInMixedFolder && IsFileProtocol) + { + info.Name = System.IO.Path.GetFileName(ContainingFolderPath); + } + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + else + { + // Try to get the year from the folder name + if (!IsInMixedFolder) + { + info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); + + yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + } + } + + return hasChanges; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + }); + } + + return list; + } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs new file mode 100644 index 000000000..f569c8021 --- /dev/null +++ b/MediaBrowser.Controller/Entities/User.cs @@ -0,0 +1,342 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Connect; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class User + /// </summary> + public class User : BaseItem + { + public static IUserManager UserManager { get; set; } + public static IXmlSerializer XmlSerializer { get; set; } + + /// <summary> + /// From now on all user paths will be Id-based. + /// This is for backwards compatibility. + /// </summary> + public bool UsesIdForConfigurationPath { get; set; } + + /// <summary> + /// Gets or sets the password. + /// </summary> + /// <value>The password.</value> + public string Password { get; set; } + public string EasyPassword { get; set; } + public string Salt { get; set; } + + public string ConnectUserName { get; set; } + public string ConnectUserId { get; set; } + public UserLinkType? ConnectLinkType { get; set; } + public string ConnectAccessKey { get; set; } + + // Strictly to remove IgnoreDataMember + public override ItemImageInfo[] ImageInfos + { + get + { + return base.ImageInfos; + } + set + { + base.ImageInfos = value; + } + } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + [IgnoreDataMember] + public override string Path + { + get + { + // Return this so that metadata providers will look in here + return ConfigurationDirectoryPath; + } + set + { + base.Path = value; + } + } + + private string _name; + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public override string Name + { + get + { + return _name; + } + set + { + _name = value; + + // lazy load this again + SortName = null; + } + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets the root folder. + /// </summary> + /// <value>The root folder.</value> + [IgnoreDataMember] + public Folder RootFolder + { + get + { + return LibraryManager.GetUserRootFolder(); + } + } + + /// <summary> + /// Gets or sets the last login date. + /// </summary> + /// <value>The last login date.</value> + public DateTime? LastLoginDate { get; set; } + /// <summary> + /// Gets or sets the last activity date. + /// </summary> + /// <value>The last activity date.</value> + public DateTime? LastActivityDate { get; set; } + + private volatile UserConfiguration _config; + private readonly object _configSyncLock = new object(); + [IgnoreDataMember] + public UserConfiguration Configuration + { + get + { + if (_config == null) + { + lock (_configSyncLock) + { + if (_config == null) + { + _config = UserManager.GetUserConfiguration(this); + } + } + } + + return _config; + } + set { _config = value; } + } + + private volatile UserPolicy _policy; + private readonly object _policySyncLock = new object(); + [IgnoreDataMember] + public UserPolicy Policy + { + get + { + if (_policy == null) + { + lock (_policySyncLock) + { + if (_policy == null) + { + _policy = UserManager.GetUserPolicy(this); + } + } + } + + return _policy; + } + set { _policy = value; } + } + + /// <summary> + /// Renames the user. + /// </summary> + /// <param name="newName">The new name.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public Task Rename(string newName) + { + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException("newName"); + } + + // If only the casing is changing, leave the file system alone + if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) + { + UsesIdForConfigurationPath = true; + + // Move configuration + var newConfigDirectory = GetConfigurationDirectoryPath(newName); + var oldConfigurationDirectory = ConfigurationDirectoryPath; + + // Exceptions will be thrown if these paths already exist + if (FileSystem.DirectoryExists(newConfigDirectory)) + { + FileSystem.DeleteDirectory(newConfigDirectory, true); + } + + if (FileSystem.DirectoryExists(oldConfigurationDirectory)) + { + FileSystem.MoveDirectory(oldConfigurationDirectory, newConfigDirectory); + } + else + { + FileSystem.CreateDirectory(newConfigDirectory); + } + } + + Name = newName; + + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + { + ReplaceAllMetadata = true, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = true + + }, CancellationToken.None); + } + + public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + UserManager.UpdateUser(this); + } + + /// <summary> + /// Gets the path to the user's configuration directory + /// </summary> + /// <value>The configuration directory path.</value> + [IgnoreDataMember] + public string ConfigurationDirectoryPath + { + get + { + return GetConfigurationDirectoryPath(Name); + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + /// <summary> + /// Gets the configuration directory path. + /// </summary> + /// <param name="username">The username.</param> + /// <returns>System.String.</returns> + private string GetConfigurationDirectoryPath(string username) + { + var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; + + // Legacy + if (!UsesIdForConfigurationPath) + { + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentNullException("username"); + } + + var safeFolderName = FileSystem.GetValidFilename(username); + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); + } + + return System.IO.Path.Combine(parentPath, Id.ToString("N")); + } + + public bool IsParentalScheduleAllowed() + { + return IsParentalScheduleAllowed(DateTime.UtcNow); + } + + public bool IsParentalScheduleAllowed(DateTime date) + { + var schedules = Policy.AccessSchedules; + + if (schedules.Length == 0) + { + return true; + } + + foreach (var i in schedules) + { + if (IsParentalScheduleAllowed(i, date)) + { + return true; + } + } + return false; + } + + private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) + { + if (date.Kind != DateTimeKind.Utc) + { + throw new ArgumentException("Utc date expected"); + } + + var localTime = date.ToLocalTime(); + + return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) && + IsWithinTime(schedule, localTime); + } + + private bool IsWithinTime(AccessSchedule schedule, DateTime localTime) + { + var hour = localTime.TimeOfDay.TotalHours; + + return hour >= schedule.StartHour && hour <= schedule.EndHour; + } + + public bool IsFolderGrouped(Guid id) + { + foreach (var i in Configuration.GroupedFolders) + { + if (new Guid(i) == id) + { + return true; + } + } + return false; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public long InternalId { get; set;} + + + } +} diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs new file mode 100644 index 000000000..0e1326949 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -0,0 +1,124 @@ +using System; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class UserItemData + /// </summary> + public class UserItemData + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the key. + /// </summary> + /// <value>The key.</value> + public string Key { get; set; } + + /// <summary> + /// The _rating + /// </summary> + private double? _rating; + /// <summary> + /// Gets or sets the users 0-10 rating + /// </summary> + /// <value>The rating.</value> + /// <exception cref="System.ArgumentOutOfRangeException">Rating;A 0 to 10 rating is required for UserItemData.</exception> + public double? Rating + { + get + { + return _rating; + } + set + { + if (value.HasValue) + { + if (value.Value < 0 || value.Value > 10) + { + throw new ArgumentOutOfRangeException("value", "A 0 to 10 rating is required for UserItemData."); + } + } + + _rating = value; + } + } + + /// <summary> + /// Gets or sets the playback position ticks. + /// </summary> + /// <value>The playback position ticks.</value> + public long PlaybackPositionTicks { get; set; } + + /// <summary> + /// Gets or sets the play count. + /// </summary> + /// <value>The play count.</value> + public int PlayCount { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is favorite. + /// </summary> + /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value> + public bool IsFavorite { get; set; } + + /// <summary> + /// Gets or sets the last played date. + /// </summary> + /// <value>The last played date.</value> + public DateTime? LastPlayedDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UserItemData" /> is played. + /// </summary> + /// <value><c>true</c> if played; otherwise, <c>false</c>.</value> + public bool Played { get; set; } + /// <summary> + /// Gets or sets the index of the audio stream. + /// </summary> + /// <value>The index of the audio stream.</value> + public int? AudioStreamIndex { get; set; } + /// <summary> + /// Gets or sets the index of the subtitle stream. + /// </summary> + /// <value>The index of the subtitle stream.</value> + public int? SubtitleStreamIndex { get; set; } + + public const double MinLikeValue = 6.5; + + /// <summary> + /// This is an interpreted property to indicate likes or dislikes + /// This should never be serialized. + /// </summary> + /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool? Likes + { + get + { + if (Rating != null) + { + return Rating >= MinLikeValue; + } + + return null; + } + set + { + if (value.HasValue) + { + Rating = value.Value ? 10 : 1; + } + else + { + Rating = null; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs new file mode 100644 index 000000000..f8e843d92 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -0,0 +1,158 @@ +using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Special class used for User Roots. Children contain actual ones defined for this user + /// PLUS the virtual folders from the physical root (added by plug-ins). + /// </summary> + public class UserRootFolder : Folder + { + private List<Guid> _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override List<BaseItem> LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null) + { + var list = base.LoadChildren(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + if (query.Recursive) + { + return QueryRecursive(query); + } + + var result = UserViewManager.GetUserViews(new UserViewQuery + { + UserId = query.User.Id, + PresetViews = query.PresetViews + }); + + var itemsArray = result; + var totalCount = itemsArray.Length; + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = itemsArray + }; + } + + public override int GetChildCount(User user) + { + return GetChildren(user, true).Count; + } + + [IgnoreDataMember] + protected override bool SupportsShortcutChildren + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList(); + list.AddRange(LibraryManager.RootFolder.VirtualChildren); + + return list; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + ClearCache(); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase)) + { + Name = "Media Folders"; + hasChanges = true; + } + + return hasChanges; + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + ClearCache(); + + return base.GetNonCachedChildren(directoryService); + } + + protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + ClearCache(); + + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + .ConfigureAwait(false); + + ClearCache(); + + // Not the best way to handle this, but it solves an issue + // CollectionFolders aren't always getting saved after changes + // This means that grabbing the item by Id may end up returning the old one + // Fix is in two places - make sure the folder gets saved + // And here to remedy it for affected users. + // In theory this can be removed eventually. + foreach (var item in Children) + { + LibraryManager.RegisterItem(item); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs new file mode 100644 index 000000000..984cad481 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -0,0 +1,210 @@ +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Collections; + +namespace MediaBrowser.Controller.Entities +{ + public class UserView : Folder, IHasCollectionType + { + public string ViewType { get; set; } + public Guid DisplayParentId { get; set; } + + public Guid? UserId { get; set; } + + public static ITVSeriesManager TVSeriesManager; + public static IPlaylistManager PlaylistManager; + + [IgnoreDataMember] + public string CollectionType + { + get + { + return ViewType; + } + } + + public override IEnumerable<Guid> GetIdsForAncestorQuery() + { + var list = new List<Guid>(); + + if (!DisplayParentId.Equals(Guid.Empty)) + { + list.Add(DisplayParentId); + } + else if (!ParentId.Equals(Guid.Empty)) + { + list.Add(ParentId); + } + else + { + list.Add(Id); + } + return list; + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + //public override double? GetDefaultPrimaryImageAspectRatio() + //{ + // double value = 16; + // value /= 9; + + // return value; + //} + + public override int GetChildCount(User user) + { + return GetChildren(user, true).Count; + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + var parent = this as Folder; + + if (!DisplayParentId.Equals(Guid.Empty)) + { + parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent; + } + else if (!ParentId.Equals(Guid.Empty)) + { + parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; + } + + return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager) + .GetUserItems(parent, this, CollectionType, query); + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + if (query == null) + { + query = new InternalItemsQuery(user); + } + + query.EnableTotalRecordCount = false; + var result = GetItemList(query); + + return result.ToList(); + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + query.SetUser(user); + query.Recursive = true; + query.EnableTotalRecordCount = false; + query.ForceDirect = true; + + return GetItemList(query); + } + + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + return GetChildren(user, false); + } + + private static string[] UserSpecificViewTypes = new string[] + { + MediaBrowser.Model.Entities.CollectionType.Playlists + }; + + public static bool IsUserSpecific(Folder folder) + { + var collectionFolder = folder as ICollectionFolder; + + if (collectionFolder == null) + { + return false; + } + + var supportsUserSpecific = folder as ISupportsUserSpecificView; + if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView) + { + return true; + } + + return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + public static bool IsEligibleForGrouping(Folder folder) + { + var collectionFolder = folder as ICollectionFolder; + return collectionFolder != null && IsEligibleForGrouping(collectionFolder.CollectionType); + } + + private static string[] ViewTypesEligibleForGrouping = new string[] + { + MediaBrowser.Model.Entities.CollectionType.Movies, + MediaBrowser.Model.Entities.CollectionType.TvShows, + string.Empty + }; + + public static bool IsEligibleForGrouping(string viewType) + { + return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + private static string[] OriginalFolderViewTypes = new string[] + { + MediaBrowser.Model.Entities.CollectionType.Games, + MediaBrowser.Model.Entities.CollectionType.Books, + MediaBrowser.Model.Entities.CollectionType.MusicVideos, + MediaBrowser.Model.Entities.CollectionType.HomeVideos, + MediaBrowser.Model.Entities.CollectionType.Photos, + MediaBrowser.Model.Entities.CollectionType.Music, + MediaBrowser.Model.Entities.CollectionType.BoxSets + }; + + public static bool EnableOriginalFolder(string viewType) + { + return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService) + { + return Task.FromResult(true); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs new file mode 100644 index 000000000..36035a2bb --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -0,0 +1,1070 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + public class UserViewBuilder + { + private readonly IUserViewManager _userViewManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IUserDataManager _userDataManager; + private readonly ITVSeriesManager _tvSeriesManager; + private readonly IServerConfigurationManager _config; + private readonly IPlaylistManager _playlistManager; + + public UserViewBuilder(IUserViewManager userViewManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager) + { + _userViewManager = userViewManager; + _libraryManager = libraryManager; + _logger = logger; + _userDataManager = userDataManager; + _tvSeriesManager = tvSeriesManager; + _config = config; + _playlistManager = playlistManager; + } + + public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) + { + var user = query.User; + + //if (query.IncludeItemTypes != null && + // query.IncludeItemTypes.Length == 1 && + // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) + //{ + // if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + // { + // return await FindPlaylists(queryParent, user, query).ConfigureAwait(false); + // } + //} + + switch (viewType) + { + case CollectionType.Folders: + return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query); + + case CollectionType.TvShows: + return GetTvView(queryParent, user, query); + + case CollectionType.Movies: + return GetMovieFolders(queryParent, user, query); + + case SpecialFolder.TvShowSeries: + return GetTvSeries(queryParent, user, query); + + case SpecialFolder.TvGenres: + return GetTvGenres(queryParent, user, query); + + case SpecialFolder.TvGenre: + return GetTvGenreItems(queryParent, displayParent, user, query); + + case SpecialFolder.TvResume: + return GetTvResume(queryParent, user, query); + + case SpecialFolder.TvNextUp: + return GetTvNextUp(queryParent, query); + + case SpecialFolder.TvLatest: + return GetTvLatest(queryParent, user, query); + + case SpecialFolder.MovieFavorites: + return GetFavoriteMovies(queryParent, user, query); + + case SpecialFolder.MovieLatest: + return GetMovieLatest(queryParent, user, query); + + case SpecialFolder.MovieGenres: + return GetMovieGenres(queryParent, user, query); + + case SpecialFolder.MovieGenre: + return GetMovieGenreItems(queryParent, displayParent, user, query); + + case SpecialFolder.MovieResume: + return GetMovieResume(queryParent, user, query); + + case SpecialFolder.MovieMovies: + return GetMovieMovies(queryParent, user, query); + + case SpecialFolder.MovieCollections: + return GetMovieCollections(queryParent, user, query); + + case SpecialFolder.TvFavoriteEpisodes: + return GetFavoriteEpisodes(queryParent, user, query); + + case SpecialFolder.TvFavoriteSeries: + return GetFavoriteSeries(queryParent, user, query); + + default: + { + if (queryParent is UserView) + { + return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query); + } + return queryParent.GetItems(query); + } + } + } + + private int GetSpecialItemsLimit() + { + return 50; + } + + private QueryResult<BaseItem> GetMovieFolders(Folder parent, User user, InternalItemsQuery query) + { + if (query.Recursive) + { + query.Recursive = true; + query.SetUser(user); + + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + } + + return parent.QueryRecursive(query); + } + + var list = new List<BaseItem>(); + + list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent)); + list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent)); + list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent)); + list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent)); + list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent)); + list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)); + + return GetResult(list, parent, query); + } + + private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetFavoriteSeries(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Series).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetFavoriteEpisodes(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetMovieMovies(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query) + { + query.Parent = null; + query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; + query.SetUser(user); + query.Recursive = true; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.IsResumable = true; + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> ConvertToResult(List<BaseItem> items) + { + var arr = items.ToArray(); + return new QueryResult<BaseItem> + { + Items = arr, + TotalRecordCount = arr.Length + }; + } + + private QueryResult<BaseItem> GetMovieGenres(Folder parent, User user, InternalItemsQuery query) + { + var genres = parent.QueryRecursive(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Movie).Name }, + Recursive = true, + EnableTotalRecordCount = false + + }).Items + .SelectMany(i => i.Genres) + .DistinctNames() + .Select(i => + { + try + { + return _libraryManager.GetGenre(i); + } + catch + { + // Full exception logged at lower levels + _logger.Error("Error getting genre"); + return null; + } + + }) + .Where(i => i != null) + .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent)); + + return GetResult(genres, parent, query); + } + + private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = queryParent; + query.GenreIds = new[] { displayParent.Id }; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetTvView(Folder parent, User user, InternalItemsQuery query) + { + if (query.Recursive) + { + query.Recursive = true; + query.SetUser(user); + + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name }; + } + + return parent.QueryRecursive(query); + } + + var list = new List<BaseItem>(); + + list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent)); + list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent)); + list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent)); + list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent)); + list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent)); + list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent)); + list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)); + + return GetResult(list, parent, query); + } + + private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IsVirtualItem = false; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query) + { + var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); + + var result = _tvSeriesManager.GetNextUp(new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + + }, parentFolders, query.DtoOptions); + + return result; + } + + private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.IsResumable = true; + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> GetTvSeries(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Series).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetTvGenres(Folder parent, User user, InternalItemsQuery query) + { + var genres = parent.QueryRecursive(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true, + EnableTotalRecordCount = false + + }).Items + .SelectMany(i => i.Genres) + .DistinctNames() + .Select(i => + { + try + { + return _libraryManager.GetGenre(i); + } + catch + { + // Full exception logged at lower levels + _logger.Error("Error getting genre"); + return null; + } + + }) + .Where(i => i != null) + .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent)); + + return GetResult(genres, parent, query); + } + + private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = queryParent; + query.GenreIds = new[] { displayParent.Id }; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Series).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetResult<T>(QueryResult<T> result) + where T : BaseItem + { + return new QueryResult<BaseItem> + { + Items = result.Items, + TotalRecordCount = result.TotalRecordCount + }; + } + + private QueryResult<BaseItem> GetResult<T>(IEnumerable<T> items, + BaseItem queryParent, + InternalItemsQuery query) + where T : BaseItem + { + items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager)); + + return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config); + } + + public static bool FilterItem(BaseItem item, InternalItemsQuery query) + { + return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager); + } + + public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, + BaseItem queryParent, + int? totalRecordLimit, + InternalItemsQuery query, + ILibraryManager libraryManager, + IServerConfigurationManager configurationManager) + { + var user = query.User; + + // This must be the last filter + if (!string.IsNullOrEmpty(query.AdjacentTo)) + { + items = FilterForAdjacency(items.ToList(), query.AdjacentTo); + } + + return SortAndPage(items, totalRecordLimit, query, libraryManager, true); + } + + public static QueryResult<BaseItem> SortAndPage(IEnumerable<BaseItem> items, + int? totalRecordLimit, + InternalItemsQuery query, + ILibraryManager libraryManager, bool enableSorting) + { + if (enableSorting) + { + if (query.OrderBy.Length > 0) + { + items = libraryManager.Sort(items, query.User, query.OrderBy); + } + } + + var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); + var totalCount = itemsArray.Length; + + if (query.Limit.HasValue) + { + itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray(); + } + else if (query.StartIndex.HasValue) + { + itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray(); + } + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = itemsArray + }; + } + + public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) + { + if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.IsVirtualItem.HasValue && item.IsVirtualItem != query.IsVirtualItem.Value) + { + return false; + } + + if (query.IsFolder.HasValue && query.IsFolder.Value != item.IsFolder) + { + return false; + } + + UserItemData userData = null; + + if (query.IsLiked.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + + if (!userData.Likes.HasValue || userData.Likes != query.IsLiked.Value) + { + return false; + } + } + + if (query.IsFavoriteOrLiked.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + var isFavoriteOrLiked = userData.IsFavorite || (userData.Likes ?? false); + + if (isFavoriteOrLiked != query.IsFavoriteOrLiked.Value) + { + return false; + } + } + + if (query.IsFavorite.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + + if (userData.IsFavorite != query.IsFavorite.Value) + { + return false; + } + } + + if (query.IsResumable.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + var isResumable = userData.PlaybackPositionTicks > 0; + + if (isResumable != query.IsResumable.Value) + { + return false; + } + } + + if (query.IsPlayed.HasValue) + { + if (item.IsPlayed(user) != query.IsPlayed.Value) + { + return false; + } + } + + // Filter by Video3DFormat + if (query.Is3D.HasValue) + { + var val = query.Is3D.Value; + var video = item as Video; + + if (video == null || val != video.Video3DFormat.HasValue) + { + return false; + } + } + + /* + * fuck - fix this + if (query.IsHD.HasValue) + { + if (item.IsHD != query.IsHD.Value) + { + return false; + } + } + */ + + if (query.IsLocked.HasValue) + { + var val = query.IsLocked.Value; + if (item.IsLocked != val) + { + return false; + } + } + + if (query.HasOverview.HasValue) + { + var filterValue = query.HasOverview.Value; + + var hasValue = !string.IsNullOrEmpty(item.Overview); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasImdbId.HasValue) + { + var filterValue = query.HasImdbId.Value; + + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Imdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasTmdbId.HasValue) + { + var filterValue = query.HasTmdbId.Value; + + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasTvdbId.HasValue) + { + var filterValue = query.HasTvdbId.Value; + + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasOfficialRating.HasValue) + { + var filterValue = query.HasOfficialRating.Value; + + var hasValue = !string.IsNullOrEmpty(item.OfficialRating); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.IsPlaceHolder.HasValue) + { + var filterValue = query.IsPlaceHolder.Value; + + var isPlaceHolder = false; + + var hasPlaceHolder = item as ISupportsPlaceHolders; + + if (hasPlaceHolder != null) + { + isPlaceHolder = hasPlaceHolder.IsPlaceHolder; + } + + if (isPlaceHolder != filterValue) + { + return false; + } + } + + if (query.HasSpecialFeature.HasValue) + { + var filterValue = query.HasSpecialFeature.Value; + + var movie = item as IHasSpecialFeatures; + + if (movie != null) + { + var ok = filterValue + ? movie.SpecialFeatureIds.Length > 0 + : movie.SpecialFeatureIds.Length == 0; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (query.HasSubtitles.HasValue) + { + var val = query.HasSubtitles.Value; + + var video = item as Video; + + if (video == null || val != video.HasSubtitles) + { + return false; + } + } + + if (query.HasParentalRating.HasValue) + { + var val = query.HasParentalRating.Value; + + var rating = item.CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = item.OfficialRating; + } + + if (val) + { + if (string.IsNullOrEmpty(rating)) + { + return false; + } + } + else + { + if (!string.IsNullOrEmpty(rating)) + { + return false; + } + } + } + + if (query.HasTrailer.HasValue) + { + var val = query.HasTrailer.Value; + var trailerCount = 0; + + var hasTrailers = item as IHasTrailers; + if (hasTrailers != null) + { + trailerCount = hasTrailers.GetTrailerIds().Count; + } + + var ok = val ? trailerCount > 0 : trailerCount == 0; + + if (!ok) + { + return false; + } + } + + if (query.HasThemeSong.HasValue) + { + var filterValue = query.HasThemeSong.Value; + + var themeCount = item.ThemeSongIds.Length; + var ok = filterValue ? themeCount > 0 : themeCount == 0; + + if (!ok) + { + return false; + } + } + + if (query.HasThemeVideo.HasValue) + { + var filterValue = query.HasThemeVideo.Value; + + var themeCount = item.ThemeVideoIds.Length; + var ok = filterValue ? themeCount > 0 : themeCount == 0; + + if (!ok) + { + return false; + } + } + + // Apply genre filter + if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + + // Filter by VideoType + if (query.VideoTypes.Length > 0) + { + var video = item as Video; + if (video == null || !query.VideoTypes.Contains(video.VideoType)) + { + return false; + } + } + + if (query.ImageTypes.Length > 0 && !query.ImageTypes.Any(item.HasImage)) + { + return false; + } + + // Apply studio filter + if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id => + { + var studioItem = libraryManager.GetItemById(id); + return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + + // Apply genre filter + if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => + { + var genreItem = libraryManager.GetItemById(id); + return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + + // Apply year filter + if (query.Years.Length > 0) + { + if (!(item.ProductionYear.HasValue && query.Years.Contains(item.ProductionYear.Value))) + { + return false; + } + } + + // Apply official rating filter + if (query.OfficialRatings.Length > 0 && !query.OfficialRatings.Contains(item.OfficialRating ?? string.Empty)) + { + return false; + } + + if (query.ItemIds.Length > 0) + { + if (!query.ItemIds.Contains(item.Id)) + { + return false; + } + } + + // Apply tag filter + var tags = query.Tags; + if (tags.Length > 0) + { + if (!tags.Any(v => item.Tags.Contains(v, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + } + + if (query.MinPlayers.HasValue) + { + var filterValue = query.MinPlayers.Value; + + var game = item as Game; + + if (game != null) + { + var players = game.PlayersSupported ?? 1; + + var ok = players >= filterValue; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (query.MaxPlayers.HasValue) + { + var filterValue = query.MaxPlayers.Value; + + var game = item as Game; + + if (game != null) + { + var players = game.PlayersSupported ?? 1; + + var ok = players <= filterValue; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (query.MinCommunityRating.HasValue) + { + var val = query.MinCommunityRating.Value; + + if (!(item.CommunityRating.HasValue && item.CommunityRating >= val)) + { + return false; + } + } + + if (query.MinCriticRating.HasValue) + { + var val = query.MinCriticRating.Value; + + if (!(item.CriticRating.HasValue && item.CriticRating >= val)) + { + return false; + } + } + + if (query.MinIndexNumber.HasValue) + { + var val = query.MinIndexNumber.Value; + + if (!(item.IndexNumber.HasValue && item.IndexNumber.Value >= val)) + { + return false; + } + } + + if (query.MinPremiereDate.HasValue) + { + var val = query.MinPremiereDate.Value; + + if (!(item.PremiereDate.HasValue && item.PremiereDate.Value >= val)) + { + return false; + } + } + + if (query.MaxPremiereDate.HasValue) + { + var val = query.MaxPremiereDate.Value; + + if (!(item.PremiereDate.HasValue && item.PremiereDate.Value <= val)) + { + return false; + } + } + + if (query.ParentIndexNumber.HasValue) + { + var filterValue = query.ParentIndexNumber.Value; + + if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value != filterValue) + { + return false; + } + } + + if (query.SeriesStatuses.Length > 0) + { + var ok = new[] { item }.OfType<Series>().Any(p => p.Status.HasValue && query.SeriesStatuses.Contains(p.Status.Value)); + if (!ok) + { + return false; + } + } + + if (query.AiredDuringSeason.HasValue) + { + var episode = item as Episode; + + if (episode == null) + { + return false; + } + + if (!Series.FilterEpisodesBySeason(new[] { episode }, query.AiredDuringSeason.Value, true).Any()) + { + return false; + } + } + + return true; + } + + private IEnumerable<BaseItem> GetMediaFolders(User user) + { + if (user == null) + { + return _libraryManager.RootFolder + .Children + .OfType<Folder>() + .Where(UserView.IsEligibleForGrouping); + } + return _libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i)); + } + + private BaseItem[] GetMediaFolders(User user, IEnumerable<string> viewTypes) + { + if (user == null) + { + return GetMediaFolders(null) + .Where(i => + { + var folder = i as ICollectionFolder; + + return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + }).ToArray(); + } + return GetMediaFolders(user) + .Where(i => + { + var folder = i as ICollectionFolder; + + return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + }).ToArray(); + } + + private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<string> viewTypes) + { + if (parent == null || parent is UserView) + { + return GetMediaFolders(user, viewTypes); + } + + return new BaseItem[] { parent }; + } + + private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent) + { + return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N"), type, sortName); + } + + private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) + { + return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName); + } + + public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, string adjacentToId) + { + var adjacentToIdGuid = new Guid(adjacentToId); + var adjacentToItem = list.FirstOrDefault(i => i.Id == adjacentToIdGuid); + + var index = list.IndexOf(adjacentToItem); + + var previousId = Guid.Empty; + var nextId = Guid.Empty; + + if (index > 0) + { + previousId = list[index - 1].Id; + } + + if (index < list.Count - 1) + { + nextId = list[index + 1].Id; + } + + return list.Where(i => i.Id == previousId || i.Id == nextId || i.Id == adjacentToIdGuid); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs new file mode 100644 index 000000000..65f5b8382 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -0,0 +1,626 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.LiveTv; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Video + /// </summary> + public class Video : BaseItem, + IHasAspectRatio, + ISupportsPlaceHolders, + IHasMediaSources + { + [IgnoreDataMember] + public string PrimaryVersionId { get; set; } + + public string[] AdditionalParts { get; set; } + public string[] LocalAlternateVersions { get; set; } + public LinkedChild[] LinkedAlternateVersions { get; set; } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPositionTicksResume + { + get + { + var extraType = ExtraType; + if (extraType.HasValue) + { + if (extraType.Value == Model.Entities.ExtraType.Sample) + { + return false; + } + if (extraType.Value == Model.Entities.ExtraType.ThemeVideo) + { + return false; + } + if (extraType.Value == Model.Entities.ExtraType.Trailer) + { + return false; + } + } + return true; + } + } + + public void SetPrimaryVersionId(string id) + { + if (string.IsNullOrEmpty(id)) + { + PrimaryVersionId = null; + } + else + { + PrimaryVersionId = id; + } + + PresentationUniqueKey = CreatePresentationUniqueKey(); + } + + public override string CreatePresentationUniqueKey() + { + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + return PrimaryVersionId; + } + + return base.CreatePresentationUniqueKey(); + } + + [IgnoreDataMember] + public override bool SupportsThemeMedia + { + get { return true; } + } + + /// <summary> + /// Gets or sets the timestamp. + /// </summary> + /// <value>The timestamp.</value> + public TransportStreamTimestamp? Timestamp { get; set; } + + /// <summary> + /// Gets or sets the subtitle paths. + /// </summary> + /// <value>The subtitle paths.</value> + public 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; } + + public bool IsPlaceHolder { get; set; } + public bool IsShortcut { get; set; } + public string ShortcutPath { get; set; } + + /// <summary> + /// Gets or sets the default index of the video stream. + /// </summary> + /// <value>The default index of the video stream.</value> + public int? DefaultVideoStreamIndex { get; set; } + + /// <summary> + /// Gets or sets the type of the video. + /// </summary> + /// <value>The type of the video.</value> + public VideoType VideoType { get; set; } + + /// <summary> + /// Gets or sets the type of the iso. + /// </summary> + /// <value>The type of the iso.</value> + public IsoType? IsoType { get; set; } + + /// <summary> + /// Gets or sets the video3 D format. + /// </summary> + /// <value>The video3 D format.</value> + public Video3DFormat? Video3DFormat { get; set; } + + public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder) + { + var videoType = VideoType; + + if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay) + { + videoType = VideoType.BluRay; + } + else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd) + { + videoType = VideoType.Dvd; + } + else + { + return new string[] {}; + } + return mediaEncoder.GetPlayableStreamFileNames(Path, videoType); + } + + /// <summary> + /// Gets or sets the aspect ratio. + /// </summary> + /// <value>The aspect ratio.</value> + public string AspectRatio { get; set; } + + public Video() + { + AdditionalParts = new string[] {}; + LocalAlternateVersions = new string[] {}; + SubtitleFiles = new string[] {}; + LinkedAlternateVersions = EmptyLinkedChildArray; + } + + public override bool CanDownload() + { + if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay) + { + return false; + } + + return IsFileProtocol; + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public int MediaSourceCount + { + get + { + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + var item = LibraryManager.GetItemById(PrimaryVersionId) as Video; + if (item != null) + { + return item.MediaSourceCount; + } + } + return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1; + } + } + + [IgnoreDataMember] + public bool IsStacked + { + get { return AdditionalParts.Length > 0; } + } + + [IgnoreDataMember] + public override bool HasLocalAlternateVersions + { + get { return LocalAlternateVersions.Length > 0; } + } + + public IEnumerable<Guid> GetAdditionalPartIds() + { + return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + + public IEnumerable<Guid> GetLocalAlternateVersionIds() + { + return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + + public static ILiveTvManager LiveTvManager { get; set; } + + [IgnoreDataMember] + public override SourceType SourceType + { + get + { + if (IsActiveRecording()) + { + return SourceType.LiveTV; + } + + return base.SourceType; + } + } + + protected override bool IsActiveRecording() + { + return LiveTvManager.GetActiveRecordingInfo(Path) != null; + } + + public override bool CanDelete() + { + if (IsActiveRecording()) + { + return false; + } + + return base.CanDelete(); + } + + [IgnoreDataMember] + public bool IsCompleteMedia + { + get + { + if (SourceType == SourceType.Channel) + { + return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + } + + return !IsActiveRecording(); + } + } + + [IgnoreDataMember] + protected virtual bool EnableDefaultVideoUserDataKeys + { + get + { + return true; + } + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (EnableDefaultVideoUserDataKeys) + { + if (ExtraType.HasValue) + { + var key = this.GetProviderId(MetadataProviders.Tmdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, GetUserDataKey(key)); + } + + key = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, GetUserDataKey(key)); + } + } + else + { + var key = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + key = this.GetProviderId(MetadataProviders.Tmdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + } + } + + return list; + } + + private string GetUserDataKey(string providerId) + { + var key = providerId + "-" + ExtraType.ToString().ToLower(); + + // Make sure different trailers have their own data. + if (RunTimeTicks.HasValue) + { + key += "-" + RunTimeTicks.Value.ToString(CultureInfo.InvariantCulture); + } + + return key; + } + + public IEnumerable<Video> GetLinkedAlternateVersions() + { + return LinkedAlternateVersions + .Select(GetLinkedChild) + .Where(i => i != null) + .OfType<Video>() + .OrderBy(i => i.SortName); + } + + /// <summary> + /// Gets the additional parts. + /// </summary> + /// <returns>IEnumerable{Video}.</returns> + public IEnumerable<Video> GetAdditionalParts() + { + return GetAdditionalPartIds() + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null) + .OfType<Video>() + .OrderBy(i => i.SortName); + } + + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + if (IsStacked) + { + return FileSystem.GetDirectoryName(Path); + } + + if (!IsPlaceHolder) + { + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return Path; + } + } + + return base.ContainingFolderPath; + } + } + + [IgnoreDataMember] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return System.IO.Path.GetFileName(Path); + } + + return System.IO.Path.GetFileNameWithoutExtension(Path); + } + + return null; + } + } + + internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) + { + var updateType = base.UpdateFromResolvedItem(newItem); + + var newVideo = newItem as Video; + if (newVideo != null) + { + if (!AdditionalParts.SequenceEqual(newVideo.AdditionalParts, StringComparer.Ordinal)) + { + AdditionalParts = newVideo.AdditionalParts; + updateType |= ItemUpdateType.MetadataImport; + } + if (!LocalAlternateVersions.SequenceEqual(newVideo.LocalAlternateVersions, StringComparer.Ordinal)) + { + LocalAlternateVersions = newVideo.LocalAlternateVersions; + updateType |= ItemUpdateType.MetadataImport; + } + if (VideoType != newVideo.VideoType) + { + VideoType = newVideo.VideoType; + updateType |= ItemUpdateType.MetadataImport; + } + } + + return updateType; + } + + public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType) + { + if (videoType == VideoType.Dvd) + { + return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true) + .OrderByDescending(i => i.Length) + .ThenBy(i => i.FullName) + .Take(1) + .Select(i => i.FullName) + .ToArray(); + } + if (videoType == VideoType.BluRay) + { + return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true) + .OrderByDescending(i => i.Length) + .ThenBy(i => i.FullName) + .Take(1) + .Select(i => i.FullName) + .ToArray(); + } + return new string[] {}; + } + + /// <summary> + /// Gets a value indicating whether [is3 D]. + /// </summary> + /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool Is3D + { + get { return Video3DFormat.HasValue; } + } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Video; + } + } + + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + if (IsStacked) + { + var tasks = AdditionalParts + .Select(i => RefreshMetadataForOwnedVideo(options, true, i, cancellationToken)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + // Must have a parent to have additional parts or alternate versions + // In other words, it must be part of the Parent/Child tree + // The additional parts won't have additional parts themselves + if (IsFileProtocol && SupportsOwnedItems) + { + if (!IsStacked) + { + RefreshLinkedAlternateVersions(); + + var tasks = LocalAlternateVersions + .Select(i => RefreshMetadataForOwnedVideo(options, false, i, cancellationToken)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + + return hasChanges; + } + + private void RefreshLinkedAlternateVersions() + { + foreach (var child in LinkedAlternateVersions) + { + // Reset the cached value + if (child.ItemId.HasValue && child.ItemId.Value.Equals(Guid.Empty)) + { + child.ItemId = null; + } + } + } + + public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + base.UpdateToRepository(updateReason, cancellationToken); + + var localAlternates = GetLocalAlternateVersionIds() + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null); + + foreach (var item in localAlternates) + { + item.ImageInfos = ImageInfos; + item.Overview = Overview; + item.ProductionYear = ProductionYear; + item.PremiereDate = PremiereDate; + item.CommunityRating = CommunityRating; + item.OfficialRating = OfficialRating; + item.Genres = Genres; + item.ProviderIds = ProviderIds; + + item.UpdateToRepository(ItemUpdateType.MetadataDownload, cancellationToken); + } + } + + public override IEnumerable<FileSystemMetadata> GetDeletePaths() + { + if (!IsInMixedFolder) + { + return new[] { + new FileSystemMetadata + { + FullName = ContainingFolderPath, + IsDirectory = true + } + }; + } + + return base.GetDeletePaths(); + } + + public virtual MediaStream GetDefaultVideoStream() + { + if (!DefaultVideoStreamIndex.HasValue) + { + return null; + } + + return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + { + ItemId = Id, + Index = DefaultVideoStreamIndex.Value + + }).FirstOrDefault(); + } + + protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources() + { + var list = new List<Tuple<BaseItem, MediaSourceType>>(); + + list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default)); + list.AddRange(GetLinkedAlternateVersions().Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping))); + + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + var primary = LibraryManager.GetItemById(PrimaryVersionId) as Video; + if (primary != null) + { + var existingIds = list.Select(i => i.Item1.Id).ToList(); + list.Add(new Tuple<BaseItem, MediaSourceType>(primary, MediaSourceType.Grouping)); + list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping))); + } + } + + var localAlternates = list + .SelectMany(i => + { + var video = i.Item1 as Video; + return video == null ? new List<Guid>() : video.GetLocalAlternateVersionIds(); + }) + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .ToList(); + + list.AddRange(localAlternates.Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Default))); + + return list; + } + + public static bool IsHD (Video video) { + return video.Height >= 720; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs new file mode 100644 index 000000000..81e030cea --- /dev/null +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Year + /// </summary> + public class Year : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, "Year-" + Name); + return list; + } + + /// <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> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + int year; + + var usCulture = new CultureInfo("en-US"); + + if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out year)) + { + return new List<BaseItem>(); + } + + query.Years = new[] { year }; + + return LibraryManager.GetItemList(query); + } + + public int? GetYearValue() + { + int i; + + if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out i)) + { + return i; + } + + return null; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} |
