aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Entities
diff options
context:
space:
mode:
authorAndrew Rabert <ar@nullsum.net>2018-12-27 18:27:57 -0500
committerAndrew Rabert <ar@nullsum.net>2018-12-27 18:27:57 -0500
commita86b71899ec52c44ddc6c3018e8cc5e9d7ff4d62 (patch)
treea74f6ea4a8abfa1664a605d31d48bc38245ccf58 /MediaBrowser.Controller/Entities
parent9bac3ac616b01f67db98381feb09d34ebe821f9a (diff)
Add GPL modules
Diffstat (limited to 'MediaBrowser.Controller/Entities')
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs219
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs216
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs15
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs272
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs275
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs145
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs69
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2960
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs65
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs54
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs72
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs405
-rw-r--r--MediaBrowser.Controller/Entities/DayOfWeekHelper.cs71
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs46
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs1803
-rw-r--r--MediaBrowser.Controller/Entities/Game.cs129
-rw-r--r--MediaBrowser.Controller/Entities/GameGenre.cs128
-rw-r--r--MediaBrowser.Controller/Entities/GameSystem.cs101
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs140
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs27
-rw-r--r--MediaBrowser.Controller/Entities/IHasAspectRatio.cs14
-rw-r--r--MediaBrowser.Controller/Entities/IHasDisplayOrder.cs15
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs19
-rw-r--r--MediaBrowser.Controller/Entities/IHasProgramAttributes.cs17
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs10
-rw-r--r--MediaBrowser.Controller/Entities/IHasSeries.cs20
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs13
-rw-r--r--MediaBrowser.Controller/Entities/IHasStartDate.cs9
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs39
-rw-r--r--MediaBrowser.Controller/Entities/IItemByName.cs17
-rw-r--r--MediaBrowser.Controller/Entities/IMetadataContainer.cs19
-rw-r--r--MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs12
-rw-r--r--MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs12
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs261
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs21
-rw-r--r--MediaBrowser.Controller/Entities/ItemImageInfo.cs46
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs73
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs263
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs203
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs79
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs119
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs216
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs104
-rw-r--r--MediaBrowser.Controller/Entities/PhotoAlbum.cs34
-rw-r--r--MediaBrowser.Controller/Entities/Share.cs15
-rw-r--r--MediaBrowser.Controller/Entities/SourceType.cs10
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs141
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs374
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs273
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs540
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs35
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs111
-rw-r--r--MediaBrowser.Controller/Entities/User.cs342
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs124
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs158
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs210
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs1070
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs626
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs147
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&lt;Guid&gt;.</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;
+ }
+ }
+}