diff options
Diffstat (limited to 'MediaBrowser.Controller')
265 files changed, 27594 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs new file mode 100644 index 000000000..6dd8f02d8 --- /dev/null +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -0,0 +1,14 @@ +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; + + +namespace MediaBrowser.Controller.Authentication +{ + public class AuthenticationResult + { + public UserDto User { get; set; } + public SessionInfo SessionInfo { get; set; } + public string AccessToken { get; set; } + public string ServerId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs new file mode 100644 index 000000000..becb3ea62 --- /dev/null +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Users; + +namespace MediaBrowser.Controller.Authentication +{ + public interface IAuthenticationProvider + { + string Name { get; } + bool IsEnabled { get; } + Task<ProviderAuthenticationResult> Authenticate(string username, string password); + Task<bool> HasPassword(User user); + Task ChangePassword(User user, string newPassword); + } + + public interface IRequiresResolvedUser + { + Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser); + } + + public interface IHasNewUserPolicy + { + UserPolicy GetNewUserPolicy(); + } + + public class ProviderAuthenticationResult + { + public string Username { get; set; } + public string DisplayName { get; set; } + } +} diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs new file mode 100644 index 000000000..9cd50db17 --- /dev/null +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -0,0 +1,94 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Querying; +using System; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Progress; + +namespace MediaBrowser.Controller.Channels +{ + public class Channel : Folder + { + public override bool IsVisible(User user) + { + if (user.Policy.BlockedChannels != null) + { + if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + else + { + if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + + return base.IsVisible(user); + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override SourceType SourceType + { + get { return SourceType.Channel; } + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + try + { + query.Parent = this; + query.ChannelIds = new Guid[] { Id }; + + // 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>(); + } + } + + protected override string GetInternalMetadataPath(string basePath) + { + return GetInternalMetadataPath(basePath, Id); + } + + public static string GetInternalMetadataPath(string basePath, Guid id) + { + return System.IO.Path.Combine(basePath, "channels", id.ToString("N"), "metadata"); + } + + public override bool CanDelete() + { + return false; + } + + protected override bool IsAllowTagFilterEnforced() + { + return false; + } + + internal static bool IsChannelVisible(BaseItem channelItem, User user) + { + var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString("")); + + return channel.IsVisible(user); + } + } +} diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs new file mode 100644 index 000000000..0de2b9a0c --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs @@ -0,0 +1,82 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Channels +{ + public class ChannelItemInfo : IHasProviderIds + { + public string Name { get; set; } + + public string SeriesName { get; set; } + + public string Id { get; set; } + + public DateTime DateModified { get; set; } + + public ChannelItemType Type { get; set; } + + public string OfficialRating { get; set; } + + public string Overview { get; set; } + + public List<string> Genres { get; set; } + public List<string> Studios { get; set; } + public List<string> Tags { get; set; } + + public List<PersonInfo> People { get; set; } + + public float? CommunityRating { get; set; } + + public long? RunTimeTicks { get; set; } + + public string ImageUrl { get; set; } + public string OriginalTitle { get; set; } + + public ChannelMediaType MediaType { get; set; } + public ChannelFolderType FolderType { get; set; } + + public ChannelMediaContentType ContentType { get; set; } + public ExtraType ExtraType { get; set; } + public List<TrailerType> TrailerTypes { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + public DateTime? PremiereDate { get; set; } + public int? ProductionYear { get; set; } + + public DateTime? DateCreated { get; set; } + + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + + public List<MediaSourceInfo> MediaSources { get; set; } + + public string HomePageUrl { get; set; } + + public List<string> Artists { get; set; } + + public List<string> AlbumArtists { get; set; } + public bool IsLiveStream { get; set; } + public string Etag { get; set; } + + public ChannelItemInfo() + { + MediaSources = new List<MediaSourceInfo>(); + TrailerTypes = new List<TrailerType>(); + Genres = new List<string>(); + Studios = new List<string>(); + People = new List<PersonInfo>(); + Tags = new List<string>(); + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + Artists = new List<string>(); + AlbumArtists = new List<string>(); + } + } +} diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs new file mode 100644 index 000000000..f88881811 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Channels +{ + public class ChannelItemResult + { + public List<ChannelItemInfo> Items { get; set; } + + public int? TotalRecordCount { get; set; } + + public ChannelItemResult() + { + Items = new List<ChannelItemInfo>(); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ChannelItemType.cs b/MediaBrowser.Controller/Channels/ChannelItemType.cs new file mode 100644 index 000000000..184ce8a76 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelItemType.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Channels +{ + public enum ChannelItemType + { + Media = 0, + + Folder = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ChannelParentalRating.cs b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs new file mode 100644 index 000000000..d9cc521b3 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs @@ -0,0 +1,15 @@ +namespace MediaBrowser.Controller.Channels +{ + public enum ChannelParentalRating + { + GeneralAudience = 0, + + UsPG = 1, + + UsPG13 = 2, + + UsR = 3, + + Adult = 4 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs new file mode 100644 index 000000000..c2a51654c --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Controller.Channels +{ + public class ChannelSearchInfo + { + public string SearchTerm { get; set; } + + public string UserId { get; set; } + } + + public class ChannelLatestMediaSearch + { + public string UserId { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs new file mode 100644 index 000000000..dc1d9b00a --- /dev/null +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -0,0 +1,76 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Channels +{ + public interface IChannel + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + string Description { get; } + + /// <summary> + /// Gets the data version. + /// </summary> + /// <value>The data version.</value> + string DataVersion { get; } + + /// <summary> + /// Gets the home page URL. + /// </summary> + /// <value>The home page URL.</value> + string HomePageUrl { get; } + + /// <summary> + /// Gets the parental rating. + /// </summary> + /// <value>The parental rating.</value> + ChannelParentalRating ParentalRating { get; } + + /// <summary> + /// Gets the channel information. + /// </summary> + /// <returns>ChannelFeatures.</returns> + InternalChannelFeatures GetChannelFeatures(); + + /// <summary> + /// Determines whether [is enabled for] [the specified user]. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <returns><c>true</c> if [is enabled for] [the specified user]; otherwise, <c>false</c>.</returns> + bool IsEnabledFor(string userId); + + /// <summary> + /// Gets the channel items. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{ChannelItem}}.</returns> + Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel image. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{DynamicImageInfo}.</returns> + Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken); + + /// <summary> + /// Gets the supported channel images. + /// </summary> + /// <returns>IEnumerable{ImageType}.</returns> + IEnumerable<ImageType> GetSupportedChannelImages(); + } +} diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs new file mode 100644 index 000000000..a9839e1fb --- /dev/null +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -0,0 +1,89 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Channels +{ + public interface IChannelManager + { + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="channels">The channels.</param> + void AddParts(IEnumerable<IChannel> channels); + + /// <summary> + /// Gets the channel features. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>ChannelFeatures.</returns> + ChannelFeatures GetChannelFeatures(string id); + + /// <summary> + /// Gets all channel features. + /// </summary> + /// <returns>IEnumerable{ChannelFeatures}.</returns> + ChannelFeatures[] GetAllChannelFeatures(); + + bool EnableMediaSourceDisplay(BaseItem item); + bool CanDelete(BaseItem item); + + Task DeleteItem(BaseItem item); + + /// <summary> + /// Gets the channel. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>Channel.</returns> + Channel GetChannel(string id); + + /// <summary> + /// Gets the channels internal. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + QueryResult<Channel> GetChannelsInternal(ChannelQuery query); + + /// <summary> + /// Gets the channels. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + QueryResult<BaseItemDto> GetChannels(ChannelQuery query); + + /// <summary> + /// Gets the latest media. + /// </summary> + Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the latest media. + /// </summary> + Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel items. + /// </summary> + Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel items internal. + /// </summary> + Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel item media sources. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns> + IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken); + + bool EnableMediaProbe(BaseItem item); + } +} diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs new file mode 100644 index 000000000..6376d2f91 --- /dev/null +++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs @@ -0,0 +1,13 @@ + +namespace MediaBrowser.Controller.Channels +{ + public interface IHasCacheKey + { + /// <summary> + /// Gets the cache key. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <returns>System.String.</returns> + string GetCacheKey(string userId); + } +} diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs new file mode 100644 index 000000000..a2c63586b --- /dev/null +++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Channels +{ + public interface IRequiresMediaInfoCallback + { + /// <summary> + /// Gets the channel item media information. + /// </summary> + Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs new file mode 100644 index 000000000..bf9842eb4 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Channels +{ + public interface ISearchableChannel + { + /// <summary> + /// Searches the specified search term. + /// </summary> + /// <param name="searchInfo">The search information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns> + Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken); + } + + public interface ISupportsLatestMedia + { + /// <summary> + /// Gets the latest media. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns> + Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken); + } + + public interface ISupportsDelete + { + bool CanDelete(BaseItem item); + Task DeleteItem(string id, CancellationToken cancellationToken); + } + + public interface IDisableMediaSourceDisplay + { + + } + + public interface ISupportsMediaProbe + { + + } + + public interface IHasFolderAttributes + { + string[] Attributes { get; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs new file mode 100644 index 000000000..976808aad --- /dev/null +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -0,0 +1,61 @@ +using System; +using MediaBrowser.Model.Channels; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Channels +{ + public class InternalChannelFeatures + { + /// <summary> + /// Gets or sets the media types. + /// </summary> + /// <value>The media types.</value> + public List<ChannelMediaType> MediaTypes { get; set; } + + /// <summary> + /// Gets or sets the content types. + /// </summary> + /// <value>The content types.</value> + public List<ChannelMediaContentType> ContentTypes { get; set; } + + /// <summary> + /// Represents the maximum number of records the channel allows retrieving at a time + /// </summary> + public int? MaxPageSize { get; set; } + + /// <summary> + /// Gets or sets the default sort orders. + /// </summary> + /// <value>The default sort orders.</value> + public List<ChannelItemSortField> DefaultSortFields { get; set; } + + /// <summary> + /// Indicates if a sort ascending/descending toggle is supported or not. + /// </summary> + public bool SupportsSortOrderToggle { get; set; } + /// <summary> + /// Gets or sets the automatic refresh levels. + /// </summary> + /// <value>The automatic refresh levels.</value> + public int? AutoRefreshLevels { get; set; } + + /// <summary> + /// Gets or sets the daily download limit. + /// </summary> + /// <value>The daily download limit.</value> + public int? DailyDownloadLimit { get; set; } + /// <summary> + /// Gets or sets a value indicating whether [supports downloading]. + /// </summary> + /// <value><c>true</c> if [supports downloading]; otherwise, <c>false</c>.</value> + public bool SupportsContentDownloading { get; set; } + + public InternalChannelFeatures() + { + MediaTypes = new List<ChannelMediaType>(); + ContentTypes = new List<ChannelMediaContentType>(); + + DefaultSortFields = new List<ChannelItemSortField>(); + } + } +} diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs new file mode 100644 index 000000000..c69a1f6c3 --- /dev/null +++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs @@ -0,0 +1,21 @@ +using MediaBrowser.Model.Channels; +using System; + + +namespace MediaBrowser.Controller.Channels +{ + public class InternalChannelItemQuery + { + public string FolderId { get; set; } + + public Guid UserId { get; set; } + + public int? StartIndex { get; set; } + + public int? Limit { get; set; } + + public ChannelItemSortField? SortBy { get; set; } + + public bool SortDescending { get; set; } + } +} diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs new file mode 100644 index 000000000..2a20eb365 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Chapters +{ + /// <summary> + /// Interface IChapterManager + /// </summary> + public interface IChapterManager + { + /// <summary> + /// Gets the chapters. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <returns>List{ChapterInfo}.</returns> + + /// <summary> + /// Saves the chapters. + /// </summary> + void SaveChapters(string itemId, List<ChapterInfo> chapters); + } +} diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs new file mode 100644 index 000000000..727b487a7 --- /dev/null +++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs @@ -0,0 +1,27 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Collections +{ + public class CollectionCreationOptions : IHasProviderIds + { + public string Name { get; set; } + + public Guid? ParentId { get; set; } + + public bool IsLocked { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + public string[] ItemIdList { get; set; } + public Guid[] UserIds { get; set; } + + public CollectionCreationOptions() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ItemIdList = new string[] {}; + UserIds = new Guid[] {}; + } + } +} diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionEvents.cs new file mode 100644 index 000000000..80f66a444 --- /dev/null +++ b/MediaBrowser.Controller/Collections/CollectionEvents.cs @@ -0,0 +1,37 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Collections +{ + public class CollectionCreatedEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the collection. + /// </summary> + /// <value>The collection.</value> + public BoxSet Collection { get; set; } + + /// <summary> + /// Gets or sets the options. + /// </summary> + /// <value>The options.</value> + public CollectionCreationOptions Options { get; set; } + } + + public class CollectionModifiedEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the collection. + /// </summary> + /// <value>The collection.</value> + public BoxSet Collection { get; set; } + + /// <summary> + /// Gets or sets the items changed. + /// </summary> + /// <value>The items changed.</value> + public List<BaseItem> ItemsChanged { get; set; } + } +} diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs new file mode 100644 index 000000000..05bc927ba --- /dev/null +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -0,0 +1,57 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Collections +{ + public interface ICollectionManager + { + /// <summary> + /// Occurs when [collection created]. + /// </summary> + event EventHandler<CollectionCreatedEventArgs> CollectionCreated; + + /// <summary> + /// Occurs when [items added to collection]. + /// </summary> + event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection; + + /// <summary> + /// Occurs when [items removed from collection]. + /// </summary> + event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection; + + /// <summary> + /// Creates the collection. + /// </summary> + /// <param name="options">The options.</param> + BoxSet CreateCollection(CollectionCreationOptions options); + + /// <summary> + /// Adds to collection. + /// </summary> + /// <param name="collectionId">The collection identifier.</param> + /// <param name="itemIds">The item ids.</param> + void AddToCollection(Guid collectionId, IEnumerable<string> itemIds); + + /// <summary> + /// Removes from collection. + /// </summary> + /// <param name="collectionId">The collection identifier.</param> + /// <param name="itemIds">The item ids.</param> + void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds); + + void AddToCollection(Guid collectionId, IEnumerable<Guid> itemIds); + void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds); + + /// <summary> + /// Collapses the items within box sets. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{BaseItem}.</returns> + IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user); + } +} diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs new file mode 100644 index 000000000..af5714932 --- /dev/null +++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.Configuration +{ + /// <summary> + /// Interface IServerConfigurationManager + /// </summary> + public interface IServerConfigurationManager : IConfigurationManager + { + /// <summary> + /// Gets the application paths. + /// </summary> + /// <value>The application paths.</value> + IServerApplicationPaths ApplicationPaths { get; } + + /// <summary> + /// Gets the configuration. + /// </summary> + /// <value>The configuration.</value> + ServerConfiguration Configuration { get; } + + bool SetOptimalValues(); + } +} diff --git a/MediaBrowser.Controller/Connect/IConnectManager.cs b/MediaBrowser.Controller/Connect/IConnectManager.cs new file mode 100644 index 000000000..8ac61bf2b --- /dev/null +++ b/MediaBrowser.Controller/Connect/IConnectManager.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Connect; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Connect +{ + public interface IConnectManager + { + /// <summary> + /// Gets the wan API address. + /// </summary> + /// <value>The wan API address.</value> + string WanApiAddress { get; } + + /// <summary> + /// Links the user. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <param name="connectUsername">The connect username.</param> + /// <returns>Task.</returns> + Task<UserLinkResult> LinkUser(string userId, string connectUsername); + + /// <summary> + /// Removes the link. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <returns>Task.</returns> + Task RemoveConnect(string userId); + + User GetUserFromExchangeToken(string token); + + /// <summary> + /// Authenticates the specified username. + /// </summary> + Task<ConnectAuthenticationResult> Authenticate(string username, string password, string passwordMd5); + + /// <summary> + /// Determines whether [is authorization token valid] [the specified token]. + /// </summary> + /// <param name="token">The token.</param> + /// <returns><c>true</c> if [is authorization token valid] [the specified token]; otherwise, <c>false</c>.</returns> + bool IsAuthorizationTokenValid(string token); + } +} diff --git a/MediaBrowser.Controller/Connect/UserLinkResult.cs b/MediaBrowser.Controller/Connect/UserLinkResult.cs new file mode 100644 index 000000000..16ebfc70a --- /dev/null +++ b/MediaBrowser.Controller/Connect/UserLinkResult.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Connect +{ + public class UserLinkResult + { + public bool IsPending { get; set; } + public bool IsNewUserInvitation { get; set; } + public string GuestDisplayName { get; set; } + } +} diff --git a/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs b/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs new file mode 100644 index 000000000..b3f3bb902 --- /dev/null +++ b/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs @@ -0,0 +1,10 @@ +using MediaBrowser.Model.Devices; + +namespace MediaBrowser.Controller.Devices +{ + public class CameraImageUploadInfo + { + public LocalFileInfo FileInfo { get; set; } + public DeviceInfo Device { get; set; } + } +} diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs new file mode 100644 index 000000000..d29fb8ded --- /dev/null +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -0,0 +1,73 @@ +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Session; +using System; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Devices +{ + public interface IDeviceManager + { + /// <summary> + /// Occurs when [camera image uploaded]. + /// </summary> + event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; + + /// <summary> + /// Saves the capabilities. + /// </summary> + /// <param name="reportedId">The reported identifier.</param> + /// <param name="capabilities">The capabilities.</param> + /// <returns>Task.</returns> + void SaveCapabilities(string reportedId, ClientCapabilities capabilities); + + /// <summary> + /// Gets the capabilities. + /// </summary> + /// <param name="reportedId">The reported identifier.</param> + /// <returns>ClientCapabilities.</returns> + ClientCapabilities GetCapabilities(string reportedId); + + /// <summary> + /// Gets the device information. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>DeviceInfo.</returns> + DeviceInfo GetDevice(string id); + + /// <summary> + /// Gets the devices. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable<DeviceInfo>.</returns> + QueryResult<DeviceInfo> GetDevices(DeviceQuery query); + + /// <summary> + /// Gets the upload history. + /// </summary> + /// <param name="deviceId">The device identifier.</param> + /// <returns>ContentUploadHistory.</returns> + ContentUploadHistory GetCameraUploadHistory(string deviceId); + + /// <summary> + /// Accepts the upload. + /// </summary> + /// <param name="deviceId">The device identifier.</param> + /// <param name="stream">The stream.</param> + /// <param name="file">The file.</param> + /// <returns>Task.</returns> + Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file); + + /// <summary> + /// Determines whether this instance [can access device] the specified user identifier. + /// </summary> + bool CanAccessDevice(User user, string deviceId); + + void UpdateDeviceOptions(string deviceId, DeviceOptions options); + DeviceOptions GetDeviceOptions(string deviceId); + event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; + } +} diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs new file mode 100644 index 000000000..2f64cd194 --- /dev/null +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -0,0 +1,76 @@ +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Dlna; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Dlna +{ + public interface IDlnaManager + { + /// <summary> + /// Gets the profile infos. + /// </summary> + /// <returns>IEnumerable{DeviceProfileInfo}.</returns> + IEnumerable<DeviceProfileInfo> GetProfileInfos(); + + /// <summary> + /// Gets the profile. + /// </summary> + /// <param name="headers">The headers.</param> + /// <returns>DeviceProfile.</returns> + DeviceProfile GetProfile(IDictionary<string,string> headers); + + /// <summary> + /// Gets the default profile. + /// </summary> + /// <returns>DeviceProfile.</returns> + DeviceProfile GetDefaultProfile(); + + /// <summary> + /// Creates the profile. + /// </summary> + /// <param name="profile">The profile.</param> + void CreateProfile(DeviceProfile profile); + + /// <summary> + /// Updates the profile. + /// </summary> + /// <param name="profile">The profile.</param> + void UpdateProfile(DeviceProfile profile); + + /// <summary> + /// Deletes the profile. + /// </summary> + /// <param name="id">The identifier.</param> + void DeleteProfile(string id); + + /// <summary> + /// Gets the profile. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>DeviceProfile.</returns> + DeviceProfile GetProfile(string id); + + /// <summary> + /// Gets the profile. + /// </summary> + /// <param name="deviceInfo">The device information.</param> + /// <returns>DeviceProfile.</returns> + DeviceProfile GetProfile(DeviceIdentification deviceInfo); + + /// <summary> + /// Gets the server description XML. + /// </summary> + /// <param name="headers">The headers.</param> + /// <param name="serverUuId">The server uu identifier.</param> + /// <param name="serverAddress">The server address.</param> + /// <returns>System.String.</returns> + string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress); + + /// <summary> + /// Gets the icon. + /// </summary> + /// <param name="filename">The filename.</param> + /// <returns>DlnaIconResponse.</returns> + ImageStream GetIcon(string filename); + } +} diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs new file mode 100644 index 000000000..757448eb2 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -0,0 +1,49 @@ +using System; +using MediaBrowser.Model.Drawing; + +namespace MediaBrowser.Controller.Drawing +{ + public interface IImageEncoder + { + /// <summary> + /// Gets the supported input formats. + /// </summary> + /// <value>The supported input formats.</value> + string[] SupportedInputFormats { get; } + /// <summary> + /// Gets the supported output formats. + /// </summary> + /// <value>The supported output formats.</value> + ImageFormat[] SupportedOutputFormats { get; } + + /// <summary> + /// Encodes the image. + /// </summary> + string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); + + /// <summary> + /// Creates the image collage. + /// </summary> + /// <param name="options">The options.</param> + void CreateImageCollage(ImageCollageOptions options); + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets a value indicating whether [supports image collage creation]. + /// </summary> + /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> + bool SupportsImageCollageCreation { get; } + + /// <summary> + /// Gets a value indicating whether [supports image encoding]. + /// </summary> + /// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value> + bool SupportsImageEncoding { get; } + + ImageSize GetImageSize(string path); + } +} diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs new file mode 100644 index 000000000..fdf10e223 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -0,0 +1,118 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Drawing +{ + /// <summary> + /// Interface IImageProcessor + /// </summary> + public interface IImageProcessor + { + /// <summary> + /// Gets the supported input formats. + /// </summary> + /// <value>The supported input formats.</value> + string[] SupportedInputFormats { get; } + + /// <summary> + /// Gets the image enhancers. + /// </summary> + /// <value>The image enhancers.</value> + IImageEnhancer[] ImageEnhancers { get; } + + ImageSize GetImageSize(string path); + + /// <summary> + /// Gets the size of the image. + /// </summary> + /// <param name="info">The information.</param> + /// <returns>ImageSize.</returns> + ImageSize GetImageSize(BaseItem item, ItemImageInfo info); + + ImageSize GetImageSize(BaseItem item, ItemImageInfo info, bool allowSlowMethods, bool updateItem); + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="enhancers">The enhancers.</param> + void AddParts(IEnumerable<IImageEnhancer> enhancers); + + /// <summary> + /// Gets the supported enhancers. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <returns>IEnumerable{IImageEnhancer}.</returns> + IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType); + + /// <summary> + /// Gets the image cache tag. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <returns>Guid.</returns> + string GetImageCacheTag(BaseItem item, ItemImageInfo image); + string GetImageCacheTag(BaseItem item, ChapterInfo info); + + /// <summary> + /// Gets the image cache tag. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <param name="imageEnhancers">The image enhancers.</param> + /// <returns>Guid.</returns> + string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers); + + /// <summary> + /// Processes the image. + /// </summary> + /// <param name="options">The options.</param> + /// <param name="toStream">To stream.</param> + /// <returns>Task.</returns> + Task ProcessImage(ImageProcessingOptions options, Stream toStream); + + /// <summary> + /// Processes the image. + /// </summary> + /// <param name="options">The options.</param> + /// <returns>Task.</returns> + Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options); + + /// <summary> + /// Gets the enhanced image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>Task{System.String}.</returns> + Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex); + + /// <summary> + /// Gets the supported image output formats. + /// </summary> + /// <returns>ImageOutputFormat[].</returns> + ImageFormat[] GetSupportedImageOutputFormats(); + + /// <summary> + /// Creates the image collage. + /// </summary> + /// <param name="options">The options.</param> + void CreateImageCollage(ImageCollageOptions options); + + /// <summary> + /// Gets a value indicating whether [supports image collage creation]. + /// </summary> + /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> + bool SupportsImageCollageCreation { get; } + + IImageEncoder ImageEncoder { get; set; } + + bool SupportsTransparency(string path); + } +} diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs new file mode 100644 index 000000000..92a7f5ac9 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs @@ -0,0 +1,27 @@ + +namespace MediaBrowser.Controller.Drawing +{ + public class ImageCollageOptions + { + /// <summary> + /// Gets or sets the input paths. + /// </summary> + /// <value>The input paths.</value> + public string[] InputPaths { get; set; } + /// <summary> + /// Gets or sets the output path. + /// </summary> + /// <value>The output path.</value> + public string OutputPath { get; set; } + /// <summary> + /// Gets or sets the width. + /// </summary> + /// <value>The width.</value> + public int Width { get; set; } + /// <summary> + /// Gets or sets the height. + /// </summary> + /// <value>The height.</value> + public int Height { get; set; } + } +} diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs new file mode 100644 index 000000000..6fb9f256e --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -0,0 +1,72 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Drawing +{ + public static class ImageHelper + { + public static ImageSize GetNewImageSize(ImageProcessingOptions options, ImageSize? originalImageSize) + { + if (originalImageSize.HasValue) + { + // Determine the output size based on incoming parameters + var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0); + + return newSize; + } + return GetSizeEstimate(options); + } + + public static IImageProcessor ImageProcessor { get; set; } + + private static ImageSize GetSizeEstimate(ImageProcessingOptions options) + { + if (options.Width.HasValue && options.Height.HasValue) + { + return new ImageSize(options.Width.Value, options.Height.Value); + } + + var aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item); + + var width = options.Width ?? options.MaxWidth; + + if (width.HasValue) + { + var heightValue = width.Value / aspect; + return new ImageSize(width.Value, heightValue); + } + + var height = options.Height ?? options.MaxHeight ?? 200; + var widthValue = aspect * height; + return new ImageSize(widthValue, height); + } + + private static double GetEstimatedAspectRatio(ImageType type, BaseItem item) + { + switch (type) + { + case ImageType.Art: + case ImageType.Backdrop: + case ImageType.Chapter: + case ImageType.Screenshot: + case ImageType.Thumb: + return 1.78; + case ImageType.Banner: + return 5.4; + case ImageType.Box: + case ImageType.BoxRear: + case ImageType.Disc: + case ImageType.Menu: + return 1; + case ImageType.Logo: + return 2.58; + case ImageType.Primary: + return item.GetDefaultPrimaryImageAspectRatio(); + default: + return 1; + } + } + } +} diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs new file mode 100644 index 000000000..ffc3e6cc0 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -0,0 +1,114 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Drawing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Controller.Drawing +{ + public class ImageProcessingOptions + { + public ImageProcessingOptions() + { + RequiresAutoOrientation = true; + } + + public Guid ItemId { get; set; } + public BaseItem Item { get; set; } + + public ItemImageInfo Image { get; set; } + + public int ImageIndex { get; set; } + + public bool CropWhiteSpace { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public int? MaxWidth { get; set; } + + public int? MaxHeight { get; set; } + + public int Quality { get; set; } + + public IImageEnhancer[] Enhancers { get; set; } + + public ImageFormat[] SupportedOutputFormats { get; set; } + + public bool AddPlayedIndicator { get; set; } + + public int? UnplayedCount { get; set; } + public int? Blur { get; set; } + + public double PercentPlayed { get; set; } + + public string BackgroundColor { get; set; } + public string ForegroundLayer { get; set; } + public bool RequiresAutoOrientation { get; set; } + + private bool HasDefaultOptions(string originalImagePath) + { + return HasDefaultOptionsWithoutSize(originalImagePath) && + !Width.HasValue && + !Height.HasValue && + !MaxWidth.HasValue && + !MaxHeight.HasValue; + } + + public bool HasDefaultOptions(string originalImagePath, ImageSize? size) + { + if (!size.HasValue) + { + return HasDefaultOptions(originalImagePath); + } + + if (!HasDefaultOptionsWithoutSize(originalImagePath)) + { + return false; + } + + var sizeValue = size.Value; + + if (Width.HasValue && !sizeValue.Width.Equals(Width.Value)) + { + return false; + } + if (Height.HasValue && !sizeValue.Height.Equals(Height.Value)) + { + return false; + } + if (MaxWidth.HasValue && sizeValue.Width > MaxWidth.Value) + { + return false; + } + if (MaxHeight.HasValue && sizeValue.Height > MaxHeight.Value) + { + return false; + } + + return true; + } + + private bool HasDefaultOptionsWithoutSize(string originalImagePath) + { + return (Quality >= 90) && + IsFormatSupported(originalImagePath) && + !AddPlayedIndicator && + PercentPlayed.Equals(0) && + !UnplayedCount.HasValue && + !Blur.HasValue && + !CropWhiteSpace && + string.IsNullOrEmpty(BackgroundColor) && + string.IsNullOrEmpty(ForegroundLayer); + } + + private bool IsFormatSupported(string originalImagePath) + { + var ext = Path.GetExtension(originalImagePath); + return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs new file mode 100644 index 000000000..948219bf5 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Drawing +{ + public static class ImageProcessorExtensions + { + public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) + { + return processor.GetImageCacheTag(item, imageType, 0); + } + + public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) + { + var imageInfo = item.GetImageInfo(imageType, imageIndex); + + if (imageInfo == null) + { + return null; + } + + return processor.GetImageCacheTag(item, imageInfo); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs new file mode 100644 index 000000000..353abaca3 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageStream.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Model.Drawing; +using System; +using System.IO; + +namespace MediaBrowser.Controller.Drawing +{ + public class ImageStream : IDisposable + { + /// <summary> + /// Gets or sets the stream. + /// </summary> + /// <value>The stream.</value> + public Stream Stream { get; set; } + /// <summary> + /// Gets or sets the format. + /// </summary> + /// <value>The format.</value> + public ImageFormat Format { get; set; } + + public void Dispose() + { + if (Stream != null) + { + Stream.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs new file mode 100644 index 000000000..b5ce09028 --- /dev/null +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -0,0 +1,72 @@ +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Linq; + +namespace MediaBrowser.Controller.Dto +{ + public class DtoOptions + { + private static readonly ItemFields[] DefaultExcludedFields = new [] + { + ItemFields.SeasonUserData, + ItemFields.RefreshState + }; + + public ItemFields[] Fields { get; set; } + public ImageType[] ImageTypes { get; set; } + public int ImageTypeLimit { get; set; } + public bool EnableImages { get; set; } + public bool AddProgramRecordingInfo { get; set; } + public bool EnableUserData { get; set; } + public bool AddCurrentProgram { get; set; } + + public DtoOptions() + : this(true) + { + } + + private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType)) + .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true)) + .ToArray(); + + private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields)) + .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + .Except(DefaultExcludedFields) + .ToArray(); + + public bool ContainsField(ItemFields field) + { + return AllItemFields.Contains(field); + } + + public DtoOptions(bool allFields) + { + ImageTypeLimit = int.MaxValue; + EnableImages = true; + EnableUserData = true; + AddCurrentProgram = true; + + if (allFields) + { + Fields = AllItemFields; + } + else + { + Fields = new ItemFields[] { }; + } + + ImageTypes = AllImageTypes; + } + + public int GetImageLimit(ImageType type) + { + if (EnableImages && ImageTypes.Contains(type)) + { + return ImageTypeLimit; + } + + return 0; + } + } +} diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs new file mode 100644 index 000000000..219b36789 --- /dev/null +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -0,0 +1,70 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using System.Collections.Generic; +using MediaBrowser.Controller.Sync; + +namespace MediaBrowser.Controller.Dto +{ + /// <summary> + /// Interface IDtoService + /// </summary> + public interface IDtoService + { + /// <summary> + /// Gets the dto id. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + string GetDtoId(BaseItem item); + + /// <summary> + /// Attaches the primary image aspect ratio. + /// </summary> + /// <param name="dto">The dto.</param> + /// <param name="item">The item.</param> + void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item); + + /// <summary> + /// Gets the primary image aspect ratio. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.Nullable<System.Double>.</returns> + double? GetPrimaryImageAspectRatio(BaseItem item); + + /// <summary> + /// Gets the base item dto. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="fields">The fields.</param> + /// <param name="user">The user.</param> + /// <param name="owner">The owner.</param> + BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null); + + /// <summary> + /// Gets the base item dto. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="options">The options.</param> + /// <param name="user">The user.</param> + /// <param name="owner">The owner.</param> + /// <returns>BaseItemDto.</returns> + BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null); + + /// <summary> + /// Gets the base item dtos. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="options">The options.</param> + /// <param name="user">The user.</param> + /// <param name="owner">The owner.</param> + BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null); + + BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); + + /// <summary> + /// Gets the item by name dto. + /// </summary> + BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null); + } +} diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs new file mode 100644 index 000000000..4f4b3483c --- /dev/null +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -0,0 +1,219 @@ +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Specialized folder that can have items added to it's children by external entities. + /// Used for our RootFolder so plug-ins can add items. + /// </summary> + public class AggregateFolder : Folder + { + public AggregateFolder() + { + PhysicalLocationsList = new string[] { }; + } + + [IgnoreDataMember] + public override bool IsPhysicalRoot + { + get { return true; } + } + + public override bool CanDelete() + { + return false; + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + /// <summary> + /// The _virtual children + /// </summary> + private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>(); + + /// <summary> + /// Gets the virtual children. + /// </summary> + /// <value>The virtual children.</value> + public ConcurrentBag<BaseItem> VirtualChildren + { + get { return _virtualChildren; } + } + + [IgnoreDataMember] + public override string[] PhysicalLocations + { + get + { + return PhysicalLocationsList; + } + } + + public string[] PhysicalLocationsList { get; set; } + + protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) + { + return CreateResolveArgs(directoryService, true).FileSystemChildren; + } + + private Guid[] _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override List<BaseItem> LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null || _childrenIds.Length == 0) + { + var list = base.LoadChildren(); + _childrenIds = list.Select(i => i.Id).ToArray(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + + private bool _requiresRefresh; + public override bool RequiresRefresh() + { + var changed = base.RequiresRefresh() || _requiresRefresh; + + if (!changed) + { + var locations = PhysicalLocations; + + var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + + if (!locations.SequenceEqual(newLocations)) + { + changed = true; + } + } + + return changed; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + ClearCache(); + + var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + _requiresRefresh = false; + return changed; + } + + private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) + { + ClearCache(); + + var path = ContainingFolderPath; + + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + { + FileInfo = FileSystem.GetDirectoryInfo(path), + Path = path + }; + + // Gather child folder and files + if (args.IsDirectory) + { + // When resolving the root, we need it's grandchildren (children of user views) + var flattenFolderDepth = 2; + + var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, CollectionFolder.ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true); + + // Need to remove subpaths that may have been resolved from shortcuts + // Example: if \\server\movies exists, then strip out \\server\movies\action + files = LibraryManager.NormalizeRootPathList(files).ToArray(); + + args.FileSystemChildren = files; + } + + _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations); + if (setPhysicalLocations) + { + PhysicalLocationsList = args.PhysicalLocations; + } + + return args; + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); + } + + protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + ClearCache(); + + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + .ConfigureAwait(false); + + ClearCache(); + } + + /// <summary> + /// Adds the virtual child. + /// </summary> + /// <param name="child">The child.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddVirtualChild(BaseItem child) + { + if (child == null) + { + throw new ArgumentNullException(); + } + + _virtualChildren.Add(child); + } + + /// <summary> + /// Finds the virtual child. + /// </summary> + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + public BaseItem FindVirtualChild(Guid id) + { + if (id.Equals(Guid.Empty)) + { + throw new ArgumentNullException("id"); + } + + foreach (var child in _virtualChildren) + { + if (child.Id == id) + { + return child; + } + } + return null; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs new file mode 100644 index 000000000..d07e31d8a --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -0,0 +1,216 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class Audio + /// </summary> + public class Audio : BaseItem, + IHasAlbumArtist, + IHasArtist, + IHasMusicGenres, + IHasLookupInfo<SongInfo>, + IHasMediaSources + { + /// <summary> + /// Gets or sets the artist. + /// </summary> + /// <value>The artist.</value> + [IgnoreDataMember] + public string[] Artists { get; set; } + + [IgnoreDataMember] + public string[] AlbumArtists { get; set; } + + public Audio() + { + Artists = new string[] {}; + AlbumArtists = new string[] {}; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return false; } + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + protected override bool SupportsOwnedItems + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override Folder LatestItemsIndexContainer + { + get + { + return AlbumEntity; + } + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + [IgnoreDataMember] + public string[] AllArtists + { + get + { + var list = new string[AlbumArtists.Length + Artists.Length]; + + var index = 0; + foreach (var artist in AlbumArtists) + { + list[index] = artist; + index++; + } + foreach (var artist in Artists) + { + list[index] = artist; + index++; + } + + return list; + + } + } + + [IgnoreDataMember] + public MusicAlbum AlbumEntity + { + get { return FindParent<MusicAlbum>(); } + } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Audio; + } + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected override string CreateSortName() + { + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "") + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty; + + + if (ParentIndexNumber.HasValue) + { + songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey; + } + songKey += Name; + + if (!string.IsNullOrEmpty(Album)) + { + songKey = Album + "-" + songKey; + } + + var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0]; + if (!string.IsNullOrEmpty(albumArtist)) + { + songKey = albumArtist + "-" + songKey; + } + + list.Insert(0, songKey); + + return list; + } + + public override UnratedItem GetBlockUnratedType() + { + if (SourceType == SourceType.Library) + { + return UnratedItem.Music; + } + return base.GetBlockUnratedType(); + } + + public List<MediaStream> GetMediaStreams(MediaStreamType type) + { + return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + { + ItemId = Id, + Type = type + }); + } + + public SongInfo GetLookupInfo() + { + var info = GetItemLookupInfo<SongInfo>(); + + info.AlbumArtists = AlbumArtists; + info.Album = Album; + info.Artists = Artists; + + return info; + } + + protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources() + { + var list = new List<Tuple<BaseItem, MediaSourceType>>(); + list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default)); + return list; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs new file mode 100644 index 000000000..b2dedada4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -0,0 +1,15 @@ + +namespace MediaBrowser.Controller.Entities.Audio +{ + public interface IHasAlbumArtist + { + string[] AlbumArtists { get; set; } + } + + public interface IHasArtist + { + string[] AllArtists { get; } + + string[] Artists { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs new file mode 100644 index 000000000..2200d4b75 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities.Audio +{ + public interface IHasMusicGenres + { + string[] Genres { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs new file mode 100644 index 000000000..48b5c64b2 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -0,0 +1,272 @@ +using System; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Users; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class MusicAlbum + /// </summary> + public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer + { + public string[] AlbumArtists { get; set; } + public string[] Artists { get; set; } + + public MusicAlbum() + { + Artists = new string[] {}; + AlbumArtists = new string[] {}; + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + public MusicArtist MusicArtist + { + get { return GetMusicArtist(new DtoOptions(true)); } + } + + public MusicArtist GetMusicArtist(DtoOptions options) + { + var parents = GetParents(); + foreach (var parent in parents) + { + var artist = parent as MusicArtist; + if (artist != null) + { + return artist; + } + } + + var name = AlbumArtist; + if (!string.IsNullOrEmpty(name)) + { + return LibraryManager.GetArtist(name, options); + } + return null; + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsCumulativeRunTimeTicks + { + get + { + return true; + } + } + + [IgnoreDataMember] + public string[] AllArtists + { + get + { + var list = new string[AlbumArtists.Length + Artists.Length]; + + var index = 0; + foreach (var artist in AlbumArtists) + { + list[index] = artist; + index++; + } + foreach (var artist in Artists) + { + list[index] = artist; + index++; + } + + return list; + } + } + + [IgnoreDataMember] + public string AlbumArtist + { + get { return AlbumArtists.Length == 0 ? null : AlbumArtists[0]; } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return false; } + } + + /// <summary> + /// Gets the tracks. + /// </summary> + /// <value>The tracks.</value> + [IgnoreDataMember] + public IEnumerable<BaseItem> Tracks + { + get + { + return GetRecursiveChildren(i => i is Audio); + } + } + + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + return Tracks; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var albumArtist = AlbumArtist; + if (!string.IsNullOrEmpty(albumArtist)) + { + list.Insert(0, albumArtist + "-" + Name); + } + + var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum); + + if (!string.IsNullOrEmpty(id)) + { + list.Insert(0, "MusicAlbum-Musicbrainz-" + id); + } + + id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + + if (!string.IsNullOrEmpty(id)) + { + list.Insert(0, "MusicAlbum-MusicBrainzReleaseGroup-" + id); + } + + return list; + } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Music); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + + public AlbumInfo GetLookupInfo() + { + var id = GetItemLookupInfo<AlbumInfo>(); + + id.AlbumArtists = AlbumArtists; + + var artist = GetMusicArtist(new DtoOptions(false)); + + if (artist != null) + { + id.ArtistProviderIds = artist.ProviderIds; + } + + id.SongInfos = GetRecursiveChildren(i => i is Audio) + .Cast<Audio>() + .Select(i => i.GetLookupInfo()) + .ToList(); + + var album = id.SongInfos + .Select(i => i.Album) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + + if (!string.IsNullOrEmpty(album)) + { + id.Name = album; + } + + return id; + } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + var items = GetRecursiveChildren(); + + var totalItems = items.Count; + var numComplete = 0; + + var childUpdateType = ItemUpdateType.None; + + // Refresh songs + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var updateType = await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + childUpdateType = childUpdateType | updateType; + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 95); + } + + var parentRefreshOptions = refreshOptions; + if (childUpdateType > ItemUpdateType.None) + { + parentRefreshOptions = new MetadataRefreshOptions(refreshOptions); + parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh; + } + + // Refresh current item + await RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false); + + if (!refreshOptions.IsAutomated) + { + await RefreshArtists(refreshOptions, cancellationToken).ConfigureAwait(false); + } + } + + private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + { + var all = AllArtists; + foreach (var i in all) + { + // This should not be necessary but we're seeing some cases of it + if (string.IsNullOrEmpty(i)) + { + continue; + } + + var artist = LibraryManager.GetArtist(i); + + if (!artist.IsAccessedByName) + { + continue; + } + + await artist.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs new file mode 100644 index 000000000..82dece84b --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -0,0 +1,275 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class MusicArtist + /// </summary> + public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> + { + [IgnoreDataMember] + public bool IsAccessedByName + { + get { return ParentId.Equals(Guid.Empty); } + } + + [IgnoreDataMember] + public override bool IsFolder + { + get + { + return !IsAccessedByName; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsCumulativeRunTimeTicks + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool CanDelete() + { + return !IsAccessedByName; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name }; + query.ArtistIds = new[] { Id }; + } + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override IEnumerable<BaseItem> Children + { + get + { + if (IsAccessedByName) + { + return new List<BaseItem>(); + } + + return base.Children; + } + } + + public override int GetChildCount(User user) + { + if (IsAccessedByName) + { + return 0; + } + return base.GetChildCount(user); + } + + public override bool IsSaveLocalMetadataEnabled() + { + if (IsAccessedByName) + { + return true; + } + + return base.IsSaveLocalMetadataEnabled(); + } + + private readonly Task _cachedTask = Task.FromResult(true); + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + if (IsAccessedByName) + { + // Should never get in here anyway + return _cachedTask; + } + + return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.InsertRange(0, GetUserDataKeys(this)); + return list; + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets the user data key. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + private static List<string> GetUserDataKeys(MusicArtist item) + { + var list = new List<string>(); + var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); + + if (!string.IsNullOrEmpty(id)) + { + list.Add("Artist-Musicbrainz-" + id); + } + + list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); + } + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Music); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + + public ArtistInfo GetLookupInfo() + { + var info = GetItemLookupInfo<ArtistInfo>(); + + info.SongInfos = GetRecursiveChildren(i => i is Audio) + .Cast<Audio>() + .Select(i => i.GetLookupInfo()) + .ToList(); + + return info; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs new file mode 100644 index 000000000..d60ce83ad --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// <summary> + /// Class MusicGenre + /// </summary> + public class MusicGenre : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.GenreIds = new[] { Id }; + query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + + return LibraryManager.GetItemList(query); + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs new file mode 100644 index 000000000..679facf64 --- /dev/null +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -0,0 +1,69 @@ +using System; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> + { + [IgnoreDataMember] + public override bool SupportsPositionTicksResume + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + [IgnoreDataMember] + public string SeriesName { get; set; } + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public string FindSeriesSortName() + { + return SeriesName; + } + public string FindSeriesName() + { + return SeriesName; + } + public string FindSeriesPresentationUniqueKey() + { + return SeriesPresentationUniqueKey; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 0; + } + + public Guid FindSeriesId() + { + return SeriesId; + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Book; + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs new file mode 100644 index 000000000..053ee1b96 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -0,0 +1,2960 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class BaseItem + /// </summary> + public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo> + { + protected static MetadataFields[] EmptyMetadataFieldsArray = new MetadataFields[] { }; + protected static MediaUrl[] EmptyMediaUrlArray = new MediaUrl[] { }; + protected static ItemImageInfo[] EmptyItemImageInfoArray = new ItemImageInfo[] { }; + public static readonly LinkedChild[] EmptyLinkedChildArray = new LinkedChild[] { }; + + protected BaseItem() + { + ThemeSongIds = new Guid[] {}; + ThemeVideoIds = new Guid[] {}; + Tags = new string[] {}; + Genres = new string[] {}; + Studios = new string[] {}; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + LockedFields = EmptyMetadataFieldsArray; + ImageInfos = EmptyItemImageInfoArray; + ProductionLocations = new string[] {}; + RemoteTrailers = new MediaUrl[] { }; + ExtraIds = new Guid[] {}; + } + + public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; + public static char SlugChar = '-'; + + /// <summary> + /// The supported image extensions + /// </summary> + public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList(); + + /// <summary> + /// The trailer folder name + /// </summary> + public static string TrailerFolderName = "trailers"; + public static string ThemeSongsFolderName = "theme-music"; + public static string ThemeSongFilename = "theme"; + public static string ThemeVideosFolderName = "backdrops"; + + [IgnoreDataMember] + public Guid[] ThemeSongIds { get; set; } + [IgnoreDataMember] + public Guid[] ThemeVideoIds { get; set; } + + [IgnoreDataMember] + public string PreferredMetadataCountryCode { get; set; } + [IgnoreDataMember] + public string PreferredMetadataLanguage { get; set; } + + public long? Size { get; set; } + public string Container { get; set; } + + [IgnoreDataMember] + public string Tagline { get; set; } + + [IgnoreDataMember] + public virtual ItemImageInfo[] ImageInfos { get; set; } + + [IgnoreDataMember] + public bool IsVirtualItem { get; set; } + + /// <summary> + /// Gets or sets the album. + /// </summary> + /// <value>The album.</value> + [IgnoreDataMember] + public string Album { get; set; } + + /// <summary> + /// Gets or sets the channel identifier. + /// </summary> + /// <value>The channel identifier.</value> + [IgnoreDataMember] + public Guid ChannelId { get; set; } + + [IgnoreDataMember] + public virtual bool SupportsAddingToPlaylist + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool AlwaysScanInternalMetadataPath + { + get { return false; } + } + + /// <summary> + /// Gets a value indicating whether this instance is in mixed folder. + /// </summary> + /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsInMixedFolder { get; set; } + + [IgnoreDataMember] + public virtual bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsPositionTicksResume + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsRemoteImageDownloading + { + get + { + return true; + } + } + + private string _name; + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + [IgnoreDataMember] + public virtual string Name + { + get + { + return _name; + } + set + { + _name = value; + + // lazy load this again + _sortName = null; + } + } + + [IgnoreDataMember] + public bool IsUnaired + { + get { return PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; } + } + + [IgnoreDataMember] + public int? TotalBitrate { get; set; } + [IgnoreDataMember] + public ExtraType? ExtraType { get; set; } + + [IgnoreDataMember] + public bool IsThemeMedia + { + get + { + return ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); + } + } + + [IgnoreDataMember] + public string OriginalTitle { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [IgnoreDataMember] + public Guid Id { get; set; } + + [IgnoreDataMember] + public Guid OwnerId { get; set; } + + /// <summary> + /// Gets or sets the audio. + /// </summary> + /// <value>The audio.</value> + [IgnoreDataMember] + public ProgramAudio? Audio { get; set; } + + /// <summary> + /// Return the id that should be used to key display prefs for this item. + /// Default is based on the type for everything except actual generic folders. + /// </summary> + /// <value>The display prefs id.</value> + [IgnoreDataMember] + public virtual Guid DisplayPreferencesId + { + get + { + var thisType = GetType(); + return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5(); + } + } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + [IgnoreDataMember] + public virtual string Path { get; set; } + + [IgnoreDataMember] + public virtual SourceType SourceType + { + get + { + if (!ChannelId.Equals(Guid.Empty)) + { + return SourceType.Channel; + } + + return SourceType.Library; + } + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + [IgnoreDataMember] + public virtual string ContainingFolderPath + { + get + { + if (IsFolder) + { + return Path; + } + + return FileSystem.GetDirectoryName(Path); + } + } + + /// <summary> + /// Gets or sets the name of the service. + /// </summary> + /// <value>The name of the service.</value> + [IgnoreDataMember] + public string ServiceName { get; set; } + + /// <summary> + /// If this content came from an external service, the id of the content on that service + /// </summary> + [IgnoreDataMember] + public string ExternalId { get; set; } + + [IgnoreDataMember] + public string ExternalSeriesId { get; set; } + + /// <summary> + /// Gets or sets the etag. + /// </summary> + /// <value>The etag.</value> + [IgnoreDataMember] + public string ExternalEtag { get; set; } + + [IgnoreDataMember] + public virtual bool IsHidden + { + get + { + return false; + } + } + + public BaseItem GetOwner() + { + var ownerId = OwnerId; + return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); + } + + /// <summary> + /// Gets or sets the type of the location. + /// </summary> + /// <value>The type of the location.</value> + [IgnoreDataMember] + public virtual LocationType LocationType + { + get + { + //if (IsOffline) + //{ + // return LocationType.Offline; + //} + + var path = Path; + if (string.IsNullOrEmpty(path)) + { + if (SourceType == SourceType.Channel) + { + return LocationType.Remote; + } + + return LocationType.Virtual; + } + + return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote; + } + } + + [IgnoreDataMember] + public MediaProtocol? PathProtocol + { + get + { + var path = Path; + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + return MediaSourceManager.GetPathProtocol(path); + } + } + + public bool IsPathProtocol(MediaProtocol protocol) + { + var current = PathProtocol; + + return current.HasValue && current.Value == protocol; + } + + [IgnoreDataMember] + public bool IsFileProtocol + { + get + { + return IsPathProtocol(MediaProtocol.File); + } + } + + [IgnoreDataMember] + public bool HasPathProtocol + { + get + { + return PathProtocol.HasValue; + } + } + + [IgnoreDataMember] + public virtual bool SupportsLocalMetadata + { + get + { + if (SourceType == SourceType.Channel) + { + return false; + } + + return IsFileProtocol; + } + } + + [IgnoreDataMember] + public virtual string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileNameWithoutExtension(Path); + } + + return null; + } + } + + [IgnoreDataMember] + public virtual bool EnableAlphaNumericSorting + { + get + { + return true; + } + } + + private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) + { + var list = new List<Tuple<StringBuilder, bool>>(); + + int thisMarker = 0, thisNumericChunk = 0; + + while (thisMarker < s1.Length) + { + if (thisMarker >= s1.Length) + { + break; + } + char thisCh = s1[thisMarker]; + + StringBuilder thisChunk = new StringBuilder(); + + while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) + { + thisChunk.Append(thisCh); + thisMarker++; + + if (thisMarker < s1.Length) + { + thisCh = s1[thisMarker]; + } + } + + var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); + list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); + } + + return list; + } + + /// <summary> + /// This is just a helper for convenience + /// </summary> + /// <value>The primary image path.</value> + [IgnoreDataMember] + public string PrimaryImagePath + { + get { return this.GetImagePath(ImageType.Primary); } + } + + public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name) + { + if (SourceType == SourceType.Channel) + { + // hack alert + return !EnableMediaSourceDisplay; + } + + var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); + if (typeOptions != null) + { + return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + if (!libraryOptions.EnableInternetProviders) + { + return false; + } + + var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + + return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name) + { + if (this is Channel) + { + // hack alert + return true; + } + if (SourceType == SourceType.Channel) + { + // hack alert + return !EnableMediaSourceDisplay; + } + + var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); + if (typeOptions != null) + { + return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + if (!libraryOptions.EnableInternetProviders) + { + return false; + } + + var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); + + return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + public virtual bool CanDelete() + { + if (SourceType == SourceType.Channel) + { + return ChannelManager.CanDelete(this); + } + + return IsFileProtocol; + } + + public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) + { + if (user.Policy.EnableContentDeletion) + { + return true; + } + + var allowed = user.Policy.EnableContentDeletionFromFolders; + + if (SourceType == SourceType.Channel) + { + return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase); + } + else + { + var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); + + foreach (var folder in collectionFolders) + { + if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + public bool CanDelete(User user, List<Folder> allCollectionFolders) + { + return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); + } + + public bool CanDelete(User user) + { + var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); + + return CanDelete(user, allCollectionFolders); + } + + public virtual bool CanDownload() + { + return false; + } + + public virtual bool IsAuthorizedToDownload(User user) + { + return user.Policy.EnableContentDownloading; + } + + public bool CanDownload(User user) + { + return CanDownload() && IsAuthorizedToDownload(user); + } + + /// <summary> + /// Gets or sets the date created. + /// </summary> + /// <value>The date created.</value> + [IgnoreDataMember] + public DateTime DateCreated { get; set; } + + /// <summary> + /// Gets or sets the date modified. + /// </summary> + /// <value>The date modified.</value> + [IgnoreDataMember] + public DateTime DateModified { get; set; } + + [IgnoreDataMember] + public DateTime DateLastSaved { get; set; } + + [IgnoreDataMember] + public DateTime DateLastRefreshed { get; set; } + + /// <summary> + /// The logger + /// </summary> + public static ILogger Logger { get; set; } + public static ILibraryManager LibraryManager { get; set; } + public static IServerConfigurationManager ConfigurationManager { get; set; } + public static IProviderManager ProviderManager { get; set; } + public static ILocalizationManager LocalizationManager { get; set; } + public static IItemRepository ItemRepository { get; set; } + public static IFileSystem FileSystem { get; set; } + public static IUserDataManager UserDataManager { get; set; } + public static IChannelManager ChannelManager { get; set; } + public static IMediaSourceManager MediaSourceManager { get; set; } + + /// <summary> + /// Returns a <see cref="System.String" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="System.String" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + [IgnoreDataMember] + public bool IsLocked { get; set; } + + /// <summary> + /// Gets or sets the locked fields. + /// </summary> + /// <value>The locked fields.</value> + [IgnoreDataMember] + public MetadataFields[] LockedFields { get; set; } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public virtual string MediaType + { + get + { + return null; + } + } + + [IgnoreDataMember] + public virtual string[] PhysicalLocations + { + get + { + if (!IsFileProtocol) + { + return new string[] { }; + } + + return new[] { Path }; + } + } + + private string _forcedSortName; + /// <summary> + /// Gets or sets the name of the forced sort. + /// </summary> + /// <value>The name of the forced sort.</value> + [IgnoreDataMember] + public string ForcedSortName + { + get { return _forcedSortName; } + set { _forcedSortName = value; _sortName = null; } + } + + private string _sortName; + /// <summary> + /// Gets the name of the sort. + /// </summary> + /// <value>The name of the sort.</value> + [IgnoreDataMember] + public string SortName + { + get + { + if (_sortName == null) + { + if (!string.IsNullOrEmpty(ForcedSortName)) + { + // Need the ToLower because that's what CreateSortName does + _sortName = ModifySortChunks(ForcedSortName).ToLower(); + } + else + { + _sortName = CreateSortName(); + } + } + return _sortName; + } + set + { + _sortName = value; + } + } + + public string GetInternalMetadataPath() + { + var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; + + return GetInternalMetadataPath(basePath); + } + + protected virtual string GetInternalMetadataPath(string basePath) + { + if (SourceType == SourceType.Channel) + { + return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N")); + } + + var idString = Id.ToString("N"); + + basePath = System.IO.Path.Combine(basePath, "library"); + + return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected virtual string CreateSortName() + { + if (Name == null) return null; //some items may not have name filled in properly + + if (!EnableAlphaNumericSorting) + { + return Name.TrimStart(); + } + + var sortable = Name.Trim().ToLower(); + + foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) + { + sortable = sortable.Replace(removeChar, string.Empty); + } + + foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) + { + sortable = sortable.Replace(replaceChar, " "); + } + + foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) + { + // Remove from beginning if a space follows + if (sortable.StartsWith(search + " ")) + { + sortable = sortable.Remove(0, search.Length + 1); + } + // Remove from middle if surrounded by spaces + sortable = sortable.Replace(" " + search + " ", " "); + + // Remove from end if followed by a space + if (sortable.EndsWith(" " + search)) + { + sortable = sortable.Remove(sortable.Length - (search.Length + 1)); + } + } + + return ModifySortChunks(sortable); + } + + private string ModifySortChunks(string name) + { + var chunks = GetSortChunks(name); + + var builder = new StringBuilder(); + + foreach (var chunk in chunks) + { + var chunkBuilder = chunk.Item1; + + // This chunk is numeric + if (chunk.Item2) + { + while (chunkBuilder.Length < 10) + { + chunkBuilder.Insert(0, '0'); + } + } + + builder.Append(chunkBuilder); + } + //Logger.Debug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); + return builder.ToString().RemoveDiacritics(); + } + + [IgnoreDataMember] + public bool EnableMediaSourceDisplay + { + get + { + if (SourceType == SourceType.Channel) + { + return ChannelManager.EnableMediaSourceDisplay(this); + } + + return true; + } + } + + [IgnoreDataMember] + public Guid ParentId { get; set; } + + /// <summary> + /// Gets or sets the parent. + /// </summary> + /// <value>The parent.</value> + [IgnoreDataMember] + public Folder Parent + { + get { return GetParent() as Folder; } + set + { + + } + } + + public void SetParent(Folder parent) + { + ParentId = parent == null ? Guid.Empty : parent.Id; + } + + public BaseItem GetParent() + { + var parentId = ParentId; + if (!parentId.Equals(Guid.Empty)) + { + return LibraryManager.GetItemById(parentId); + } + + return null; + } + + public IEnumerable<BaseItem> GetParents() + { + var parent = GetParent(); + + while (parent != null) + { + yield return parent; + + parent = parent.GetParent(); + } + } + + /// <summary> + /// Finds a parent of a given type + /// </summary> + /// <typeparam name="T"></typeparam> + /// <returns>``0.</returns> + public T FindParent<T>() + where T : Folder + { + foreach (var parent in GetParents()) + { + var item = parent as T; + if (item != null) + { + return item; + } + } + return null; + } + + [IgnoreDataMember] + public virtual Guid DisplayParentId + { + get + { + var parentId = ParentId; + return parentId; + } + } + + [IgnoreDataMember] + public BaseItem DisplayParent + { + get + { + var id = DisplayParentId; + if (id.Equals(Guid.Empty)) + { + return null; + } + return LibraryManager.GetItemById(id); + } + } + + /// <summary> + /// When the item first debuted. For movies this could be premiere date, episodes would be first aired + /// </summary> + /// <value>The premiere date.</value> + [IgnoreDataMember] + public DateTime? PremiereDate { get; set; } + + /// <summary> + /// Gets or sets the end date. + /// </summary> + /// <value>The end date.</value> + [IgnoreDataMember] + public DateTime? EndDate { get; set; } + + /// <summary> + /// Gets or sets the official rating. + /// </summary> + /// <value>The official rating.</value> + [IgnoreDataMember] + public string OfficialRating { get; set; } + + [IgnoreDataMember] + public int InheritedParentalRatingValue { get; set; } + + /// <summary> + /// Gets or sets the critic rating. + /// </summary> + /// <value>The critic rating.</value> + [IgnoreDataMember] + public float? CriticRating { get; set; } + + /// <summary> + /// Gets or sets the custom rating. + /// </summary> + /// <value>The custom rating.</value> + [IgnoreDataMember] + public string CustomRating { get; set; } + + /// <summary> + /// Gets or sets the overview. + /// </summary> + /// <value>The overview.</value> + [IgnoreDataMember] + public string Overview { get; set; } + + /// <summary> + /// Gets or sets the studios. + /// </summary> + /// <value>The studios.</value> + [IgnoreDataMember] + public string[] Studios { get; set; } + + /// <summary> + /// Gets or sets the genres. + /// </summary> + /// <value>The genres.</value> + [IgnoreDataMember] + public string[] Genres { get; set; } + + /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + [IgnoreDataMember] + public string[] Tags { get; set; } + + [IgnoreDataMember] + public string[] ProductionLocations { get; set; } + + /// <summary> + /// Gets or sets the home page URL. + /// </summary> + /// <value>The home page URL.</value> + [IgnoreDataMember] + public string HomePageUrl { get; set; } + + /// <summary> + /// Gets or sets the community rating. + /// </summary> + /// <value>The community rating.</value> + [IgnoreDataMember] + public float? CommunityRating { get; set; } + + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + [IgnoreDataMember] + public long? RunTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the production year. + /// </summary> + /// <value>The production year.</value> + [IgnoreDataMember] + public int? ProductionYear { get; set; } + + /// <summary> + /// If the item is part of a series, this is it's number in the series. + /// This could be episode number, album track number, etc. + /// </summary> + /// <value>The index number.</value> + [IgnoreDataMember] + public int? IndexNumber { get; set; } + + /// <summary> + /// For an episode this could be the season number, or for a song this could be the disc number. + /// </summary> + /// <value>The parent index number.</value> + [IgnoreDataMember] + public int? ParentIndexNumber { get; set; } + + [IgnoreDataMember] + public virtual bool HasLocalAlternateVersions + { + get { return false; } + } + + [IgnoreDataMember] + public string OfficialRatingForComparison + { + get + { + var officialRating = OfficialRating; + if (!string.IsNullOrEmpty(officialRating)) + { + return officialRating; + } + + var parent = DisplayParent; + if (parent != null) + { + return parent.OfficialRatingForComparison; + } + + return null; + } + } + + [IgnoreDataMember] + public string CustomRatingForComparison + { + get + { + var customRating = CustomRating; + if (!string.IsNullOrEmpty(customRating)) + { + return customRating; + } + + var parent = DisplayParent; + if (parent != null) + { + return parent.CustomRatingForComparison; + } + + return null; + } + } + + /// <summary> + /// Gets the play access. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>PlayAccess.</returns> + public PlayAccess GetPlayAccess(User user) + { + if (!user.Policy.EnableMediaPlayback) + { + return PlayAccess.None; + } + + //if (!user.IsParentalScheduleAllowed()) + //{ + // return PlayAccess.None; + //} + + return PlayAccess.Full; + } + + public virtual List<MediaStream> GetMediaStreams() + { + return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + { + ItemId = Id + }); + } + + protected virtual bool IsActiveRecording() + { + return false; + } + + public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + { + if (SourceType == SourceType.Channel) + { + var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None) + .ToList(); + + if (sources.Count > 0) + { + return sources; + } + } + + var list = GetAllItemsForMediaSources(); + var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item1, i.Item2)).ToList(); + + if (IsActiveRecording()) + { + foreach (var mediaSource in result) + { + mediaSource.Type = MediaSourceType.Placeholder; + } + } + + return result.OrderBy(i => + { + if (i.VideoType == VideoType.VideoFile) + { + return 0; + } + + return 1; + + }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) + .ThenByDescending(i => + { + var stream = i.VideoStream; + + return stream == null || stream.Width == null ? 0 : stream.Width.Value; + }) + .ToList(); + } + + protected virtual List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources() + { + return new List<Tuple<BaseItem, MediaSourceType>>(); + } + + private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type) + { + if (item == null) + { + throw new ArgumentNullException("media"); + } + + var protocol = item.PathProtocol; + + var info = new MediaSourceInfo + { + Id = item.Id.ToString("N"), + Protocol = protocol ?? MediaProtocol.File, + MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), + Name = GetMediaSourceName(item), + Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + RunTimeTicks = item.RunTimeTicks, + Container = item.Container, + Size = item.Size, + Type = type + }; + + if (string.IsNullOrEmpty(info.Path)) + { + info.Type = MediaSourceType.Placeholder; + } + + if (info.Protocol == MediaProtocol.File) + { + info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + } + + var video = item as Video; + if (video != null) + { + info.IsoType = video.IsoType; + info.VideoType = video.VideoType; + info.Video3DFormat = video.Video3DFormat; + info.Timestamp = video.Timestamp; + + if (video.IsShortcut) + { + info.IsRemote = true; + info.Path = video.ShortcutPath; + info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); + } + + if (string.IsNullOrEmpty(info.Container)) + { + if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) + { + if (protocol.HasValue && protocol.Value == MediaProtocol.File) + { + info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); + } + } + } + } + + if (string.IsNullOrEmpty(info.Container)) + { + if (protocol.HasValue && protocol.Value == MediaProtocol.File) + { + info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); + } + } + + if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path)) + { + info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol); + } + + if (video != null && video.VideoType != VideoType.VideoFile) + { + info.SupportsDirectStream = false; + } + + info.Bitrate = item.TotalBitrate; + info.InferTotalBitrate(); + + return info; + } + + private string GetMediaSourceName(BaseItem item) + { + var terms = new List<string>(); + + var path = item.Path; + if (item.IsFileProtocol && !string.IsNullOrEmpty(path)) + { + if (HasLocalAlternateVersions) + { + var displayName = System.IO.Path.GetFileNameWithoutExtension(path) + .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) + .TrimStart(new char[] { ' ', '-' }); + + if (!string.IsNullOrEmpty(displayName)) + { + terms.Add(displayName); + } + } + + if (terms.Count == 0) + { + var displayName = System.IO.Path.GetFileNameWithoutExtension(path); + terms.Add(displayName); + } + } + + if (terms.Count == 0) + { + terms.Add(item.Name); + } + + var video = item as Video; + if (video != null) + { + if (video.Video3DFormat.HasValue) + { + terms.Add("3D"); + } + + if (video.VideoType == VideoType.BluRay) + { + terms.Add("Bluray"); + } + else if (video.VideoType == VideoType.Dvd) + { + terms.Add("DVD"); + } + else if (video.VideoType == VideoType.Iso) + { + if (video.IsoType.HasValue) + { + if (video.IsoType.Value == Model.Entities.IsoType.BluRay) + { + terms.Add("Bluray"); + } + else if (video.IsoType.Value == Model.Entities.IsoType.Dvd) + { + terms.Add("DVD"); + } + } + else + { + terms.Add("ISO"); + } + } + } + + return string.Join("/", terms.ToArray(terms.Count)); + } + + /// <summary> + /// Loads the theme songs. + /// </summary> + /// <returns>List{Audio.Audio}.</returns> + private static Audio.Audio[] LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + { + var files = fileSystemChildren.Where(i => i.IsDirectory) + .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => FileSystem.GetFiles(i.FullName)) + .ToList(); + + // Support plex/xbmc convention + files.AddRange(fileSystemChildren + .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) + ); + + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + .OfType<Audio.Audio>() + .Select(audio => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio; + + if (dbItem != null) + { + audio = dbItem; + } + else + { + // item is new + audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong; + } + + return audio; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToArray(); + } + + /// <summary> + /// Loads the video backdrops. + /// </summary> + /// <returns>List{Video}.</returns> + private static Video[] LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + { + var files = fileSystemChildren.Where(i => i.IsDirectory) + .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => FileSystem.GetFiles(i.FullName)); + + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + .OfType<Video>() + .Select(item => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(item.Id) as Video; + + if (dbItem != null) + { + item = dbItem; + } + else + { + // item is new + item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; + } + + return item; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToArray(); + } + + public Task RefreshMetadata(CancellationToken cancellationToken) + { + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); + } + + protected virtual void TriggerOnRefreshStart() + { + + } + + protected virtual void TriggerOnRefreshComplete() + { + + } + + /// <summary> + /// Overrides the base implementation to refresh metadata for local trailers + /// </summary> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>true if a provider reports we changed</returns> + public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) + { + TriggerOnRefreshStart(); + + var requiresSave = false; + + if (SupportsOwnedItems) + { + try + { + var files = IsFileProtocol ? + GetFileSystemChildren(options.DirectoryService).ToList() : + new List<FileSystemMetadata>(); + + var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); + + if (ownedItemsChanged) + { + requiresSave = true; + } + } + catch (Exception ex) + { + Logger.ErrorException("Error refreshing owned items for {0}", ex, Path ?? Name); + } + } + + try + { + var refreshOptions = requiresSave + ? new MetadataRefreshOptions(options) + { + ForceSave = true + } + : options; + + return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); + } + finally + { + TriggerOnRefreshComplete(); + } + } + + [IgnoreDataMember] + protected virtual bool SupportsOwnedItems + { + get { return !ParentId.Equals(Guid.Empty) && IsFileProtocol; } + } + + [IgnoreDataMember] + public virtual bool SupportsPeople + { + get { return false; } + } + + [IgnoreDataMember] + public virtual bool SupportsThemeMedia + { + get { return false; } + } + + /// <summary> + /// Refreshes owned items such as trailers, theme videos, special features, etc. + /// Returns true or false indicating if changes were found. + /// </summary> + /// <param name="options"></param> + /// <param name="fileSystemChildren"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var themeSongsChanged = false; + + var themeVideosChanged = false; + + var localTrailersChanged = false; + + if (IsFileProtocol && SupportsOwnedItems) + { + if (SupportsThemeMedia) + { + if (!IsInMixedFolder) + { + themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + } + } + + var hasTrailers = this as IHasTrailers; + if (hasTrailers != null) + { + localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + } + } + + return themeSongsChanged || themeVideosChanged || localTrailersChanged; + } + + protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) + { + var path = ContainingFolderPath; + + return directoryService.GetFileSystemEntries(path); + } + + private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService).ToList(); + + var newItemIds = newItems.Select(i => i.Id).ToArray(); + + var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds); + var ownerId = item.Id; + + var tasks = newItems.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (!i.ExtraType.HasValue || + i.ExtraType.Value != Model.Entities.ExtraType.Trailer || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.ExtraType = Model.Entities.ExtraType.Trailer; + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.LocalTrailerIds = newItemIds; + + return itemsChanged; + } + + private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); + + var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray(newThemeVideos.Length); + + var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds); + + var ownerId = item.Id; + + var tasks = newThemeVideos.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (!i.ExtraType.HasValue || + i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.ExtraType = Model.Entities.ExtraType.ThemeVideo; + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.ThemeVideoIds = newThemeVideoIds; + + return themeVideosChanged; + } + + /// <summary> + /// Refreshes the theme songs. + /// </summary> + private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService); + var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray(newThemeSongs.Length); + + var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds); + + var ownerId = item.Id; + + var tasks = newThemeSongs.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (!i.ExtraType.HasValue || + i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong || + i.OwnerId != ownerId || + !i.ParentId.Equals(Guid.Empty)) + { + i.ExtraType = Model.Entities.ExtraType.ThemeSong; + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + item.ThemeSongIds = newThemeSongIds; + + return themeSongsChanged; + } + + /// <summary> + /// Gets or sets the provider ids. + /// </summary> + /// <value>The provider ids.</value> + [IgnoreDataMember] + public Dictionary<string, string> ProviderIds { get; set; } + + [IgnoreDataMember] + public virtual Folder LatestItemsIndexContainer + { + get { return null; } + } + + public virtual double GetDefaultPrimaryImageAspectRatio() + { + return 0; + } + + public virtual string CreatePresentationUniqueKey() + { + return Id.ToString("N"); + } + + [IgnoreDataMember] + public string PresentationUniqueKey { get; set; } + + public string GetPresentationUniqueKey() + { + return PresentationUniqueKey ?? CreatePresentationUniqueKey(); + } + + public virtual bool RequiresRefresh() + { + return false; + } + + public virtual List<string> GetUserDataKeys() + { + var list = new List<string>(); + + if (SourceType == SourceType.Channel) + { + if (!string.IsNullOrEmpty(ExternalId)) + { + list.Add(ExternalId); + } + } + + list.Add(Id.ToString()); + return list; + } + + internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) + { + var updateType = ItemUpdateType.None; + + if (IsInMixedFolder != newItem.IsInMixedFolder) + { + IsInMixedFolder = newItem.IsInMixedFolder; + updateType |= ItemUpdateType.MetadataImport; + } + + return updateType; + } + + public void AfterMetadataRefresh() + { + _sortName = null; + } + + /// <summary> + /// Gets the preferred metadata language. + /// </summary> + /// <returns>System.String.</returns> + public string GetPreferredMetadataLanguage() + { + string lang = PreferredMetadataLanguage; + + if (string.IsNullOrEmpty(lang)) + { + lang = GetParents() + .Select(i => i.PreferredMetadataLanguage) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetCollectionFolders(this) + .Select(i => i.PreferredMetadataLanguage) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage; + } + + if (string.IsNullOrEmpty(lang)) + { + lang = ConfigurationManager.Configuration.PreferredMetadataLanguage; + } + + return lang; + } + + /// <summary> + /// Gets the preferred metadata language. + /// </summary> + /// <returns>System.String.</returns> + public string GetPreferredMetadataCountryCode() + { + string lang = PreferredMetadataCountryCode; + + if (string.IsNullOrEmpty(lang)) + { + lang = GetParents() + .Select(i => i.PreferredMetadataCountryCode) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetCollectionFolders(this) + .Select(i => i.PreferredMetadataCountryCode) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode; + } + + if (string.IsNullOrEmpty(lang)) + { + lang = ConfigurationManager.Configuration.MetadataCountryCode; + } + + return lang; + } + + public virtual bool IsSaveLocalMetadataEnabled() + { + if (SourceType == SourceType.Channel) + { + return false; + } + + var libraryOptions = LibraryManager.GetLibraryOptions(this); + + return libraryOptions.SaveLocalMetadata; + } + + /// <summary> + /// Determines if a given user has access to this item + /// </summary> + /// <param name="user">The user.</param> + /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + public bool IsParentalAllowed(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (!IsVisibleViaTags(user)) + { + return false; + } + + var maxAllowedRating = user.Policy.MaxParentalRating; + + if (maxAllowedRating == null) + { + return true; + } + + var rating = CustomRatingForComparison; + + if (string.IsNullOrEmpty(rating)) + { + rating = OfficialRatingForComparison; + } + + if (string.IsNullOrEmpty(rating)) + { + return !GetBlockUnratedValue(user.Policy); + } + + var value = LocalizationManager.GetRatingLevel(rating); + + // Could not determine the integer value + if (!value.HasValue) + { + var isAllowed = !GetBlockUnratedValue(user.Policy); + + if (!isAllowed) + { + Logger.Debug("{0} has an unrecognized parental rating of {1}.", Name, rating); + } + + return isAllowed; + } + + return value.Value <= maxAllowedRating.Value; + } + + public int? GetParentalRatingValue() + { + var rating = CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = OfficialRating; + } + + if (string.IsNullOrEmpty(rating)) + { + return null; + } + + return LocalizationManager.GetRatingLevel(rating); + } + + public int? GetInheritedParentalRatingValue() + { + var rating = CustomRatingForComparison; + + if (string.IsNullOrEmpty(rating)) + { + rating = OfficialRatingForComparison; + } + + if (string.IsNullOrEmpty(rating)) + { + return null; + } + + return LocalizationManager.GetRatingLevel(rating); + } + + public List<string> GetInheritedTags() + { + var list = new List<string>(); + list.AddRange(Tags); + + foreach (var parent in GetParents()) + { + list.AddRange(parent.Tags); + } + + return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + } + + private bool IsVisibleViaTags(User user) + { + var policy = user.Policy; + if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + + return true; + } + + protected virtual bool IsAllowTagFilterEnforced() + { + return true; + } + + public virtual UnratedItem GetBlockUnratedType() + { + if (SourceType == SourceType.Channel) + { + return UnratedItem.ChannelContent; + } + + return UnratedItem.Other; + } + + /// <summary> + /// Gets the block unrated value. + /// </summary> + /// <param name="config">The configuration.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + protected virtual bool GetBlockUnratedValue(UserPolicy config) + { + // Don't block plain folders that are unrated. Let the media underneath get blocked + // Special folders like series and albums will override this method. + if (IsFolder) + { + return false; + } + if (this is IItemByName) + { + return false; + } + + return config.BlockUnratedItems.Contains(GetBlockUnratedType()); + } + + /// <summary> + /// Determines if this folder should be visible to a given user. + /// Default is just parental allowed. Can be overridden for more functionality. + /// </summary> + /// <param name="user">The user.</param> + /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + public virtual bool IsVisible(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + return IsParentalAllowed(user); + } + + public virtual bool IsVisibleStandalone(User user) + { + if (SourceType == SourceType.Channel) + { + return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user); + } + + return IsVisibleStandaloneInternal(user, true); + } + + [IgnoreDataMember] + public virtual bool SupportsInheritedParentImages + { + get { return false; } + } + + protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) + { + if (!IsVisible(user)) + { + return false; + } + + if (GetParents().Any(i => !i.IsVisible(user))) + { + return false; + } + + if (checkFolders) + { + var topParent = GetParents().LastOrDefault() ?? this; + + if (string.IsNullOrEmpty(topParent.Path)) + { + return true; + } + + var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList(); + + if (itemCollectionFolders.Count > 0) + { + var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList(); + if (!itemCollectionFolders.Any(userCollectionFolders.Contains)) + { + return false; + } + } + } + + return true; + } + + /// <summary> + /// Gets a value indicating whether this instance is folder. + /// </summary> + /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public virtual bool IsFolder + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool IsDisplayedAsFolder + { + get + { + return false; + } + } + + public virtual string GetClientTypeName() + { + if (IsFolder && SourceType == SourceType.Channel && !(this is Channel)) + { + return "ChannelFolderItem"; + } + + return GetType().Name; + } + + /// <summary> + /// Gets the linked child. + /// </summary> + /// <param name="info">The info.</param> + /// <returns>BaseItem.</returns> + protected BaseItem GetLinkedChild(LinkedChild info) + { + // First get using the cached Id + if (info.ItemId.HasValue) + { + if (info.ItemId.Value.Equals(Guid.Empty)) + { + return null; + } + + var itemById = LibraryManager.GetItemById(info.ItemId.Value); + + if (itemById != null) + { + return itemById; + } + } + + var item = FindLinkedChild(info); + + // If still null, log + if (item == null) + { + // Don't keep searching over and over + info.ItemId = Guid.Empty; + } + else + { + // Cache the id for next time + info.ItemId = item.Id; + } + + return item; + } + + private BaseItem FindLinkedChild(LinkedChild info) + { + var path = info.Path; + + if (!string.IsNullOrEmpty(path)) + { + path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path); + + var itemByPath = LibraryManager.FindByPath(path, null); + + if (itemByPath == null) + { + //Logger.Warn("Unable to find linked item at path {0}", info.Path); + } + + return itemByPath; + } + + if (!string.IsNullOrEmpty(info.LibraryItemId)) + { + var item = LibraryManager.GetItemById(info.LibraryItemId); + + if (item == null) + { + //Logger.Warn("Unable to find linked item at path {0}", info.Path); + } + + return item; + } + + return null; + } + + [IgnoreDataMember] + public virtual bool EnableRememberingTrackSelections + { + get + { + return true; + } + } + + /// <summary> + /// Adds a studio to the item + /// </summary> + /// <param name="name">The name.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddStudio(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + var current = Studios; + + if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + if (current.Length == 0) + { + Studios = new[] { name }; + } + else + { + var list = current.ToArray(current.Length + 1); + list[list.Length - 1] = name; + Studios = list; + } + } + } + + public void SetStudios(IEnumerable<string> names) + { + Studios = names.Distinct().ToArray(); + } + + /// <summary> + /// Adds a genre to the item + /// </summary> + /// <param name="name">The name.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddGenre(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + var genres = Genres; + if (!genres.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + var list = genres.ToList(); + list.Add(name); + Genres = list.ToArray(); + } + } + + /// <summary> + /// Marks the played. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="datePlayed">The date played.</param> + /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public virtual void MarkPlayed(User user, + DateTime? datePlayed, + bool resetPosition) + { + if (user == null) + { + throw new ArgumentNullException(); + } + + var data = UserDataManager.GetUserData(user, this); + + if (datePlayed.HasValue) + { + // Increment + data.PlayCount++; + } + + // Ensure it's at least one + data.PlayCount = Math.Max(data.PlayCount, 1); + + if (resetPosition) + { + data.PlaybackPositionTicks = 0; + } + + data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow; + data.Played = true; + + UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None); + } + + /// <summary> + /// Marks the unplayed. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public virtual void MarkUnplayed(User user) + { + if (user == null) + { + throw new ArgumentNullException(); + } + + var data = UserDataManager.GetUserData(user, this); + + //I think it is okay to do this here. + // if this is only called when a user is manually forcing something to un-played + // then it probably is what we want to do... + data.PlayCount = 0; + data.PlaybackPositionTicks = 0; + data.LastPlayedDate = null; + data.Played = false; + + UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None); + } + + /// <summary> + /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. + /// </summary> + /// <returns>Task.</returns> + public virtual void ChangedExternally() + { + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(FileSystem) + { + + }, RefreshPriority.High); + } + + /// <summary> + /// Gets an image + /// </summary> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> + public bool HasImage(ImageType type, int imageIndex) + { + return GetImageInfo(type, imageIndex) != null; + } + + public void SetImage(ItemImageInfo image, int index) + { + if (image.Type == ImageType.Chapter) + { + throw new ArgumentException("Cannot set chapter images using SetImagePath"); + } + + var existingImage = GetImageInfo(image.Type, index); + + if (existingImage != null) + { + existingImage.Path = image.Path; + existingImage.DateModified = image.DateModified; + existingImage.Width = image.Width; + existingImage.Height = image.Height; + } + + else + { + var currentCount = ImageInfos.Length; + var newList = ImageInfos.ToArray(currentCount + 1); + newList[currentCount] = image; + ImageInfos = newList; + } + } + + public void SetImagePath(ImageType type, int index, FileSystemMetadata file) + { + if (type == ImageType.Chapter) + { + throw new ArgumentException("Cannot set chapter images using SetImagePath"); + } + + var image = GetImageInfo(type, index); + + if (image == null) + { + var currentCount = ImageInfos.Length; + var newList = ImageInfos.ToArray(currentCount + 1); + newList[currentCount] = GetImageInfo(file, type); + ImageInfos = newList; + } + else + { + var imageInfo = GetImageInfo(file, type); + + image.Path = file.FullName; + image.DateModified = imageInfo.DateModified; + + // reset these values + image.Width = 0; + image.Height = 0; + } + } + + /// <summary> + /// Deletes the image. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="index">The index.</param> + /// <returns>Task.</returns> + public void DeleteImage(ImageType type, int index) + { + var info = GetImageInfo(type, index); + + if (info == null) + { + // Nothing to do + return; + } + + // Remove it from the item + RemoveImage(info); + + if (info.IsLocalFile) + { + FileSystem.DeleteFile(info.Path); + } + + UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + } + + public void RemoveImage(ItemImageInfo image) + { + RemoveImages(new List<ItemImageInfo> { image }); + } + + public void RemoveImages(List<ItemImageInfo> deletedImages) + { + ImageInfos = ImageInfos.Except(deletedImages).ToArray(); + } + + public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken); + } + + /// <summary> + /// Validates that images within the item are still on the file system + /// </summary> + public bool ValidateImages(IDirectoryService directoryService) + { + var allFiles = ImageInfos + .Where(i => i.IsLocalFile) + .Select(i => FileSystem.GetDirectoryName(i.Path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .SelectMany(i => directoryService.GetFilePaths(i)) + .ToList(); + + var deletedImages = ImageInfos + .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + if (deletedImages.Count > 0) + { + ImageInfos = ImageInfos.Except(deletedImages).ToArray(); + } + + return deletedImages.Count > 0; + } + + /// <summary> + /// Gets the image path. + /// </summary> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>System.String.</returns> + /// <exception cref="System.InvalidOperationException"> + /// </exception> + /// <exception cref="System.ArgumentNullException">item</exception> + public string GetImagePath(ImageType imageType, int imageIndex) + { + var info = GetImageInfo(imageType, imageIndex); + + return info == null ? null : info.Path; + } + + /// <summary> + /// Gets the image information. + /// </summary> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>ItemImageInfo.</returns> + public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex) + { + if (imageType == ImageType.Chapter) + { + var chapter = ItemRepository.GetChapter(this, imageIndex); + + if (chapter == null) + { + return null; + } + + var path = chapter.ImagePath; + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + return new ItemImageInfo + { + Path = path, + DateModified = chapter.ImageDateModified, + Type = imageType + }; + } + + return GetImages(imageType) + .ElementAtOrDefault(imageIndex); + } + + public IEnumerable<ItemImageInfo> GetImages(ImageType imageType) + { + if (imageType == ImageType.Chapter) + { + throw new ArgumentException("No image info for chapter images"); + } + + return ImageInfos.Where(i => i.Type == imageType); + } + + /// <summary> + /// Adds the images. + /// </summary> + /// <param name="imageType">Type of the image.</param> + /// <param name="images">The images.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception> + public bool AddImages(ImageType imageType, List<FileSystemMetadata> images) + { + if (imageType == ImageType.Chapter) + { + throw new ArgumentException("Cannot call AddImages with chapter images"); + } + + var existingImages = GetImages(imageType) + .ToList(); + + var newImageList = new List<FileSystemMetadata>(); + var imageAdded = false; + var imageUpdated = false; + + foreach (var newImage in images) + { + if (newImage == null) + { + throw new ArgumentException("null image found in list"); + } + + var existing = existingImages + .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); + + if (existing == null) + { + newImageList.Add(newImage); + imageAdded = true; + } + else + { + if (existing.IsLocalFile) + { + var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage); + + // If date changed then we need to reset saved image dimensions + if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0)) + { + existing.Width = 0; + existing.Height = 0; + imageUpdated = true; + } + + existing.DateModified = newDateModified; + } + } + } + + if (imageAdded || images.Count != existingImages.Count) + { + var newImagePaths = images.Select(i => i.FullName).ToList(); + + var deleted = existingImages + .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !FileSystem.FileExists(i.Path)) + .ToList(); + + if (deleted.Count > 0) + { + ImageInfos = ImageInfos.Except(deleted).ToArray(); + } + } + + if (newImageList.Count > 0) + { + var currentCount = ImageInfos.Length; + var newList = ImageInfos.ToArray(currentCount + newImageList.Count); + + foreach (var image in newImageList) + { + newList[currentCount] = GetImageInfo(image, imageType); + currentCount++; + } + + ImageInfos = newList; + } + + return imageUpdated || newImageList.Count > 0; + } + + private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type) + { + return new ItemImageInfo + { + Path = file.FullName, + Type = type, + DateModified = FileSystem.GetLastWriteTimeUtc(file) + }; + } + + /// <summary> + /// Gets the file system path to delete when the item is to be deleted + /// </summary> + /// <returns></returns> + public virtual IEnumerable<FileSystemMetadata> GetDeletePaths() + { + return new[] { + new FileSystemMetadata + { + FullName = Path, + IsDirectory = IsFolder + } + }.Concat(GetLocalMetadataFilesToDelete()); + } + + protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete() + { + if (IsFolder || !IsInMixedFolder) + { + return new List<FileSystemMetadata>(); + } + + var filename = System.IO.Path.GetFileNameWithoutExtension(Path); + var extensions = new List<string> { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; + extensions.AddRange(SupportedImageExtensions); + + return FileSystem.GetFiles(FileSystem.GetDirectoryName(Path), extensions.ToArray(extensions.Count), false, false) + .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + public bool AllowsMultipleImages(ImageType type) + { + return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter; + } + + public void SwapImages(ImageType type, int index1, int index2) + { + if (!AllowsMultipleImages(type)) + { + throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); + } + + var info1 = GetImageInfo(type, index1); + var info2 = GetImageInfo(type, index2); + + if (info1 == null || info2 == null) + { + // Nothing to do + return; + } + + if (!info1.IsLocalFile || !info2.IsLocalFile) + { + // TODO: Not supported yet + return; + } + + var path1 = info1.Path; + var path2 = info2.Path; + + FileSystem.SwapFiles(path1, path2); + + // Refresh these values + info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path); + info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path); + + info1.Width = 0; + info1.Height = 0; + info2.Width = 0; + info2.Height = 0; + + UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + } + + public virtual bool IsPlayed(User user) + { + var userdata = UserDataManager.GetUserData(user, this); + + return userdata != null && userdata.Played; + } + + public bool IsFavoriteOrLiked(User user) + { + var userdata = UserDataManager.GetUserData(user, this); + + return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false)); + } + + public virtual bool IsUnplayed(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var userdata = UserDataManager.GetUserData(user, this); + + return userdata == null || !userdata.Played; + } + + ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo() + { + return GetItemLookupInfo<ItemLookupInfo>(); + } + + protected T GetItemLookupInfo<T>() + where T : ItemLookupInfo, new() + { + return new T + { + MetadataCountryCode = GetPreferredMetadataCountryCode(), + MetadataLanguage = GetPreferredMetadataLanguage(), + Name = GetNameForMetadataLookup(), + ProviderIds = ProviderIds, + IndexNumber = IndexNumber, + ParentIndexNumber = ParentIndexNumber, + Year = ProductionYear, + PremiereDate = PremiereDate + }; + } + + protected virtual string GetNameForMetadataLookup() + { + return Name; + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + _sortName = null; + + var hasChanges = false; + + if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path)) + { + Name = FileSystem.GetFileNameWithoutExtension(Path); + hasChanges = true; + } + + return hasChanges; + } + + protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol) + { + if (protocol.HasValue && protocol.Value == MediaProtocol.File) + { + return LibraryManager.GetPathAfterNetworkSubstitution(path, item); + } + + return path; + } + + public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) + { + if (RunTimeTicks.HasValue) + { + double pct = RunTimeTicks.Value; + + if (pct > 0) + { + pct = userData.PlaybackPositionTicks / pct; + + if (pct > 0) + { + dto.PlayedPercentage = 100 * pct; + } + } + } + } + + protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var newOptions = new MetadataRefreshOptions(options); + newOptions.SearchResult = null; + + var item = this; + + if (copyTitleMetadata) + { + // Take some data from the main item, for querying purposes + if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal)) + { + newOptions.ForceSave = true; + ownedItem.Genres = item.Genres; + } + if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) + { + newOptions.ForceSave = true; + ownedItem.Studios = item.Studios; + } + if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) + { + newOptions.ForceSave = true; + ownedItem.ProductionLocations = item.ProductionLocations; + } + if (item.CommunityRating != ownedItem.CommunityRating) + { + ownedItem.CommunityRating = item.CommunityRating; + newOptions.ForceSave = true; + } + if (item.CriticRating != ownedItem.CriticRating) + { + ownedItem.CriticRating = item.CriticRating; + newOptions.ForceSave = true; + } + if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) + { + ownedItem.Overview = item.Overview; + newOptions.ForceSave = true; + } + if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) + { + ownedItem.OfficialRating = item.OfficialRating; + newOptions.ForceSave = true; + } + if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) + { + ownedItem.CustomRating = item.CustomRating; + newOptions.ForceSave = true; + } + } + + return ownedItem.RefreshMetadata(newOptions, cancellationToken); + } + + protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) + { + var newOptions = new MetadataRefreshOptions(options); + newOptions.SearchResult = null; + + var id = LibraryManager.GetNewItemId(path, typeof(Video)); + + // Try to retrieve it from the db. If we don't find it, use the resolved version + var video = LibraryManager.GetItemById(id) as Video; + + if (video == null) + { + video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; + + newOptions.ForceSave = true; + } + + //var parentId = Id; + //if (!video.IsOwnedItem || video.ParentId != parentId) + //{ + // video.IsOwnedItem = true; + // video.ParentId = parentId; + // newOptions.ForceSave = true; + //} + + if (video == null) + { + return Task.FromResult(true); + } + + return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); + } + + public string GetEtag(User user) + { + var list = GetEtagValues(user); + + return string.Join("|", list.ToArray(list.Count)).GetMD5().ToString("N"); + } + + protected virtual List<string> GetEtagValues(User user) + { + return new List<string> + { + DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) + }; + } + + public virtual IEnumerable<Guid> GetAncestorIds() + { + return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id)); + } + + public BaseItem GetTopParent() + { + if (IsTopParent) + { + return this; + } + + foreach (var parent in GetParents()) + { + if (parent.IsTopParent) + { + return parent; + } + } + return null; + } + + [IgnoreDataMember] + public virtual bool IsTopParent + { + get + { + if (this is BasePluginFolder || this is Channel) + { + return true; + } + + var view = this as IHasCollectionType; + if (view != null) + { + if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + if (GetParent() is AggregateFolder) + { + return true; + } + + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsAncestors + { + get + { + return true; + } + } + + [IgnoreDataMember] + public virtual bool StopRefreshIfLocalMetadataFound + { + get + { + return true; + } + } + + public virtual IEnumerable<Guid> GetIdsForAncestorQuery() + { + return new[] { Id }; + } + + public virtual List<ExternalUrl> GetRelatedUrls() + { + return new List<ExternalUrl>(); + } + + public virtual double? GetRefreshProgress() + { + return null; + } + + public virtual ItemUpdateType OnMetadataChanged() + { + var updateType = ItemUpdateType.None; + + var item = this; + + var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0; + if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) + { + item.InheritedParentalRatingValue = inheritedParentalRatingValue; + updateType |= ItemUpdateType.MetadataImport; + } + + return updateType; + } + + /// <summary> + /// Updates the official rating based on content and returns true or false indicating if it changed. + /// </summary> + /// <returns></returns> + public bool UpdateRatingToItems(IList<BaseItem> children) + { + var currentOfficialRating = OfficialRating; + + // Gather all possible ratings + var ratings = children + .Select(i => i.OfficialRating) + .Where(i => !string.IsNullOrEmpty(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(i => new Tuple<string, int?>(i, LocalizationManager.GetRatingLevel(i))) + .OrderBy(i => i.Item2 ?? 1000) + .Select(i => i.Item1); + + OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; + + return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty, + StringComparison.OrdinalIgnoreCase); + } + + public IEnumerable<BaseItem> GetThemeSongs() + { + return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName); + } + + public IEnumerable<BaseItem> GetThemeVideos() + { + return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + } + + public MediaUrl[] RemoteTrailers { get; set; } + + public IEnumerable<BaseItem> GetExtras() + { + return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + } + + public IEnumerable<BaseItem> GetExtras(ExtraType[] unused) + { + return GetExtras(); + } + + public IEnumerable<BaseItem> GetDisplayExtras() + { + return GetExtras(); + } + + public virtual bool IsHD { + get{ + return Height >= 720; + } + } + public bool IsShortcut{ get; set;} + public string ShortcutPath{ get; set;} + public int Width { get; set; } + public int Height { get; set; } + public Guid[] ExtraIds { get; set; } + public virtual long GetRunTimeTicksForPlayState() { + return RunTimeTicks ?? 0; + } + // what does this do? + public static ExtraType[] DisplayExtraTypes = new[] {Model.Entities.ExtraType.ThemeSong, Model.Entities.ExtraType.ThemeVideo }; + public virtual bool SupportsExternalTransfer { + get { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs new file mode 100644 index 000000000..c56a370a8 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Entities +{ + public static class BaseItemExtensions + { + /// <summary> + /// Gets the image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <returns>System.String.</returns> + public static string GetImagePath(this BaseItem item, ImageType imageType) + { + return item.GetImagePath(imageType, 0); + } + + public static bool HasImage(this BaseItem item, ImageType imageType) + { + return item.HasImage(imageType, 0); + } + + /// <summary> + /// Sets the image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <param name="file">The file.</param> + public static void SetImagePath(this BaseItem item, ImageType imageType, FileSystemMetadata file) + { + item.SetImagePath(imageType, 0, file); + } + + /// <summary> + /// Sets the image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <param name="file">The file.</param> + public static void SetImagePath(this BaseItem item, ImageType imageType, string file) + { + if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase)) + { + item.SetImage(new ItemImageInfo + { + Path = file, + Type = imageType + }, 0); + } + else + { + item.SetImagePath(imageType, BaseItem.FileSystem.GetFileInfo(file)); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs new file mode 100644 index 000000000..c06f1cef4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -0,0 +1,54 @@ + +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Plugins derive from and export this class to create a folder that will appear in the root along + /// with all the other actual physical folders in the system. + /// </summary> + public abstract class BasePluginFolder : Folder, ICollectionFolder + { + [IgnoreDataMember] + public virtual string CollectionType + { + get { return null; } + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + //public override double? GetDefaultPrimaryImageAspectRatio() + //{ + // double value = 16; + // value /= 9; + + // return value; + //} + } +} diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs new file mode 100644 index 000000000..6814570c3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries + { + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Book; + } + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + [IgnoreDataMember] + public string SeriesName { get; set; } + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public string FindSeriesSortName() + { + return SeriesName; + } + public string FindSeriesName() + { + return SeriesName; + } + public string FindSeriesPresentationUniqueKey() + { + return SeriesPresentationUniqueKey; + } + + public Guid FindSeriesId() + { + return SeriesId; + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Book; + } + + public BookInfo GetLookupInfo() + { + var info = GetItemLookupInfo<BookInfo>(); + + if (string.IsNullOrEmpty(SeriesName)) + { + info.SeriesName = GetParents().Select(i => i.Name).FirstOrDefault(); + } + else + { + info.SeriesName = SeriesName; + } + + return info; + } + } +} diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs new file mode 100644 index 000000000..8240a68ff --- /dev/null +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -0,0 +1,405 @@ +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Specialized Folder class that points to a subset of the physical folders in the system. + /// It is created from the user-specific folders within the system root + /// </summary> + public class CollectionFolder : Folder, ICollectionFolder + { + public static IXmlSerializer XmlSerializer { get; set; } + public static IJsonSerializer JsonSerializer { get; set; } + public static IServerApplicationHost ApplicationHost { get; set; } + + public CollectionFolder() + { + PhysicalLocationsList = new string[] { }; + PhysicalFolderIds = new Guid[] { }; + } + + //public override double? GetDefaultPrimaryImageAspectRatio() + //{ + // double value = 16; + // value /= 9; + + // return value; + //} + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + public override bool CanDelete() + { + return false; + } + + public string CollectionType { get; set; } + + private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>(); + public LibraryOptions GetLibraryOptions() + { + return GetLibraryOptions(Path); + } + + private static LibraryOptions LoadLibraryOptions(string path) + { + try + { + var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; + + if (result == null) + { + return new LibraryOptions(); + } + + foreach (var mediaPath in result.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) + { + mediaPath.Path = ApplicationHost.ExpandVirtualPath(mediaPath.Path); + } + } + + return result; + } + catch (FileNotFoundException) + { + return new LibraryOptions(); + } + catch (IOException) + { + return new LibraryOptions(); + } + catch (Exception ex) + { + Logger.ErrorException("Error loading library options", ex); + + return new LibraryOptions(); + } + } + + private static string GetLibraryOptionsPath(string path) + { + return System.IO.Path.Combine(path, "options.xml"); + } + + public void UpdateLibraryOptions(LibraryOptions options) + { + SaveLibraryOptions(Path, options); + } + + public static LibraryOptions GetLibraryOptions(string path) + { + lock (LibraryOptions) + { + LibraryOptions options; + if (!LibraryOptions.TryGetValue(path, out options)) + { + options = LoadLibraryOptions(path); + LibraryOptions[path] = options; + } + + return options; + } + } + + public static void SaveLibraryOptions(string path, LibraryOptions options) + { + lock (LibraryOptions) + { + LibraryOptions[path] = options; + + var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options)); + foreach (var mediaPath in clone.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) + { + mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); + } + } + + XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); + } + } + + public static void OnCollectionFolderChange() + { + lock (LibraryOptions) + { + LibraryOptions.Clear(); + } + } + + /// <summary> + /// Allow different display preferences for each collection folder + /// </summary> + /// <value>The display prefs id.</value> + [IgnoreDataMember] + public override Guid DisplayPreferencesId + { + get + { + return Id; + } + } + + [IgnoreDataMember] + public override string[] PhysicalLocations + { + get + { + return PhysicalLocationsList; + } + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public string[] PhysicalLocationsList { get; set; } + public Guid[] PhysicalFolderIds { get; set; } + + protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) + { + return CreateResolveArgs(directoryService, true).FileSystemChildren; + } + + private bool _requiresRefresh; + public override bool RequiresRefresh() + { + var changed = base.RequiresRefresh() || _requiresRefresh; + + if (!changed) + { + var locations = PhysicalLocations; + + var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + + if (!locations.SequenceEqual(newLocations)) + { + changed = true; + } + } + + if (!changed) + { + var folderIds = PhysicalFolderIds; + + var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + } + } + + return changed; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + _requiresRefresh = false; + return changed; + } + + public override double? GetRefreshProgress() + { + var folders = GetPhysicalFolders(true).ToList(); + double totalProgresses = 0; + var foldersWithProgress = 0; + + foreach (var folder in folders) + { + var progress = ProviderManager.GetRefreshProgress(folder.Id); + if (progress.HasValue) + { + totalProgresses += progress.Value; + foldersWithProgress++; + } + } + + if (foldersWithProgress == 0) + { + return null; + } + + return (totalProgresses / foldersWithProgress); + } + + protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) + { + return RefreshLinkedChildrenInternal(true); + } + + private bool RefreshLinkedChildrenInternal(bool setFolders) + { + var physicalFolders = GetPhysicalFolders(false) + .ToList(); + + var linkedChildren = physicalFolders + .SelectMany(c => c.LinkedChildren) + .ToList(); + + var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer(FileSystem)); + + LinkedChildren = linkedChildren.ToArray(linkedChildren.Count); + + var folderIds = PhysicalFolderIds; + var newFolderIds = physicalFolders.Select(i => i.Id).ToArray(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + if (setFolders) + { + PhysicalFolderIds = newFolderIds; + } + } + + return changed; + } + + private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) + { + var path = ContainingFolderPath; + + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + { + FileInfo = FileSystem.GetDirectoryInfo(path), + Path = path, + Parent = GetParent() as Folder, + CollectionType = CollectionType + }; + + // Gather child folder and files + if (args.IsDirectory) + { + var flattenFolderDepth = 0; + + var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true); + + args.FileSystemChildren = files; + } + + _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations); + + if (setPhysicalLocations) + { + PhysicalLocationsList = args.PhysicalLocations; + } + + return args; + } + + /// <summary> + /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes + /// ***Currently does not contain logic to maintain items that are unavailable in the file system*** + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="recursive">if set to <c>true</c> [recursive].</param> + /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns>Task.</returns> + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + return Task.FromResult(true); + } + + /// <summary> + /// Our children are actually just references to the ones in the physical root... + /// </summary> + /// <value>The actual children.</value> + [IgnoreDataMember] + public override IEnumerable<BaseItem> Children + { + get { return GetActualChildren(); } + } + + public IEnumerable<BaseItem> GetActualChildren() + { + return GetPhysicalFolders(true).SelectMany(c => c.Children); + } + + public IEnumerable<Folder> GetPhysicalFolders() + { + return GetPhysicalFolders(true); + } + + private IEnumerable<Folder> GetPhysicalFolders(bool enableCache) + { + if (enableCache) + { + return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType<Folder>(); + } + + var rootChildren = LibraryManager.RootFolder.Children + .OfType<Folder>() + .ToList(); + + return PhysicalLocations.Where(i => !FileSystem.AreEqual(i, Path)).SelectMany(i => GetPhysicalParents(i, rootChildren)).DistinctBy(i => i.Id); + } + + private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren) + { + var result = rootChildren + .Where(i => FileSystem.AreEqual(i.Path, path)) + .ToList(); + + if (result.Count == 0) + { + var folder = LibraryManager.FindByPath(path, true) as Folder; + + if (folder != null) + { + result.Add(folder); + } + } + + return result; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs new file mode 100644 index 000000000..166ef66d4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs @@ -0,0 +1,71 @@ +using MediaBrowser.Model.Configuration; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + public static class DayOfWeekHelper + { + public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day) + { + return GetDaysOfWeek(new List<DynamicDayOfWeek> { day }); + } + + public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days) + { + var list = new List<DayOfWeek>(); + + if (days.Contains(DynamicDayOfWeek.Sunday) || + days.Contains(DynamicDayOfWeek.Weekend) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Sunday); + } + + if (days.Contains(DynamicDayOfWeek.Saturday) || + days.Contains(DynamicDayOfWeek.Weekend) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Saturday); + } + + if (days.Contains(DynamicDayOfWeek.Monday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Monday); + } + + if (days.Contains(DynamicDayOfWeek.Tuesday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Tuesday + ); + } + + if (days.Contains(DynamicDayOfWeek.Wednesday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Wednesday); + } + + if (days.Contains(DynamicDayOfWeek.Thursday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Thursday); + } + + if (days.Contains(DynamicDayOfWeek.Friday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Friday); + } + + return list; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs new file mode 100644 index 000000000..c706cf36c --- /dev/null +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Linq; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Extensions + /// </summary> + public static class Extensions + { + /// <summary> + /// Adds the trailer URL. + /// </summary> + public static void AddTrailerUrl(this BaseItem item, string url) + { + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentNullException("url"); + } + + var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase)); + + if (current == null) + { + var mediaUrl = new MediaUrl + { + Url = url + }; + + if (item.RemoteTrailers.Length == 0) + { + item.RemoteTrailers = new[] { mediaUrl }; + } + else + { + var list = item.RemoteTrailers.ToArray(item.RemoteTrailers.Length + 1); + list[list.Length - 1] = mediaUrl; + + item.RemoteTrailers = list; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs new file mode 100644 index 000000000..8b9aa5fc3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -0,0 +1,1803 @@ +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Folder + /// </summary> + public class Folder : BaseItem + { + public static IUserManager UserManager { get; set; } + public static IUserViewManager UserViewManager { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is root. + /// </summary> + /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value> + public bool IsRoot { get; set; } + + public LinkedChild[] LinkedChildren { get; set; } + + [IgnoreDataMember] + public DateTime? DateLastMediaAdded { get; set; } + + public Folder() + { + LinkedChildren = EmptyLinkedChildArray; + } + + [IgnoreDataMember] + public override bool SupportsThemeMedia + { + get { return true; } + } + + [IgnoreDataMember] + public virtual bool IsPreSorted + { + get { return false; } + } + + [IgnoreDataMember] + public virtual bool IsPhysicalRoot + { + get { return false; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is folder. + /// </summary> + /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public override bool IsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public virtual bool SupportsCumulativeRunTimeTicks + { + get + { + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsDateLastMediaAdded + { + get + { + return false; + } + } + + public override bool CanDelete() + { + if (IsRoot) + { + return false; + } + + return base.CanDelete(); + } + + public override bool RequiresRefresh() + { + var baseResult = base.RequiresRefresh(); + + if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue) + { + baseResult = true; + } + + return baseResult; + } + + [IgnoreDataMember] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileName(Path); + } + + return null; + } + } + + protected override bool IsAllowTagFilterEnforced() + { + if (this is ICollectionFolder) + { + return false; + } + if (this is UserView) + { + return false; + } + return true; + } + + [IgnoreDataMember] + protected virtual bool SupportsShortcutChildren + { + get { return false; } + } + + /// <summary> + /// Adds the child. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.InvalidOperationException">Unable to add + item.Name</exception> + public void AddChild(BaseItem item, CancellationToken cancellationToken) + { + item.SetParent(this); + + if (item.Id.Equals(Guid.Empty)) + { + item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType()); + } + + if (item.DateCreated == DateTime.MinValue) + { + item.DateCreated = DateTime.UtcNow; + } + if (item.DateModified == DateTime.MinValue) + { + item.DateModified = DateTime.UtcNow; + } + + LibraryManager.CreateItem(item, this); + } + + /// <summary> + /// Gets the actual children. + /// </summary> + /// <value>The actual children.</value> + [IgnoreDataMember] + public virtual IEnumerable<BaseItem> Children + { + get + { + return LoadChildren(); + } + } + + /// <summary> + /// thread-safe access to all recursive children of this folder - without regard to user + /// </summary> + /// <value>The recursive children.</value> + [IgnoreDataMember] + public IEnumerable<BaseItem> RecursiveChildren + { + get { return GetRecursiveChildren(); } + } + + public override bool IsVisible(User user) + { + if (this is ICollectionFolder && !(this is BasePluginFolder)) + { + if (user.Policy.BlockedMediaFolders != null) + { + if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || + + // Backwards compatibility + user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + else + { + if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + } + + return base.IsVisible(user); + } + + /// <summary> + /// Loads our children. Validation will occur externally. + /// We want this sychronous. + /// </summary> + protected virtual List<BaseItem> LoadChildren() + { + //Logger.Debug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); + //just load our children from the repo - the library will be validated and maintained in other processes + return GetCachedChildren(); + } + + public override double? GetRefreshProgress() + { + return ProviderManager.GetRefreshProgress(Id); + } + + public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) + { + return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); + } + + /// <summary> + /// Validates that the children of the folder still exist + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="metadataRefreshOptions">The metadata refresh options.</param> + /// <param name="recursive">if set to <c>true</c> [recursive].</param> + /// <returns>Task.</returns> + public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) + { + return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); + } + + private Dictionary<Guid, BaseItem> GetActualChildrenDictionary() + { + var dictionary = new Dictionary<Guid, BaseItem>(); + + var childrenList = Children.ToList(); + + foreach (var child in childrenList) + { + var id = child.Id; + if (dictionary.ContainsKey(id)) + { + Logger.Error("Found folder containing items with duplicate id. Path: {0}, Child Name: {1}", + Path ?? Name, + child.Path ?? child.Name); + } + else + { + dictionary[id] = child; + } + } + + return dictionary; + } + + protected override void TriggerOnRefreshStart() + { + } + + protected override void TriggerOnRefreshComplete() + { + } + + /// <summary> + /// Validates the children internal. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="recursive">if set to <c>true</c> [recursive].</param> + /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns>Task.</returns> + protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + if (recursive) + { + ProviderManager.OnRefreshStart(this); + } + + try + { + await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false); + } + finally + { + if (recursive) + { + ProviderManager.OnRefreshComplete(this); + } + } + } + + private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + cancellationToken.ThrowIfCancellationRequested(); + + var validChildren = new List<BaseItem>(); + var validChildrenNeedGeneration = false; + + if (IsFileProtocol) + { + IEnumerable<BaseItem> nonCachedChildren; + + try + { + nonCachedChildren = GetNonCachedChildren(directoryService); + } + catch (Exception ex) + { + return; + } + + progress.Report(5); + + if (recursive) + { + ProviderManager.OnRefreshProgress(this, 5); + } + + //build a dictionary of the current children we have now by Id so we can compare quickly and easily + var currentChildren = GetActualChildrenDictionary(); + + //create a list for our validated children + var newItems = new List<BaseItem>(); + + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var child in nonCachedChildren) + { + BaseItem currentChild; + + if (currentChildren.TryGetValue(child.Id, out currentChild)) + { + validChildren.Add(currentChild); + + if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None) + { + currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + } + + continue; + } + + // Brand new item - needs to be added + child.SetParent(this); + newItems.Add(child); + validChildren.Add(child); + } + + // If any items were added or removed.... + if (newItems.Count > 0 || currentChildren.Count != validChildren.Count) + { + // That's all the new and changed ones - now see if there are any that are missing + var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); + + foreach (var item in itemsRemoved) + { + if (!item.IsFileProtocol) + { + } + + else + { + Logger.Debug("Removed item: " + item.Path); + + item.SetParent(null); + LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); + } + } + + LibraryManager.CreateItems(newItems, this, cancellationToken); + } + } + else + { + validChildrenNeedGeneration = true; + } + + progress.Report(10); + + if (recursive) + { + ProviderManager.OnRefreshProgress(this, 10); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (recursive) + { + var innerProgress = new ActionableProgress<double>(); + + var folder = this; + innerProgress.RegisterAction(p => + { + double newPct = .80 * p + 10; + progress.Report(newPct); + ProviderManager.OnRefreshProgress(folder, newPct); + }); + + if (validChildrenNeedGeneration) + { + validChildren = Children.ToList(); + validChildrenNeedGeneration = false; + } + + await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cancellationToken).ConfigureAwait(false); + } + + if (refreshChildMetadata) + { + progress.Report(90); + + if (recursive) + { + ProviderManager.OnRefreshProgress(this, 90); + } + + var container = this as IMetadataContainer; + + var innerProgress = new ActionableProgress<double>(); + + var folder = this; + innerProgress.RegisterAction(p => + { + double newPct = .10 * p + 90; + progress.Report(newPct); + if (recursive) + { + ProviderManager.OnRefreshProgress(folder, newPct); + } + }); + + if (container != null) + { + await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false); + } + else + { + if (validChildrenNeedGeneration) + { + validChildren = Children.ToList(); + } + + await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken); + } + } + } + + private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + { + var numComplete = 0; + var count = children.Count; + double currentPercent = 0; + + foreach (var child in children) + { + cancellationToken.ThrowIfCancellationRequested(); + + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentInnerPercent = currentPercent; + + innerProgress.RegisterAction(p => + { + double innerPercent = currentInnerPercent; + innerPercent += p / (count); + progress.Report(innerPercent); + }); + + await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken) + .ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + currentPercent = percent; + + progress.Report(percent); + } + } + + private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + var series = container as Series; + if (series != null) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + } + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + } + + private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + { + var container = child as IMetadataContainer; + + if (container != null) + { + await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false); + } + else + { + if (refreshOptions.RefreshItem(child)) + { + await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + + if (recursive) + { + var folder = child as Folder; + + if (folder != null) + { + await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken); + } + } + } + } + + /// <summary> + /// Refreshes the children. + /// </summary> + /// <param name="children">The children.</param> + /// <param name="directoryService">The directory service.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) + { + var numComplete = 0; + var count = children.Count; + double currentPercent = 0; + + foreach (var child in children) + { + cancellationToken.ThrowIfCancellationRequested(); + + var innerProgress = new ActionableProgress<double>(); + + // Avoid implicitly captured closure + var currentInnerPercent = currentPercent; + + innerProgress.RegisterAction(p => + { + double innerPercent = currentInnerPercent; + innerPercent += p / (count); + progress.Report(innerPercent); + }); + + await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService) + .ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + currentPercent = percent; + + progress.Report(percent); + } + } + + /// <summary> + /// Get the children of this folder from the actual file system + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + var collectionType = LibraryManager.GetContentType(this); + var libraryOptions = LibraryManager.GetLibraryOptions(this); + + return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType); + } + + /// <summary> + /// Get our children from the repo - stubbed for now + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + protected List<BaseItem> GetCachedChildren() + { + return ItemRepository.GetItemList(new InternalItemsQuery + { + Parent = this, + GroupByPresentationUniqueKey = false, + DtoOptions = new DtoOptions(true) + }); + } + + public virtual int GetChildCount(User user) + { + if (LinkedChildren.Length > 0) + { + if (!(this is ICollectionFolder)) + { + return GetChildren(user, true).Count; + } + } + + var result = GetItems(new InternalItemsQuery(user) + { + Recursive = false, + Limit = 0, + Parent = this, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }); + + return result.TotalRecordCount; + } + + public virtual int GetRecursiveChildCount(User user) + { + return GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + IsVirtualItem = false, + EnableTotalRecordCount = true, + Limit = 0, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }).TotalRecordCount; + } + + public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query) + { + var user = query.User; + + if (!query.ForceDirect && RequiresPostFiltering(query)) + { + IEnumerable<BaseItem> items; + Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + + if (query.User == null) + { + items = GetRecursiveChildren(filter); + } + else + { + items = GetRecursiveChildren(user, query); + } + + return PostFilterAndSort(items, query, true); + } + + if (!(this is UserRootFolder) && !(this is AggregateFolder)) + { + if (!query.ParentId.Equals(Guid.Empty)) + { + query.Parent = this; + } + } + + if (RequiresPostFiltering2(query)) + { + return QueryWithPostFiltering2(query); + } + + return LibraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query) + { + var startIndex = query.StartIndex; + var limit = query.Limit; + + query.StartIndex = null; + query.Limit = null; + + var itemsList = LibraryManager.GetItemList(query); + var user = query.User; + + if (user != null) + { + // needed for boxsets + itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList(); + } + + BaseItem[] returnItems; + int totalCount = 0; + + if (query.EnableTotalRecordCount) + { + var itemsArray = itemsList.ToArray(); + totalCount = itemsArray.Length; + returnItems = itemsArray; + } + else + { + returnItems = itemsList.ToArray(); + } + + if (limit.HasValue) + { + returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray(); + } + else if (startIndex.HasValue) + { + returnItems = returnItems.Skip(startIndex.Value).ToArray(); + } + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = returnItems.ToArray() + }; + } + + private bool RequiresPostFiltering2(InternalItemsQuery query) + { + if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Query requires post-filtering due to BoxSet query"); + return true; + } + + return false; + } + + private bool RequiresPostFiltering(InternalItemsQuery query) + { + if (LinkedChildren.Length > 0) + { + if (!(this is ICollectionFolder)) + { + Logger.Debug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name); + return true; + } + } + + // Filter by Video3DFormat + if (query.Is3D.HasValue) + { + Logger.Debug("Query requires post-filtering due to Is3D"); + return true; + } + + if (query.HasOfficialRating.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasOfficialRating"); + return true; + } + + if (query.IsPlaceHolder.HasValue) + { + Logger.Debug("Query requires post-filtering due to IsPlaceHolder"); + return true; + } + + if (query.HasSpecialFeature.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasSpecialFeature"); + return true; + } + + if (query.HasSubtitles.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasSubtitles"); + return true; + } + + if (query.HasTrailer.HasValue) + { + Logger.Debug("Query requires post-filtering due to HasTrailer"); + return true; + } + + // Filter by VideoType + if (query.VideoTypes.Length > 0) + { + Logger.Debug("Query requires post-filtering due to VideoTypes"); + return true; + } + + if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager)) + { + Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems"); + return true; + } + + if (!string.IsNullOrEmpty(query.AdjacentTo)) + { + Logger.Debug("Query requires post-filtering due to AdjacentTo"); + return true; + } + + if (query.SeriesStatuses.Length > 0) + { + Logger.Debug("Query requires post-filtering due to SeriesStatuses"); + return true; + } + + if (query.AiredDuringSeason.HasValue) + { + Logger.Debug("Query requires post-filtering due to AiredDuringSeason"); + return true; + } + + if (query.IsPlayed.HasValue) + { + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name)) + { + Logger.Debug("Query requires post-filtering due to IsPlayed"); + return true; + } + } + + return false; + } + + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) + { + if (query.ItemIds.Length > 0) + { + var result = LibraryManager.GetItemsResult(query); + + if (query.OrderBy.Length == 0) + { + var ids = query.ItemIds.ToList(); + + // Try to preserve order + result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + } + return result; + } + + return GetItemsInternal(query); + } + + public BaseItem[] GetItemList(InternalItemsQuery query) + { + query.EnableTotalRecordCount = false; + + if (query.ItemIds.Length > 0) + { + var result = LibraryManager.GetItemList(query); + + if (query.OrderBy.Length == 0) + { + var ids = query.ItemIds.ToList(); + + // Try to preserve order + return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + } + return result.ToArray(result.Count); + } + + return GetItemsInternal(query).Items; + } + + protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = this; + query.ChannelIds = new Guid[] { ChannelId }; + + // Don't blow up here because it could cause parent screens with other content to fail + return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result; + } + catch + { + // Already logged at lower levels + return new QueryResult<BaseItem>(); + } + } + + if (query.Recursive) + { + return QueryRecursive(query); + } + + var user = query.User; + + Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + + IEnumerable<BaseItem> items; + + if (query.User == null) + { + items = Children.Where(filter); + } + else + { + items = GetChildren(user, true).Where(filter); + } + + return PostFilterAndSort(items, query, true); + } + + public static ICollectionManager CollectionManager { get; set; } + + protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting) + { + var user = query.User; + + // Check recursive - don't substitute in plain folder views + if (user != null) + { + items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); + } + + if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) + { + items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1); + } + if (!string.IsNullOrEmpty(query.NameStartsWith)) + { + items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrEmpty(query.NameLessThan)) + { + items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1); + } + + // This must be the last filter + if (!string.IsNullOrEmpty(query.AdjacentTo)) + { + items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo); + } + + return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); + } + + private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items, + InternalItemsQuery query, + BaseItem queryParent, + User user, + IServerConfigurationManager configurationManager, ICollectionManager collectionManager) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) + { + items = collectionManager.CollapseItemsWithinBoxSets(items, user); + } + + return items; + } + + private static bool CollapseBoxSetItems(InternalItemsQuery query, + BaseItem queryParent, + User user, + IServerConfigurationManager configurationManager) + { + // Could end up stuck in a loop like this + if (queryParent is BoxSet) + { + return false; + } + if (queryParent is Series) + { + return false; + } + if (queryParent is Season) + { + return false; + } + if (queryParent is MusicAlbum) + { + return false; + } + if (queryParent is MusicArtist) + { + return false; + } + + var param = query.CollapseBoxSetItems; + + if (!param.HasValue) + { + if (user != null && !configurationManager.Configuration.EnableGroupingIntoCollections) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase)) + { + param = true; + } + } + + return param.HasValue && param.Value && AllowBoxSetCollapsing(query); + } + + private static bool AllowBoxSetCollapsing(InternalItemsQuery request) + { + if (request.IsFavorite.HasValue) + { + return false; + } + if (request.IsFavoriteOrLiked.HasValue) + { + return false; + } + if (request.IsLiked.HasValue) + { + return false; + } + if (request.IsPlayed.HasValue) + { + return false; + } + if (request.IsResumable.HasValue) + { + return false; + } + if (request.IsFolder.HasValue) + { + return false; + } + + if (request.Genres.Length > 0) + { + return false; + } + + if (request.GenreIds.Length > 0) + { + return false; + } + + if (request.HasImdbId.HasValue) + { + return false; + } + + if (request.HasOfficialRating.HasValue) + { + return false; + } + + if (request.HasOverview.HasValue) + { + return false; + } + + if (request.HasParentalRating.HasValue) + { + return false; + } + + if (request.HasSpecialFeature.HasValue) + { + return false; + } + + if (request.HasSubtitles.HasValue) + { + return false; + } + + if (request.HasThemeSong.HasValue) + { + return false; + } + + if (request.HasThemeVideo.HasValue) + { + return false; + } + + if (request.HasTmdbId.HasValue) + { + return false; + } + + if (request.HasTrailer.HasValue) + { + return false; + } + + if (request.ImageTypes.Length > 0) + { + return false; + } + + if (request.Is3D.HasValue) + { + return false; + } + + if (request.IsHD.HasValue) + { + return false; + } + + if (request.IsLocked.HasValue) + { + return false; + } + + if (request.IsPlaceHolder.HasValue) + { + return false; + } + + if (request.IsPlayed.HasValue) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(request.Person)) + { + return false; + } + + if (request.PersonIds.Length > 0) + { + return false; + } + + if (request.ItemIds.Length > 0) + { + return false; + } + + if (request.StudioIds.Length > 0) + { + return false; + } + + if (request.GenreIds.Length > 0) + { + return false; + } + + if (request.VideoTypes.Length > 0) + { + return false; + } + + if (request.Years.Length > 0) + { + return false; + } + + if (request.Tags.Length > 0) + { + return false; + } + + if (request.OfficialRatings.Length > 0) + { + return false; + } + + if (request.MinPlayers.HasValue) + { + return false; + } + + if (request.MaxPlayers.HasValue) + { + return false; + } + + if (request.MinCommunityRating.HasValue) + { + return false; + } + + if (request.MinCriticRating.HasValue) + { + return false; + } + + if (request.MinIndexNumber.HasValue) + { + return false; + } + + return true; + } + + public List<BaseItem> GetChildren(User user, bool includeLinkedChildren) + { + return GetChildren(user, includeLinkedChildren, null); + } + + public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + if (user == null) + { + throw new ArgumentNullException(); + } + + //the true root should return our users root folder children + if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + + var result = new Dictionary<Guid, BaseItem>(); + + AddChildren(user, includeLinkedChildren, result, false, query); + + return result.Values.ToList(); + } + + protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + return Children; + } + + /// <summary> + /// Adds the children to list. + /// </summary> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query) + { + foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) + { + bool? isVisibleToUser = null; + + if (query == null || UserViewBuilder.FilterItem(child, query)) + { + isVisibleToUser = child.IsVisible(user); + + if (isVisibleToUser.Value) + { + result[child.Id] = child; + } + } + + if (isVisibleToUser ?? child.IsVisible(user)) + { + if (recursive && child.IsFolder) + { + var folder = (Folder)child; + + folder.AddChildren(user, includeLinkedChildren, result, true, query); + } + } + } + + if (includeLinkedChildren) + { + foreach (var child in GetLinkedChildren(user)) + { + if (query == null || UserViewBuilder.FilterItem(child, query)) + { + if (child.IsVisible(user)) + { + result[child.Id] = child; + } + } + } + } + } + + /// <summary> + /// Gets allowed recursive children of an item + /// </summary> + /// <param name="user">The user.</param> + /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> + /// <returns>IEnumerable{BaseItem}.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true) + { + return GetRecursiveChildren(user, null); + } + + public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var result = new Dictionary<Guid, BaseItem>(); + + AddChildren(user, true, result, true, query); + + return result.Values; + } + + /// <summary> + /// Gets the recursive children. + /// </summary> + /// <returns>IList{BaseItem}.</returns> + public IList<BaseItem> GetRecursiveChildren() + { + return GetRecursiveChildren(true); + } + + public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren) + { + return GetRecursiveChildren(i => true, includeLinkedChildren); + } + + public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter) + { + return GetRecursiveChildren(filter, true); + } + + public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren) + { + var result = new Dictionary<Guid, BaseItem>(); + + AddChildrenToList(result, includeLinkedChildren, true, filter); + + return result.Values.ToList(); + } + + /// <summary> + /// Adds the children to list. + /// </summary> + private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Func<BaseItem, bool> filter) + { + foreach (var child in Children) + { + if (filter == null || filter(child)) + { + result[child.Id] = child; + } + + if (recursive && child.IsFolder) + { + var folder = (Folder)child; + + // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loop of linked items + folder.AddChildrenToList(result, false, true, filter); + } + } + + if (includeLinkedChildren) + { + foreach (var child in GetLinkedChildren()) + { + if (filter == null || filter(child)) + { + result[child.Id] = child; + } + } + } + } + + + /// <summary> + /// Gets the linked children. + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + public List<BaseItem> GetLinkedChildren() + { + var linkedChildren = LinkedChildren; + var list = new List<BaseItem>(linkedChildren.Length); + + foreach (var i in linkedChildren) + { + var child = GetLinkedChild(i); + + if (child != null) + { + list.Add(child); + } + } + return list; + } + + protected virtual bool FilterLinkedChildrenPerUser + { + get + { + return false; + } + } + + public bool ContainsLinkedChildByItemId(Guid itemId) + { + var linkedChildren = LinkedChildren; + foreach (var i in linkedChildren) + { + if (i.ItemId.HasValue && i.ItemId.Value == itemId) + { + return true; + } + + var child = GetLinkedChild(i); + + if (child != null && child.Id == itemId) + { + return true; + } + } + return false; + } + + public List<BaseItem> GetLinkedChildren(User user) + { + if (!FilterLinkedChildrenPerUser || user == null) + { + return GetLinkedChildren(); + } + + var linkedChildren = LinkedChildren; + var list = new List<BaseItem>(linkedChildren.Length); + + if (linkedChildren.Length == 0) + { + return list; + } + + var allUserRootChildren = LibraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .ToList(); + + var collectionFolderIds = allUserRootChildren + .Select(i => i.Id) + .ToList(); + + foreach (var i in linkedChildren) + { + var child = GetLinkedChild(i); + + if (child == null) + { + continue; + } + + var childOwner = child.GetOwner() ?? child; + + if (childOwner != null && !(child is IItemByName)) + { + var childProtocol = childOwner.PathProtocol; + if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File) + { + if (!childOwner.IsVisibleStandalone(user)) + { + continue; + } + } + else + { + var itemCollectionFolderIds = + LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id); + + if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains)) + { + continue; + } + } + } + + list.Add(child); + } + + return list; + } + + /// <summary> + /// Gets the linked children. + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos() + { + return LinkedChildren + .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i))) + .Where(i => i.Item2 != null); + } + + [IgnoreDataMember] + protected override bool SupportsOwnedItems + { + get + { + return base.SupportsOwnedItems || SupportsShortcutChildren; + } + } + + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var changesFound = false; + + if (IsFileProtocol) + { + if (RefreshLinkedChildren(fileSystemChildren)) + { + changesFound = true; + } + } + + var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + return baseHasChanges || changesFound; + } + + /// <summary> + /// Refreshes the linked children. + /// </summary> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) + { + if (SupportsShortcutChildren) + { + var newShortcutLinks = fileSystemChildren + .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName)) + .Select(i => + { + try + { + Logger.Debug("Found shortcut at {0}", i.FullName); + + var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShortcut(i.FullName)); + + if (!string.IsNullOrEmpty(resolvedPath)) + { + return new LinkedChild + { + Path = resolvedPath, + Type = LinkedChildType.Shortcut + }; + } + + Logger.Error("Error resolving shortcut {0}", i.FullName); + + return null; + } + catch (IOException ex) + { + Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName); + return null; + } + }) + .Where(i => i != null) + .ToList(); + + var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); + + if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem))) + { + Logger.Info("Shortcut links have changed for {0}", Path); + + newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual)); + LinkedChildren = newShortcutLinks.ToArray(newShortcutLinks.Count); + return true; + } + } + + foreach (var child in LinkedChildren) + { + // Reset the cached value + child.ItemId = null; + } + + return false; + } + + /// <summary> + /// Marks the played. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="datePlayed">The date played.</param> + /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> + /// <returns>Task.</returns> + public override void MarkPlayed(User user, + DateTime? datePlayed, + bool resetPosition) + { + var query = new InternalItemsQuery + { + User = user, + Recursive = true, + IsFolder = false, + EnableTotalRecordCount = false + }; + + if (!user.Configuration.DisplayMissingEpisodes) + { + query.IsVirtualItem = false; + } + + var itemsResult = GetItemList(query); + + // Sweep through recursively and update status + foreach (var item in itemsResult) + { + if (item.IsVirtualItem) + { + // The querying doesn't support virtual unaired + var episode = item as Episode; + if (episode != null && episode.IsUnaired) + { + continue; + } + } + + item.MarkPlayed(user, datePlayed, resetPosition); + } + } + + /// <summary> + /// Marks the unplayed. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + public override void MarkUnplayed(User user) + { + var itemsResult = GetItemList(new InternalItemsQuery + { + User = user, + Recursive = true, + IsFolder = false, + EnableTotalRecordCount = false + + }); + + // Sweep through recursively and update status + foreach (var item in itemsResult) + { + item.MarkUnplayed(user); + } + } + + public override bool IsPlayed(User user) + { + var itemsResult = GetItemList(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + IsVirtualItem = false, + EnableTotalRecordCount = false + + }); + + return itemsResult + .All(i => i.IsPlayed(user)); + } + + public override bool IsUnplayed(User user) + { + return !IsPlayed(user); + } + + [IgnoreDataMember] + public virtual bool SupportsUserDataFromChildren + { + get + { + // These are just far too slow. + if (this is ICollectionFolder) + { + return false; + } + if (this is UserView) + { + return false; + } + if (this is UserRootFolder) + { + return false; + } + if (this is Channel) + { + return false; + } + if (SourceType != SourceType.Library) + { + return false; + } + var iItemByName = this as IItemByName; + if (iItemByName != null) + { + var hasDualAccess = this as IHasDualAccess; + if (hasDualAccess == null || hasDualAccess.IsAccessedByName) + { + return false; + } + } + + return true; + } + } + + public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) + { + if (!SupportsUserDataFromChildren) + { + return; + } + + if (itemDto != null) + { + if (fields.ContainsField(ItemFields.RecursiveItemCount)) + { + itemDto.RecursiveItemCount = GetRecursiveChildCount(user); + } + } + + if (SupportsPlayedStatus) + { + var unplayedQueryResult = GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + IsVirtualItem = false, + EnableTotalRecordCount = true, + Limit = 0, + IsPlayed = false, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + + }); + + double unplayedCount = unplayedQueryResult.TotalRecordCount; + + dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; + + if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + { + if (itemDto.RecursiveItemCount.Value > 0) + { + var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; + } + } + else + { + dto.Played = (dto.UnplayedItemCount ?? 0) == 0; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs new file mode 100644 index 000000000..e4c417c8a --- /dev/null +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -0,0 +1,129 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public class Game : BaseItem, IHasTrailers, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo> + { + public Game() + { + MultiPartGameFiles = new string[] {}; + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + public override bool CanDownload() + { + return IsFileProtocol; + } + + [IgnoreDataMember] + public override bool SupportsThemeMedia + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return false; } + } + + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public override string MediaType + { + get { return Model.Entities.MediaType.Game; } + } + + /// <summary> + /// Gets or sets the players supported. + /// </summary> + /// <value>The players supported.</value> + public int? PlayersSupported { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance is place holder. + /// </summary> + /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value> + public bool IsPlaceHolder { get; set; } + + /// <summary> + /// Gets or sets the game system. + /// </summary> + /// <value>The game system.</value> + public string GameSystem { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is multi part. + /// </summary> + /// <value><c>true</c> if this instance is multi part; otherwise, <c>false</c>.</value> + public bool IsMultiPart { get; set; } + + /// <summary> + /// Holds the paths to the game files in the event this is a multipart game + /// </summary> + public string[] MultiPartGameFiles { get; set; } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + var id = this.GetProviderId(MetadataProviders.Gamesdb); + + if (!string.IsNullOrEmpty(id)) + { + list.Insert(0, "Game-Gamesdb-" + id); + } + return list; + } + + public override IEnumerable<FileSystemMetadata> GetDeletePaths() + { + if (!IsInMixedFolder) + { + return new[] { + new FileSystemMetadata + { + FullName = FileSystem.GetDirectoryName(Path), + IsDirectory = true + } + }; + } + + return base.GetDeletePaths(); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Game; + } + + public GameInfo GetLookupInfo() + { + var id = GetItemLookupInfo<GameInfo>(); + + id.GameSystem = GameSystem; + + return id; + } + } +} diff --git a/MediaBrowser.Controller/Entities/GameGenre.cs b/MediaBrowser.Controller/Entities/GameGenre.cs new file mode 100644 index 000000000..63493ad4a --- /dev/null +++ b/MediaBrowser.Controller/Entities/GameGenre.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + public class GameGenre : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override bool CanDelete() + { + return false; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.GenreIds = new[] { Id }; + query.IncludeItemTypes = new[] { typeof(Game).Name }; + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GameGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/GameSystem.cs b/MediaBrowser.Controller/Entities/GameSystem.cs new file mode 100644 index 000000000..fb60ce83a --- /dev/null +++ b/MediaBrowser.Controller/Entities/GameSystem.cs @@ -0,0 +1,101 @@ +using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Users; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class GameSystem + /// </summary> + public class GameSystem : Folder, IHasLookupInfo<GameSystemInfo> + { + /// <summary> + /// Return the id that should be used to key display prefs for this item. + /// Default is based on the type for everything except actual generic folders. + /// </summary> + /// <value>The display prefs id.</value> + [IgnoreDataMember] + public override Guid DisplayPreferencesId + { + get + { + return Id; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 16; + value /= 9; + + return value; + } + + /// <summary> + /// Gets or sets the game system. + /// </summary> + /// <value>The game system.</value> + public string GameSystemName { get; set; } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (!string.IsNullOrEmpty(GameSystemName)) + { + list.Insert(0, "GameSystem-" + GameSystemName); + } + return list; + } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + // Don't block. Determine by game + return false; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Game; + } + + public GameSystemInfo GetLookupInfo() + { + var id = GetItemLookupInfo<GameSystemInfo>(); + + id.Path = Path; + + return id; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs new file mode 100644 index 000000000..94a5984df --- /dev/null +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -0,0 +1,140 @@ +using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Entities.Audio; +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Genre + /// </summary> + public class Genre : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override bool CanDelete() + { + return false; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.GenreIds = new[] { Id }; + query.ExcludeItemTypes = new[] { typeof(Game).Name, typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs new file mode 100644 index 000000000..b61e7b339 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is just a marker interface to denote top level folders + /// </summary> + public interface ICollectionFolder : IHasCollectionType + { + string Path { get; } + string Name { get; } + Guid Id { get; } + string[] PhysicalLocations { get; } + } + + public interface ISupportsUserSpecificView + { + bool EnableUserSpecificView { get; } + } + + public interface IHasCollectionType + { + string CollectionType { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs new file mode 100644 index 000000000..5aecf4eac --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasAspectRatio + /// </summary> + public interface IHasAspectRatio + { + /// <summary> + /// Gets or sets the aspect ratio. + /// </summary> + /// <value>The aspect ratio.</value> + string AspectRatio { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs new file mode 100644 index 000000000..5e1ae2179 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -0,0 +1,15 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasDisplayOrder + /// </summary> + public interface IHasDisplayOrder + { + /// <summary> + /// Gets or sets the display order. + /// </summary> + /// <value>The display order.</value> + string DisplayOrder { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs new file mode 100644 index 000000000..a13c95942 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Model.Dto; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasMediaSources + { + /// <summary> + /// Gets the media sources. + /// </summary> + List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); + List<MediaStream> GetMediaStreams(); + Guid Id { get; set; } + long? RunTimeTicks { get; set; } + string Path { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs new file mode 100644 index 000000000..0bc9ff81e --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasProgramAttributes + { + bool IsMovie { get; set; } + bool IsSports { get; } + bool IsNews { get; } + bool IsKids { get; } + bool IsRepeat { get; set; } + bool IsSeries { get; set; } + ProgramAudio? Audio { get; set; } + string EpisodeTitle { get; set; } + string ServiceName { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs new file mode 100644 index 000000000..2fd402bc2 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasScreenshots + /// </summary> + public interface IHasScreenshots + { + } +} diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs new file mode 100644 index 000000000..18d66452a --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -0,0 +1,20 @@ + +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasSeries + { + /// <summary> + /// Gets the name of the series. + /// </summary> + /// <value>The name of the series.</value> + string SeriesName { get; set; } + string FindSeriesName(); + string FindSeriesSortName(); + Guid SeriesId { get; set; } + Guid FindSeriesId(); + string SeriesPresentationUniqueKey { get; set; } + string FindSeriesPresentationUniqueKey(); + } +} diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs new file mode 100644 index 000000000..f4905b7dc --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -0,0 +1,13 @@ +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasSpecialFeatures + { + /// <summary> + /// Gets or sets the special feature ids. + /// </summary> + /// <value>The special feature ids.</value> + Guid[] SpecialFeatureIds { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasStartDate.cs b/MediaBrowser.Controller/Entities/IHasStartDate.cs new file mode 100644 index 000000000..a6714fb96 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasStartDate.cs @@ -0,0 +1,9 @@ +using System; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasStartDate + { + DateTime StartDate { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs new file mode 100644 index 000000000..8e7c4e007 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -0,0 +1,39 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasTrailers : IHasProviderIds + { + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets or sets the local trailer ids. + /// </summary> + /// <value>The local trailer ids.</value> + Guid[] LocalTrailerIds { get; set; } + Guid[] RemoteTrailerIds { get; set; } + Guid Id { get; set; } + } + + public static class HasTrailerExtensions + { + /// <summary> + /// Gets the trailer ids. + /// </summary> + /// <returns>List<Guid>.</returns> + public static List<Guid> GetTrailerIds(this IHasTrailers item) + { + var list = item.LocalTrailerIds.ToList(); + list.AddRange(item.RemoteTrailerIds); + return list; + } + + } +} diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs new file mode 100644 index 000000000..d21c6ae4d --- /dev/null +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Marker interface + /// </summary> + public interface IItemByName + { + IList<BaseItem> GetTaggedItems(InternalItemsQuery query); + } + + public interface IHasDualAccess : IItemByName + { + bool IsAccessedByName { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/IMetadataContainer.cs b/MediaBrowser.Controller/Entities/IMetadataContainer.cs new file mode 100644 index 000000000..33aa08425 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IMetadataContainer.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Controller.Providers; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + public interface IMetadataContainer + { + /// <summary> + /// Refreshes all metadata. + /// </summary> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs new file mode 100644 index 000000000..fbe5a06d0 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs @@ -0,0 +1,12 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Marker interface to denote a class that supports being hidden underneath it's boxset. + /// Just about anything can be placed into a boxset, + /// but movies should also only appear underneath and not outside separately (subject to configuration). + /// </summary> + public interface ISupportsBoxSetGrouping + { + } +} diff --git a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs new file mode 100644 index 000000000..2507c8ee6 --- /dev/null +++ b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs @@ -0,0 +1,12 @@ + +namespace MediaBrowser.Controller.Entities +{ + public interface ISupportsPlaceHolders + { + /// <summary> + /// Gets a value indicating whether this instance is place holder. + /// </summary> + /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value> + bool IsPlaceHolder { get; } + } +} diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs new file mode 100644 index 000000000..ff57c2471 --- /dev/null +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -0,0 +1,261 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Configuration; +using System.Linq; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Entities +{ + public class InternalItemsQuery + { + public bool Recursive { get; set; } + + public int? StartIndex { get; set; } + + public int? Limit { get; set; } + + public User User { get; set; } + + public BaseItem SimilarTo { get; set; } + + public bool? IsFolder { get; set; } + public bool? IsFavorite { get; set; } + public bool? IsFavoriteOrLiked { get; set; } + public bool? IsLiked { get; set; } + public bool? IsPlayed { get; set; } + public bool? IsResumable { get; set; } + public bool? IncludeItemsByName { get; set; } + + public string[] MediaTypes { get; set; } + public string[] IncludeItemTypes { get; set; } + public string[] ExcludeItemTypes { get; set; } + public string[] ExcludeTags { get; set; } + public string[] ExcludeInheritedTags { get; set; } + public string[] Genres { get; set; } + + public bool? IsSpecialSeason { get; set; } + public bool? IsMissing { get; set; } + public bool? IsUnaired { get; set; } + public bool? CollapseBoxSetItems { get; set; } + + public string NameStartsWithOrGreater { get; set; } + public string NameStartsWith { get; set; } + public string NameLessThan { get; set; } + public string NameContains { get; set; } + public string MinSortName { get; set; } + + public string PresentationUniqueKey { get; set; } + public string Path { get; set; } + public string PathNotStartsWith { get; set; } + public string Name { get; set; } + + public string Person { get; set; } + public Guid[] PersonIds { get; set; } + public Guid[] ItemIds { get; set; } + public Guid[] ExcludeItemIds { get; set; } + public string AdjacentTo { get; set; } + public string[] PersonTypes { get; set; } + + public bool? Is3D { get; set; } + public bool? IsHD { get; set; } + public bool? IsLocked { get; set; } + public bool? IsPlaceHolder { get; set; } + + public bool? HasImdbId { get; set; } + public bool? HasOverview { get; set; } + public bool? HasTmdbId { get; set; } + public bool? HasOfficialRating { get; set; } + public bool? HasTvdbId { get; set; } + public bool? HasThemeSong { get; set; } + public bool? HasThemeVideo { get; set; } + public bool? HasSubtitles { get; set; } + public bool? HasSpecialFeature { get; set; } + public bool? HasTrailer { get; set; } + public bool? HasParentalRating { get; set; } + + public Guid[] StudioIds { get; set; } + public Guid[] GenreIds { get; set; } + public ImageType[] ImageTypes { get; set; } + public VideoType[] VideoTypes { get; set; } + public UnratedItem[] BlockUnratedItems { get; set; } + public int[] Years { get; set; } + public string[] Tags { get; set; } + public string[] OfficialRatings { get; set; } + + public DateTime? MinPremiereDate { get; set; } + public DateTime? MaxPremiereDate { get; set; } + public DateTime? MinStartDate { get; set; } + public DateTime? MaxStartDate { get; set; } + public DateTime? MinEndDate { get; set; } + public DateTime? MaxEndDate { get; set; } + public bool? IsAiring { get; set; } + + public bool? IsMovie { get; set; } + public bool? IsSports { get; set; } + public bool? IsKids { get; set; } + public bool? IsNews { get; set; } + public bool? IsSeries { get; set; } + + public int? MinPlayers { get; set; } + public int? MaxPlayers { get; set; } + public int? MinIndexNumber { get; set; } + public int? AiredDuringSeason { get; set; } + public double? MinCriticRating { get; set; } + public double? MinCommunityRating { get; set; } + + public Guid[] ChannelIds { get; set; } + + public int? ParentIndexNumber { get; set; } + public int? ParentIndexNumberNotEquals { get; set; } + public int? IndexNumber { get; set; } + public int? MinParentalRating { get; set; } + public int? MaxParentalRating { get; set; } + + public bool? HasDeadParentId { get; set; } + public bool? IsVirtualItem { get; set; } + + public Guid ParentId { get; set; } + public string ParentType { get; set; } + public Guid[] AncestorIds { get; set; } + public Guid[] TopParentIds { get; set; } + + public BaseItem Parent + { + set + { + if (value == null) + { + ParentId = Guid.Empty; + ParentType = null; + } + else + { + ParentId = value.Id; + ParentType = value.GetType().Name; + } + } + } + + public string[] PresetViews { get; set; } + public TrailerType[] TrailerTypes { get; set; } + public SourceType[] SourceTypes { get; set; } + + public SeriesStatus[] SeriesStatuses { get; set; } + public string ExternalSeriesId { get; set; } + public string ExternalId { get; set; } + + public Guid[] AlbumIds { get; set; } + public Guid[] ArtistIds { get; set; } + public Guid[] ExcludeArtistIds { get; set; } + public string AncestorWithPresentationUniqueKey { get; set; } + public string SeriesPresentationUniqueKey { get; set; } + + public bool GroupByPresentationUniqueKey { get; set; } + public bool GroupBySeriesPresentationUniqueKey { get; set; } + public bool EnableTotalRecordCount { get; set; } + public bool ForceDirect { get; set; } + public Dictionary<string, string> ExcludeProviderIds { get; set; } + public bool EnableGroupByMetadataKey { get; set; } + public bool? HasChapterImages { get; set; } + + // why tuple vs value tuple? + //public Tuple<string, SortOrder>[] OrderBy { get; set; } + public ValueTuple<string, SortOrder>[] OrderBy { get; set; } + + public DateTime? MinDateCreated { get; set; } + public DateTime? MinDateLastSaved { get; set; } + public DateTime? MinDateLastSavedForUser { get; set; } + + public DtoOptions DtoOptions { get; set; } + public int MinSimilarityScore { get; set; } + public string HasNoAudioTrackWithLanguage { get; set; } + public string HasNoInternalSubtitleTrackWithLanguage { get; set; } + public string HasNoExternalSubtitleTrackWithLanguage { get; set; } + public string HasNoSubtitleTrackWithLanguage { get; set; } + public bool? IsDeadArtist { get; set; } + public bool? IsDeadStudio { get; set; } + public bool? IsDeadPerson { get; set; } + + public InternalItemsQuery() + { + AlbumArtistIds = new Guid[] {}; + AlbumIds = new Guid[] {}; + AncestorIds = new Guid[] {}; + ArtistIds = new Guid[] {}; + BlockUnratedItems = new UnratedItem[] { }; + BoxSetLibraryFolders = new Guid[] {}; + ChannelIds = new Guid[] {}; + ContributingArtistIds = new Guid[] {}; + DtoOptions = new DtoOptions(); + EnableTotalRecordCount = true; + ExcludeArtistIds = new Guid[] {}; + ExcludeInheritedTags = new string[] {}; + ExcludeItemIds = new Guid[] {}; + ExcludeItemTypes = new string[] {}; + ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ExcludeTags = new string[] {}; + GenreIds = new Guid[] {}; + Genres = new string[] {}; + GroupByPresentationUniqueKey = true; + HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ImageTypes = new ImageType[] { }; + IncludeItemTypes = new string[] {}; + ItemIds = new Guid[] {}; + MediaTypes = new string[] {}; + MinSimilarityScore = 20; + OfficialRatings = new string[] {}; + OrderBy = Array.Empty<ValueTuple<string, SortOrder>>(); + PersonIds = new Guid[] {}; + PersonTypes = new string[] {}; + PresetViews = new string[] {}; + SeriesStatuses = new SeriesStatus[] { }; + SourceTypes = new SourceType[] { }; + StudioIds = new Guid[] {}; + Tags = new string[] {}; + TopParentIds = new Guid[] {}; + TrailerTypes = new TrailerType[] { }; + VideoTypes = new VideoType[] { }; + Years = new int[] { }; + } + + public InternalItemsQuery(User user) + : this() + { + SetUser(user); + } + + public void SetUser(User user) + { + if (user != null) + { + var policy = user.Policy; + MaxParentalRating = policy.MaxParentalRating; + + if (policy.MaxParentalRating.HasValue) + { + BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray(); + } + + ExcludeInheritedTags = policy.BlockedTags; + + User = user; + } + } + + public Dictionary<string, string> HasAnyProviderId { get; set; } + public Guid[] AlbumArtistIds { get; set; } + public Guid[] BoxSetLibraryFolders { get; set; } + public Guid[] ContributingArtistIds { get; set; } + public bool? HasAired { get; set; } + public bool? HasOwnerId { get; set; } + public bool? Is4K { get; set; } + public int? MaxHeight { get; set; } + public int? MaxWidth { get; set; } + public int? MinHeight { get; set; } + public int? MinWidth { get; set; } + public string SearchTerm { get; set; } + public string SeriesTimerId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs new file mode 100644 index 000000000..7e00834e3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + public class InternalPeopleQuery + { + public Guid ItemId { get; set; } + public string[] PersonTypes { get; set; } + public string[] ExcludePersonTypes { get; set; } + public int? MaxListOrder { get; set; } + public Guid AppearsInItemId { get; set; } + public string NameContains { get; set; } + + public InternalPeopleQuery() + { + PersonTypes = new string[] { }; + ExcludePersonTypes = new string[] { }; + } + } +} diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs new file mode 100644 index 000000000..bd0011c4b --- /dev/null +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Model.Entities; +using System; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class ItemImageInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public ImageType Type { get; set; } + + /// <summary> + /// Gets or sets the date modified. + /// </summary> + /// <value>The date modified.</value> + public DateTime DateModified { get; set; } + + public int Width { get; set; } + public int Height { get; set; } + + [IgnoreDataMember] + public bool IsLocalFile + { + get + { + if (Path != null) + { + if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + return true; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs new file mode 100644 index 000000000..363a3d6fd --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class LinkedChild + { + public string Path { get; set; } + public LinkedChildType Type { get; set; } + public string LibraryItemId { get; set; } + + [IgnoreDataMember] + public string Id { get; set; } + + /// <summary> + /// Serves as a cache + /// </summary> + public Guid? ItemId { get; set; } + + public static LinkedChild Create(BaseItem item) + { + var child = new LinkedChild + { + Path = item.Path, + Type = LinkedChildType.Manual + }; + + if (string.IsNullOrEmpty(child.Path)) + { + child.LibraryItemId = item.Id.ToString("N"); + } + + return child; + } + + public LinkedChild() + { + Id = Guid.NewGuid().ToString("N"); + } + } + + public enum LinkedChildType + { + Manual = 0, + Shortcut = 1 + } + + public class LinkedChildComparer : IEqualityComparer<LinkedChild> + { + private readonly IFileSystem _fileSystem; + + public LinkedChildComparer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public bool Equals(LinkedChild x, LinkedChild y) + { + if (x.Type == y.Type) + { + return _fileSystem.AreEqual(x.Path, y.Path); + } + return false; + } + + public int GetHashCode(LinkedChild obj) + { + return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs new file mode 100644 index 000000000..5918bf981 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -0,0 +1,263 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities.Movies +{ + /// <summary> + /// Class BoxSet + /// </summary> + public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo> + { + public BoxSet() + { + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] { }; + RemoteTrailerIds = new Guid[] { }; + + DisplayOrder = ItemSortBy.PremiereDate; + } + + [IgnoreDataMember] + protected override bool FilterLinkedChildrenPerUser + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets or sets the display order. + /// </summary> + /// <value>The display order.</value> + public string DisplayOrder { get; set; } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Movie); + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Movie; + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + if (IsLegacyBoxSet) + { + return base.GetNonCachedChildren(directoryService); + } + return new List<BaseItem>(); + } + + protected override List<BaseItem> LoadChildren() + { + if (IsLegacyBoxSet) + { + return base.LoadChildren(); + } + + // Save a trip to the database + return new List<BaseItem>(); + } + + [IgnoreDataMember] + private bool IsLegacyBoxSet + { + get + { + if (string.IsNullOrEmpty(Path)) + { + return false; + } + + if (LinkedChildren.Length > 0) + { + return false; + } + + return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); + } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) + { + return true; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + var children = base.GetChildren(user, includeLinkedChildren, query); + + if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)) + { + // Sort by name + return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + { + // Sort by release date + return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + // Default sorting + return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + var children = base.GetRecursiveChildren(user, query); + + if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + { + // Sort by release date + return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); + } + + return children; + } + + public BoxSetInfo GetLookupInfo() + { + return GetItemLookupInfo<BoxSetInfo>(); + } + + public override bool IsVisible(User user) + { + if (IsLegacyBoxSet) + { + return base.IsVisible(user); + } + + if (base.IsVisible(user)) + { + if (LinkedChildren.Length == 0) + { + return true; + } + + var userLibraryFolderIds = GetLibraryFolderIds(user); + var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds(); + + if (libraryFolderIds.Length == 0) + { + return true; + } + + return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i)); + } + + return false; + } + + public override bool IsVisibleStandalone(User user) + { + if (IsLegacyBoxSet) + { + return base.IsVisibleStandalone(user); + } + + return IsVisible(user); + } + + public Guid[] LibraryFolderIds { get; set; } + + private Guid[] GetLibraryFolderIds(User user) + { + return LibraryManager.GetUserRootFolder().GetChildren(user, true) + .Select(i => i.Id) + .ToArray(); + } + + public Guid[] GetLibraryFolderIds() + { + var expandedFolders = new List<Guid>() { }; + + return FlattenItems(this, expandedFolders) + .SelectMany(i => LibraryManager.GetCollectionFolders(i)) + .Select(i => i.Id) + .Distinct() + .ToArray(); + } + + private IEnumerable<BaseItem> FlattenItems(IEnumerable<BaseItem> items, List<Guid> expandedFolders) + { + return items + .SelectMany(i => FlattenItems(i, expandedFolders)); + } + + private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders) + { + var boxset = item as BoxSet; + if (boxset != null) + { + if (!expandedFolders.Contains(item.Id)) + { + expandedFolders.Add(item.Id); + + return FlattenItems(boxset.GetLinkedChildren(), expandedFolders); + } + + return new BaseItem[] { }; + } + + return new[] { item }; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs new file mode 100644 index 000000000..878b1b860 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -0,0 +1,203 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.Movies +{ + /// <summary> + /// Class Movie + /// </summary> + public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping + { + public Guid[] SpecialFeatureIds { get; set; } + + public Movie() + { + SpecialFeatureIds = new Guid[] {}; + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets or sets the name of the TMDB collection. + /// </summary> + /// <value>The name of the TMDB collection.</value> + public string TmdbCollectionName { get; set; } + + [IgnoreDataMember] + public string CollectionName + { + get { return TmdbCollectionName; } + set { TmdbCollectionName = value; } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + // hack for tv plugins + if (SourceType == SourceType.Channel) + { + return 0; + } + + double value = 2; + value /= 3; + + return value; + } + + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + // Must have a parent to have special features + // In other words, it must be part of the Parent/Child tree + if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder) + { + var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + if (specialFeaturesChanged) + { + hasChanges = true; + } + } + + return hasChanges; + } + + private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList(); + var newItemIds = newItems.Select(i => i.Id).ToArray(); + + var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds); + + var ownerId = Id; + + var tasks = newItems.Select(i => + { + var subOptions = new MetadataRefreshOptions(options); + + if (i.OwnerId != ownerId) + { + i.OwnerId = ownerId; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + SpecialFeatureIds = newItemIds; + + return itemsChanged; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Movie; + } + + public MovieInfo GetLookupInfo() + { + var info = GetItemLookupInfo<MovieInfo>(); + + if (!IsInMixedFolder) + { + var name = System.IO.Path.GetFileName(ContainingFolderPath); + + if (VideoType == VideoType.VideoFile || VideoType == VideoType.Iso) + { + if (string.Equals(name, System.IO.Path.GetFileName(Path), StringComparison.OrdinalIgnoreCase)) + { + // if the folder has the file extension, strip it + name = System.IO.Path.GetFileNameWithoutExtension(name); + } + } + + info.Name = name; + } + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + else + { + // Try to get the year from the folder name + if (!IsInMixedFolder) + { + info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); + + yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + } + } + + return hasChanges; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + }); + } + + return list; + } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs new file mode 100644 index 000000000..78f9d0671 --- /dev/null +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -0,0 +1,79 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Controller.Entities +{ + public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo> + { + [IgnoreDataMember] + public string[] Artists { get; set; } + + public MusicVideo() + { + Artists = new string[] {}; + } + + [IgnoreDataMember] + public string[] AllArtists + { + get + { + return Artists; + } + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + + public MusicVideoInfo GetLookupInfo() + { + var info = GetItemLookupInfo<MusicVideoInfo>(); + + info.Artists = Artists; + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + else + { + // Try to get the year from the folder name + if (!IsInMixedFolder) + { + info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); + + yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs new file mode 100644 index 000000000..9f85b2aea --- /dev/null +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -0,0 +1,119 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Entities +{ + public static class PeopleHelper + { + public static void AddPerson(List<PersonInfo> people, PersonInfo person) + { + if (person == null) + { + throw new ArgumentNullException("person"); + } + + if (string.IsNullOrEmpty(person.Name)) + { + throw new ArgumentNullException(); + } + + // Normalize + if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.GuestStar; + } + else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.Director; + } + else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.Producer; + } + else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) + { + person.Type = PersonType.Writer; + } + + // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes + if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + { + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase)); + + if (existing != null) + { + existing.Type = PersonType.GuestStar; + MergeExisting(existing, person); + return; + } + } + + if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) + { + // If the actor already exists without a role and we have one, fill it in + var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))); + if (existing == null) + { + // Wasn't there - add it + people.Add(person); + } + else + { + // Was there, if no role and we have one - fill it in + if (string.IsNullOrEmpty(existing.Role) && !string.IsNullOrEmpty(person.Role)) + { + existing.Role = person.Role; + } + + MergeExisting(existing, person); + } + } + else + { + var existing = people.FirstOrDefault(p => + string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) && + string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase)); + + // Check for dupes based on the combination of Name and Type + if (existing == null) + { + people.Add(person); + } + else + { + MergeExisting(existing, person); + } + } + } + + private static void MergeExisting(PersonInfo existing, PersonInfo person) + { + existing.SortOrder = person.SortOrder ?? existing.SortOrder; + existing.ImageUrl = person.ImageUrl ?? existing.ImageUrl; + + foreach (var id in person.ProviderIds) + { + existing.SetProviderId(id.Key, id.Value); + } + } + + public static bool ContainsPerson(List<PersonInfo> people, string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException("name"); + } + + foreach (var i in people) + { + if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs new file mode 100644 index 000000000..64d775094 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -0,0 +1,216 @@ +using MediaBrowser.Controller.Providers; +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is the full Person object that can be retrieved with all of it's data. + /// </summary> + public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo> + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + public PersonLookupInfo GetLookupInfo() + { + return GetItemLookupInfo<PersonLookupInfo>(); + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.PersonIds = new[] { Id }; + + return LibraryManager.GetItemList(query); + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + [IgnoreDataMember] + public override bool EnableAlphaNumericSorting + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validFilename = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + string subFolderPrefix = null; + + foreach (char c in validFilename) + { + if (char.IsLetterOrDigit(c)) + { + subFolderPrefix = c.ToString(); + break; + } + } + + var path = ConfigurationManager.ApplicationPaths.PeoplePath; + + return string.IsNullOrEmpty(subFolderPrefix) ? + System.IO.Path.Combine(path, validFilename) : + System.IO.Path.Combine(path, subFolderPrefix, validFilename); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } + + /// <summary> + /// This is the small Person stub that is attached to BaseItems + /// </summary> + public class PersonInfo : IHasProviderIds + { + public PersonInfo() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets the role. + /// </summary> + /// <value>The role.</value> + public string Role { get; set; } + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the sort order - ascending + /// </summary> + /// <value>The sort order.</value> + public int? SortOrder { get; set; } + + public string ImageUrl { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + /// <summary> + /// Returns a <see cref="System.String" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="System.String" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public bool IsType(string type) + { + return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs new file mode 100644 index 000000000..01c10831d --- /dev/null +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -0,0 +1,104 @@ +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class Photo : BaseItem + { + [IgnoreDataMember] + public override bool SupportsLocalMetadata + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Photo; + } + } + + [IgnoreDataMember] + public override Folder LatestItemsIndexContainer + { + get + { + return AlbumEntity; + } + } + + + [IgnoreDataMember] + public PhotoAlbum AlbumEntity + { + get + { + var parents = GetParents(); + foreach (var parent in parents) + { + var photoAlbum = parent as PhotoAlbum; + if (photoAlbum != null) + { + return photoAlbum; + } + } + return null; + } + } + + public override bool CanDownload() + { + return true; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + if (Width.HasValue && Height.HasValue) + { + double width = Width.Value; + double height = Height.Value; + + if (Orientation.HasValue) + { + switch (Orientation.Value) + { + case ImageOrientation.LeftBottom: + case ImageOrientation.LeftTop: + case ImageOrientation.RightBottom: + case ImageOrientation.RightTop: + var temp = height; + height = width; + width = temp; + break; + } + } + + width /= Height.Value; + return width; + } + + return base.GetDefaultPrimaryImageAspectRatio(); + } + + public int? Width { get; set; } + public int? Height { get; set; } + public string CameraMake { get; set; } + public string CameraModel { get; set; } + public string Software { get; set; } + public double? ExposureTime { get; set; } + public double? FocalLength { get; set; } + public ImageOrientation? Orientation { get; set; } + public double? Aperture { get; set; } + public double? ShutterSpeed { get; set; } + + public double? Latitude { get; set; } + public double? Longitude { get; set; } + public double? Altitude { get; set; } + public int? IsoSpeedRating { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs new file mode 100644 index 000000000..af9d8c801 --- /dev/null +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + public class PhotoAlbum : Folder + { + [IgnoreDataMember] + public override bool AlwaysScanInternalMetadataPath + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs new file mode 100644 index 000000000..4ea0b1ea6 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasShares + { + Share[] Shares { get; set; } + } + + public class Share + { + public string UserId { get; set; } + public bool CanEdit { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/SourceType.cs b/MediaBrowser.Controller/Entities/SourceType.cs new file mode 100644 index 000000000..9c307b4e6 --- /dev/null +++ b/MediaBrowser.Controller/Entities/SourceType.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Entities +{ + public enum SourceType + { + Library = 0, + Channel = 1, + LiveTV = 2 + } +} diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs new file mode 100644 index 000000000..29f617539 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Studio + /// </summary> + public class Studio : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); + return list; + } + public override string CreatePresentationUniqueKey() + { + return GetUserDataKeys()[0]; + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + [IgnoreDataMember] + public override bool IsDisplayedAsFolder + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 16; + value /= 9; + + return value; + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + query.StudioIds = new[] { Id }; + + return LibraryManager.GetItemList(query); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs new file mode 100644 index 000000000..201579731 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -0,0 +1,374 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.TV +{ + /// <summary> + /// Class Episode + /// </summary> + public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries + { + public Episode() + { + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// Gets the season in which it aired. + /// </summary> + /// <value>The aired season.</value> + public int? AirsBeforeSeasonNumber { get; set; } + public int? AirsAfterSeasonNumber { get; set; } + public int? AirsBeforeEpisodeNumber { get; set; } + + /// <summary> + /// This is the ending episode number for double episodes. + /// </summary> + /// <value>The index number.</value> + public int? IndexNumberEnd { get; set; } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesName : series.SortName; + } + + [IgnoreDataMember] + protected override bool SupportsOwnedItems + { + get + { + return IsStacked || MediaSourceCount > 1; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + [IgnoreDataMember] + public int? AiredSeasonNumber + { + get + { + return AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber; + } + } + + [IgnoreDataMember] + public override Folder LatestItemsIndexContainer + { + get + { + return Series; + } + } + + [IgnoreDataMember] + public override Guid DisplayParentId + { + get + { + return SeasonId; + } + } + + [IgnoreDataMember] + protected override bool EnableDefaultVideoUserDataKeys + { + get + { + return false; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + // hack for tv plugins + if (SourceType == SourceType.Channel) + { + return 0; + } + + double value = 16; + value /= 9; + + return value; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var series = Series; + if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) + { + var seriesUserDataKeys = series.GetUserDataKeys(); + var take = seriesUserDataKeys.Count; + if (seriesUserDataKeys.Count > 1) + { + take--; + } + list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"))); + } + + return list; + } + + /// <summary> + /// This Episode's Series Instance + /// </summary> + /// <value>The series.</value> + [IgnoreDataMember] + public Series Series + { + get + { + var seriesId = SeriesId; + if (seriesId.Equals(Guid.Empty)) { + seriesId = FindSeriesId(); + } + return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; + } + } + + [IgnoreDataMember] + public Season Season + { + get + { + var seasonId = SeasonId; + if (seasonId.Equals(Guid.Empty)) { + seasonId = FindSeasonId(); + } + return !seasonId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seasonId) as Season) : null; + } + } + + [IgnoreDataMember] + public bool IsInSeasonFolder + { + get + { + return FindParent<Season>() != null; + } + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + + [IgnoreDataMember] + public string SeriesName { get; set; } + + [IgnoreDataMember] + public string SeasonName { get; set; } + + public string FindSeriesPresentationUniqueKey() + { + var series = Series; + return series == null ? null : series.PresentationUniqueKey; + } + + public string FindSeasonName() + { + var season = Season; + + if (season == null) + { + if (ParentIndexNumber.HasValue) + { + return "Season " + ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture); + } + return "Season Unknown"; + } + + return season.Name; + } + + public string FindSeriesName() + { + var series = Series; + return series == null ? SeriesName : series.Name; + } + + public Guid FindSeasonId() + { + var season = FindParent<Season>(); + + // Episodes directly in series folder + if (season == null) + { + var series = Series; + + if (series != null && ParentIndexNumber.HasValue) + { + var findNumber = ParentIndexNumber.Value; + + season = series.Children + .OfType<Season>() + .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber); + } + } + + return season == null ? Guid.Empty : season.Id; + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected override string CreateSortName() + { + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "") + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + } + + /// <summary> + /// Determines whether [contains episode number] [the specified number]. + /// </summary> + /// <param name="number">The number.</param> + /// <returns><c>true</c> if [contains episode number] [the specified number]; otherwise, <c>false</c>.</returns> + public bool ContainsEpisodeNumber(int number) + { + if (IndexNumber.HasValue) + { + if (IndexNumberEnd.HasValue) + { + return number >= IndexNumber.Value && number <= IndexNumberEnd.Value; + } + + return IndexNumber.Value == number; + } + + return false; + } + + [IgnoreDataMember] + public override bool SupportsRemoteImageDownloading + { + get + { + if (IsMissingEpisode) + { + return false; + } + + return true; + } + } + + [IgnoreDataMember] + public bool IsMissingEpisode + { + get + { + return LocationType == LocationType.Virtual; + } + } + + [IgnoreDataMember] + public Guid SeasonId { get; set; } + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public Guid FindSeriesId() + { + var series = FindParent<Series>(); + return series == null ? Guid.Empty : series.Id; + } + + public override IEnumerable<Guid> GetAncestorIds() + { + var list = base.GetAncestorIds().ToList(); + + var seasonId = SeasonId; + + if (!seasonId.Equals(Guid.Empty) && !list.Contains(seasonId)) + { + list.Add(seasonId); + } + + return list; + } + + public override IEnumerable<FileSystemMetadata> GetDeletePaths() + { + return new[] { + new FileSystemMetadata + { + FullName = Path, + IsDirectory = IsFolder + } + }.Concat(GetLocalMetadataFilesToDelete()); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + + public EpisodeInfo GetLookupInfo() + { + var id = GetItemLookupInfo<EpisodeInfo>(); + + var series = Series; + + if (series != null) + { + id.SeriesProviderIds = series.ProviderIds; + id.SeriesDisplayOrder = series.DisplayOrder; + } + + id.IsMissingEpisode = IsMissingEpisode; + id.IndexNumberEnd = IndexNumberEnd; + + return id; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!IsLocked) + { + if (SourceType == SourceType.Library) + { + try + { + if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata)) + { + hasChanges = true; + } + } + catch (Exception ex) + { + Logger.ErrorException("Error in FillMissingEpisodeNumbersFromPath. Episode: {0}", ex, Path ?? Name ?? Id.ToString()); + } + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs new file mode 100644 index 000000000..b5f77df55 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -0,0 +1,273 @@ +using System; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Users; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.TV +{ + /// <summary> + /// Class Season + /// </summary> + public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> + { + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsDateLastMediaAdded + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get { return true; } + } + + [IgnoreDataMember] + public override Guid DisplayParentId + { + get { return SeriesId; } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesName : series.SortName; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var series = Series; + if (series != null) + { + list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000"))); + } + + return list; + } + + public override int GetChildCount(User user) + { + var result = GetChildren(user, true).Count; + + return result; + } + + /// <summary> + /// This Episode's Series Instance + /// </summary> + /// <value>The series.</value> + [IgnoreDataMember] + public Series Series + { + get + { + var seriesId = SeriesId; + if (seriesId.Equals(Guid.Empty)) { + seriesId = FindSeriesId(); + } + return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; + } + } + + [IgnoreDataMember] + public string SeriesPath + { + get + { + var series = Series; + + if (series != null) + { + return series.Path; + } + + return FileSystem.GetDirectoryName(Path); + } + } + + public override string CreatePresentationUniqueKey() + { + if (IndexNumber.HasValue) + { + var series = Series; + if (series != null) + { + return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); + } + } + + return base.CreatePresentationUniqueKey(); + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + protected override string CreateSortName() + { + return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name; + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + if (query.User == null) + { + return base.GetItemsInternal(query); + } + + var user = query.User; + + Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + + var items = GetEpisodes(user, query.DtoOptions).Where(filter); + + return PostFilterAndSort(items, query, false); + } + + /// <summary> + /// Gets the episodes. + /// </summary> + public List<BaseItem> GetEpisodes(User user, DtoOptions options) + { + return GetEpisodes(Series, user, options); + } + + public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options) + { + return GetEpisodes(series, user, null, options); + } + + public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options) + { + return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options); + } + + public List<BaseItem> GetEpisodes() + { + return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true)); + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + return GetEpisodes(user, new DtoOptions(true)); + } + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + // Don't block. Let either the entire series rating or episode rating determine it + return false; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + + [IgnoreDataMember] + public string SeriesPresentationUniqueKey { get; set; } + + [IgnoreDataMember] + public string SeriesName { get; set; } + + [IgnoreDataMember] + public Guid SeriesId { get; set; } + + public string FindSeriesPresentationUniqueKey() + { + var series = Series; + return series == null ? null : series.PresentationUniqueKey; + } + + public string FindSeriesName() + { + var series = Series; + return series == null ? SeriesName : series.Name; + } + + public Guid FindSeriesId() + { + var series = FindParent<Series>(); + return series == null ? Guid.Empty: series.Id; + } + + /// <summary> + /// Gets the lookup information. + /// </summary> + /// <returns>SeasonInfo.</returns> + public SeasonInfo GetLookupInfo() + { + var id = GetItemLookupInfo<SeasonInfo>(); + + var series = Series; + + if (series != null) + { + id.SeriesProviderIds = series.ProviderIds; + } + + return id; + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) + { + IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path); + + // If a change was made record it + if (IndexNumber.HasValue) + { + hasChanges = true; + } + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs new file mode 100644 index 000000000..88fde1760 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -0,0 +1,540 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities.TV +{ + /// <summary> + /// Class Series + /// </summary> + public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer + { + public Series() + { + RemoteTrailers = EmptyMediaUrlArray; + LocalTrailerIds = new Guid[] {}; + RemoteTrailerIds = new Guid[] {}; + AirDays = new DayOfWeek[] { }; + } + + public DayOfWeek[] AirDays { get; set; } + public string AirTime { get; set; } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsDateLastMediaAdded + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + public Guid[] LocalTrailerIds { get; set; } + public Guid[] RemoteTrailerIds { get; set; } + + public MediaUrl[] RemoteTrailers { get; set; } + + /// <summary> + /// airdate, dvd or absolute + /// </summary> + public string DisplayOrder { get; set; } + + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public SeriesStatus? Status { get; set; } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public override string CreatePresentationUniqueKey() + { + if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping) + { + var userdatakeys = GetUserDataKeys(); + + if (userdatakeys.Count > 1) + { + return AddLibrariesToPresentationUniqueKey(userdatakeys[0]); + } + } + + return base.CreatePresentationUniqueKey(); + } + + private string AddLibrariesToPresentationUniqueKey(string key) + { + var lang = GetPreferredMetadataLanguage(); + if (!string.IsNullOrEmpty(lang)) + { + key += "-" + lang; + } + + var folders = LibraryManager.GetCollectionFolders(this) + .Select(i => i.Id.ToString("N")) + .ToArray(); + + if (folders.Length == 0) + { + return key; + } + + return key + "-" + string.Join("-", folders); + } + + private static string GetUniqueSeriesKey(BaseItem series) + { + return series.GetPresentationUniqueKey(); + } + + public override int GetChildCount(User user) + { + var seriesKey = GetUniqueSeriesKey(this); + + var result = LibraryManager.GetCount(new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] { typeof(Season).Name }, + IsVirtualItem = false, + Limit = 0, + DtoOptions = new Dto.DtoOptions(false) + { + EnableImages = false + } + }); + + return result; + } + + public override int GetRecursiveChildCount(User user) + { + var seriesKey = GetUniqueSeriesKey(this); + + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + DtoOptions = new Dto.DtoOptions(false) + { + EnableImages = false + } + }; + + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + } + query.IsVirtualItem = false; + query.Limit = 0; + var totalRecordCount = LibraryManager.GetCount(query); + + return totalRecordCount; + } + + /// <summary> + /// Gets the user data key. + /// </summary> + /// <returns>System.String.</returns> + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var key = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + key = this.GetProviderId(MetadataProviders.Tvdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + return list; + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + return GetSeasons(user, new DtoOptions(true)); + } + + public List<BaseItem> GetSeasons(User user, DtoOptions options) + { + var query = new InternalItemsQuery(user) + { + DtoOptions = options + }; + + SetSeasonQueryOptions(query, user); + + return LibraryManager.GetItemList(query); + } + + private void SetSeasonQueryOptions(InternalItemsQuery query, User user) + { + var seriesKey = GetUniqueSeriesKey(this); + + query.AncestorWithPresentationUniqueKey = null; + query.SeriesPresentationUniqueKey = seriesKey; + query.IncludeItemTypes = new[] { typeof(Season).Name }; + query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + + if (user != null) + { + var config = user.Configuration; + + if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + } + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + var user = query.User; + + if (query.Recursive) + { + var seriesKey = GetUniqueSeriesKey(this); + + query.AncestorWithPresentationUniqueKey = null; + query.SeriesPresentationUniqueKey = seriesKey; + if (query.OrderBy.Length == 0) + { + query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + } + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }; + } + query.IsVirtualItem = false; + return LibraryManager.GetItemsResult(query); + } + + SetSeasonQueryOptions(query, user); + + return LibraryManager.GetItemsResult(query); + } + + public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options) + { + var seriesKey = GetUniqueSeriesKey(this); + + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }, + OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + DtoOptions = options + }; + var config = user.Configuration; + if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + + var allItems = LibraryManager.GetItemList(query); + + var allSeriesEpisodes = allItems.OfType<Episode>().ToList(); + + var allEpisodes = allItems.OfType<Season>() + .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options)) + .Reverse(); + + // Specials could appear twice based on above - once in season 0, once in the aired season + // This depends on settings for that series + // When this happens, remove the duplicate from season 0 + + return allEpisodes.DistinctBy(i => i.Id).Reverse(); + } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) + { + // Refresh bottom up, children first, then the boxset + // By then hopefully the movies within will have Tmdb collection values + var items = GetRecursiveChildren(); + + var totalItems = items.Count; + var numComplete = 0; + + // Refresh seasons + foreach (var item in items) + { + if (!(item is Season)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (refreshOptions.RefreshItem(item)) + { + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 100); + } + + // Refresh episodes and other children + foreach (var item in items) + { + if ((item is Season)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var skipItem = false; + + var episode = item as Episode; + + if (episode != null + && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh + && !refreshOptions.ReplaceAllMetadata + && episode.IsMissingEpisode + && episode.LocationType == LocationType.Virtual + && episode.PremiereDate.HasValue + && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30) + { + skipItem = true; + } + + if (!skipItem) + { + if (refreshOptions.RefreshItem(item)) + { + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } + } + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 100); + } + + refreshOptions = new MetadataRefreshOptions(refreshOptions); + await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); + } + + public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options) + { + var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons; + + // add optimization when this setting is not enabled + var seriesKey = queryFromSeries ? + GetUniqueSeriesKey(this) : + GetUniqueSeriesKey(parentSeason); + + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey, + SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null, + IncludeItemTypes = new[] { typeof(Episode).Name }, + OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + DtoOptions = options + }; + if (user != null) + { + var config = user.Configuration; + if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + } + + var allItems = LibraryManager.GetItemList(query); + + return GetSeasonEpisodes(parentSeason, user, allItems, options); + } + + public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options) + { + if (allSeriesEpisodes == null) + { + return GetSeasonEpisodes(parentSeason, user, options); + } + + var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons); + + var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder; + + return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList(); + } + + /// <summary> + /// Filters the episodes by season. + /// </summary> + public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials) + { + var seasonNumber = parentSeason.IndexNumber; + var seasonPresentationKey = GetUniqueSeriesKey(parentSeason); + + var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0; + + return episodes.Where(episode => + { + var episodeItem = (Episode)episode; + + var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexNumber; + if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.Value) + { + return true; + } + + if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual) + { + return true; + } + + var season = episodeItem.Season; + return season != null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase); + }); + } + + /// <summary> + /// Filters the episodes by season. + /// </summary> + public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials) + { + if (!includeSpecials || seasonNumber < 1) + { + return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber); + } + + return episodes.Where(i => + { + var episode = i; + + if (episode != null) + { + var currentSeasonNumber = episode.AiredSeasonNumber; + + return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber; + } + + return false; + }); + } + + + protected override bool GetBlockUnratedValue(UserPolicy config) + { + return config.BlockUnratedItems.Contains(UnratedItem.Series); + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + + public SeriesInfo GetLookupInfo() + { + var info = GetItemLookupInfo<SeriesInfo>(); + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + + return hasChanges; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/shows/{0}", imdbId) + }); + } + + return list; + } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs new file mode 100644 index 000000000..e5d8f35d9 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + public static class TagExtensions + { + public static void AddTag(this BaseItem item, string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + var current = item.Tags; + + if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + if (current.Length == 0) + { + item.Tags = new[] { name }; + } + else + { + var list = current.ToArray(current.Length + 1); + list[list.Length - 1] = name; + + item.Tags = list; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs new file mode 100644 index 000000000..4f2a5631b --- /dev/null +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -0,0 +1,111 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Trailer + /// </summary> + public class Trailer : Video, IHasLookupInfo<TrailerInfo> + { + public Trailer() + { + TrailerTypes = new TrailerType[] { }; + } + + public TrailerType[] TrailerTypes { get; set; } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Trailer; + } + + public TrailerInfo GetLookupInfo() + { + var info = GetItemLookupInfo<TrailerInfo>(); + + if (!IsInMixedFolder && IsFileProtocol) + { + info.Name = System.IO.Path.GetFileName(ContainingFolderPath); + } + + return info; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (!ProductionYear.HasValue) + { + var info = LibraryManager.ParseName(Name); + + var yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + else + { + // Try to get the year from the folder name + if (!IsInMixedFolder) + { + info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath)); + + yearInName = info.Year; + + if (yearInName.HasValue) + { + ProductionYear = yearInName; + hasChanges = true; + } + } + } + } + + return hasChanges; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + }); + } + + return list; + } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs new file mode 100644 index 000000000..f569c8021 --- /dev/null +++ b/MediaBrowser.Controller/Entities/User.cs @@ -0,0 +1,342 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Connect; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class User + /// </summary> + public class User : BaseItem + { + public static IUserManager UserManager { get; set; } + public static IXmlSerializer XmlSerializer { get; set; } + + /// <summary> + /// From now on all user paths will be Id-based. + /// This is for backwards compatibility. + /// </summary> + public bool UsesIdForConfigurationPath { get; set; } + + /// <summary> + /// Gets or sets the password. + /// </summary> + /// <value>The password.</value> + public string Password { get; set; } + public string EasyPassword { get; set; } + public string Salt { get; set; } + + public string ConnectUserName { get; set; } + public string ConnectUserId { get; set; } + public UserLinkType? ConnectLinkType { get; set; } + public string ConnectAccessKey { get; set; } + + // Strictly to remove IgnoreDataMember + public override ItemImageInfo[] ImageInfos + { + get + { + return base.ImageInfos; + } + set + { + base.ImageInfos = value; + } + } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + [IgnoreDataMember] + public override string Path + { + get + { + // Return this so that metadata providers will look in here + return ConfigurationDirectoryPath; + } + set + { + base.Path = value; + } + } + + private string _name; + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public override string Name + { + get + { + return _name; + } + set + { + _name = value; + + // lazy load this again + SortName = null; + } + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + /// <summary> + /// Gets the root folder. + /// </summary> + /// <value>The root folder.</value> + [IgnoreDataMember] + public Folder RootFolder + { + get + { + return LibraryManager.GetUserRootFolder(); + } + } + + /// <summary> + /// Gets or sets the last login date. + /// </summary> + /// <value>The last login date.</value> + public DateTime? LastLoginDate { get; set; } + /// <summary> + /// Gets or sets the last activity date. + /// </summary> + /// <value>The last activity date.</value> + public DateTime? LastActivityDate { get; set; } + + private volatile UserConfiguration _config; + private readonly object _configSyncLock = new object(); + [IgnoreDataMember] + public UserConfiguration Configuration + { + get + { + if (_config == null) + { + lock (_configSyncLock) + { + if (_config == null) + { + _config = UserManager.GetUserConfiguration(this); + } + } + } + + return _config; + } + set { _config = value; } + } + + private volatile UserPolicy _policy; + private readonly object _policySyncLock = new object(); + [IgnoreDataMember] + public UserPolicy Policy + { + get + { + if (_policy == null) + { + lock (_policySyncLock) + { + if (_policy == null) + { + _policy = UserManager.GetUserPolicy(this); + } + } + } + + return _policy; + } + set { _policy = value; } + } + + /// <summary> + /// Renames the user. + /// </summary> + /// <param name="newName">The new name.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public Task Rename(string newName) + { + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException("newName"); + } + + // If only the casing is changing, leave the file system alone + if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) + { + UsesIdForConfigurationPath = true; + + // Move configuration + var newConfigDirectory = GetConfigurationDirectoryPath(newName); + var oldConfigurationDirectory = ConfigurationDirectoryPath; + + // Exceptions will be thrown if these paths already exist + if (FileSystem.DirectoryExists(newConfigDirectory)) + { + FileSystem.DeleteDirectory(newConfigDirectory, true); + } + + if (FileSystem.DirectoryExists(oldConfigurationDirectory)) + { + FileSystem.MoveDirectory(oldConfigurationDirectory, newConfigDirectory); + } + else + { + FileSystem.CreateDirectory(newConfigDirectory); + } + } + + Name = newName; + + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + { + ReplaceAllMetadata = true, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = true + + }, CancellationToken.None); + } + + public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + UserManager.UpdateUser(this); + } + + /// <summary> + /// Gets the path to the user's configuration directory + /// </summary> + /// <value>The configuration directory path.</value> + [IgnoreDataMember] + public string ConfigurationDirectoryPath + { + get + { + return GetConfigurationDirectoryPath(Name); + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + /// <summary> + /// Gets the configuration directory path. + /// </summary> + /// <param name="username">The username.</param> + /// <returns>System.String.</returns> + private string GetConfigurationDirectoryPath(string username) + { + var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; + + // Legacy + if (!UsesIdForConfigurationPath) + { + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentNullException("username"); + } + + var safeFolderName = FileSystem.GetValidFilename(username); + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); + } + + return System.IO.Path.Combine(parentPath, Id.ToString("N")); + } + + public bool IsParentalScheduleAllowed() + { + return IsParentalScheduleAllowed(DateTime.UtcNow); + } + + public bool IsParentalScheduleAllowed(DateTime date) + { + var schedules = Policy.AccessSchedules; + + if (schedules.Length == 0) + { + return true; + } + + foreach (var i in schedules) + { + if (IsParentalScheduleAllowed(i, date)) + { + return true; + } + } + return false; + } + + private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) + { + if (date.Kind != DateTimeKind.Utc) + { + throw new ArgumentException("Utc date expected"); + } + + var localTime = date.ToLocalTime(); + + return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) && + IsWithinTime(schedule, localTime); + } + + private bool IsWithinTime(AccessSchedule schedule, DateTime localTime) + { + var hour = localTime.TimeOfDay.TotalHours; + + return hour >= schedule.StartHour && hour <= schedule.EndHour; + } + + public bool IsFolderGrouped(Guid id) + { + foreach (var i in Configuration.GroupedFolders) + { + if (new Guid(i) == id) + { + return true; + } + } + return false; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public long InternalId { get; set;} + + + } +} diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs new file mode 100644 index 000000000..0e1326949 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -0,0 +1,124 @@ +using System; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class UserItemData + /// </summary> + public class UserItemData + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the key. + /// </summary> + /// <value>The key.</value> + public string Key { get; set; } + + /// <summary> + /// The _rating + /// </summary> + private double? _rating; + /// <summary> + /// Gets or sets the users 0-10 rating + /// </summary> + /// <value>The rating.</value> + /// <exception cref="System.ArgumentOutOfRangeException">Rating;A 0 to 10 rating is required for UserItemData.</exception> + public double? Rating + { + get + { + return _rating; + } + set + { + if (value.HasValue) + { + if (value.Value < 0 || value.Value > 10) + { + throw new ArgumentOutOfRangeException("value", "A 0 to 10 rating is required for UserItemData."); + } + } + + _rating = value; + } + } + + /// <summary> + /// Gets or sets the playback position ticks. + /// </summary> + /// <value>The playback position ticks.</value> + public long PlaybackPositionTicks { get; set; } + + /// <summary> + /// Gets or sets the play count. + /// </summary> + /// <value>The play count.</value> + public int PlayCount { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is favorite. + /// </summary> + /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value> + public bool IsFavorite { get; set; } + + /// <summary> + /// Gets or sets the last played date. + /// </summary> + /// <value>The last played date.</value> + public DateTime? LastPlayedDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UserItemData" /> is played. + /// </summary> + /// <value><c>true</c> if played; otherwise, <c>false</c>.</value> + public bool Played { get; set; } + /// <summary> + /// Gets or sets the index of the audio stream. + /// </summary> + /// <value>The index of the audio stream.</value> + public int? AudioStreamIndex { get; set; } + /// <summary> + /// Gets or sets the index of the subtitle stream. + /// </summary> + /// <value>The index of the subtitle stream.</value> + public int? SubtitleStreamIndex { get; set; } + + public const double MinLikeValue = 6.5; + + /// <summary> + /// This is an interpreted property to indicate likes or dislikes + /// This should never be serialized. + /// </summary> + /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool? Likes + { + get + { + if (Rating != null) + { + return Rating >= MinLikeValue; + } + + return null; + } + set + { + if (value.HasValue) + { + Rating = value.Value ? 10 : 1; + } + else + { + Rating = null; + } + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs new file mode 100644 index 000000000..f8e843d92 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -0,0 +1,158 @@ +using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Special class used for User Roots. Children contain actual ones defined for this user + /// PLUS the virtual folders from the physical root (added by plug-ins). + /// </summary> + public class UserRootFolder : Folder + { + private List<Guid> _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override List<BaseItem> LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null) + { + var list = base.LoadChildren(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + if (query.Recursive) + { + return QueryRecursive(query); + } + + var result = UserViewManager.GetUserViews(new UserViewQuery + { + UserId = query.User.Id, + PresetViews = query.PresetViews + }); + + var itemsArray = result; + var totalCount = itemsArray.Length; + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = itemsArray + }; + } + + public override int GetChildCount(User user) + { + return GetChildren(user, true).Count; + } + + [IgnoreDataMember] + protected override bool SupportsShortcutChildren + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList(); + list.AddRange(LibraryManager.RootFolder.VirtualChildren); + + return list; + } + + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + ClearCache(); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase)) + { + Name = "Media Folders"; + hasChanges = true; + } + + return hasChanges; + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + ClearCache(); + + return base.GetNonCachedChildren(directoryService); + } + + protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + ClearCache(); + + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + .ConfigureAwait(false); + + ClearCache(); + + // Not the best way to handle this, but it solves an issue + // CollectionFolders aren't always getting saved after changes + // This means that grabbing the item by Id may end up returning the old one + // Fix is in two places - make sure the folder gets saved + // And here to remedy it for affected users. + // In theory this can be removed eventually. + foreach (var item in Children) + { + LibraryManager.RegisterItem(item); + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs new file mode 100644 index 000000000..984cad481 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -0,0 +1,210 @@ +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Collections; + +namespace MediaBrowser.Controller.Entities +{ + public class UserView : Folder, IHasCollectionType + { + public string ViewType { get; set; } + public Guid DisplayParentId { get; set; } + + public Guid? UserId { get; set; } + + public static ITVSeriesManager TVSeriesManager; + public static IPlaylistManager PlaylistManager; + + [IgnoreDataMember] + public string CollectionType + { + get + { + return ViewType; + } + } + + public override IEnumerable<Guid> GetIdsForAncestorQuery() + { + var list = new List<Guid>(); + + if (!DisplayParentId.Equals(Guid.Empty)) + { + list.Add(DisplayParentId); + } + else if (!ParentId.Equals(Guid.Empty)) + { + list.Add(ParentId); + } + else + { + list.Add(Id); + } + return list; + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return false; + } + } + + //public override double? GetDefaultPrimaryImageAspectRatio() + //{ + // double value = 16; + // value /= 9; + + // return value; + //} + + public override int GetChildCount(User user) + { + return GetChildren(user, true).Count; + } + + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) + { + var parent = this as Folder; + + if (!DisplayParentId.Equals(Guid.Empty)) + { + parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent; + } + else if (!ParentId.Equals(Guid.Empty)) + { + parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; + } + + return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager) + .GetUserItems(parent, this, CollectionType, query); + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + if (query == null) + { + query = new InternalItemsQuery(user); + } + + query.EnableTotalRecordCount = false; + var result = GetItemList(query); + + return result.ToList(); + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + query.SetUser(user); + query.Recursive = true; + query.EnableTotalRecordCount = false; + query.ForceDirect = true; + + return GetItemList(query); + } + + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + return GetChildren(user, false); + } + + private static string[] UserSpecificViewTypes = new string[] + { + MediaBrowser.Model.Entities.CollectionType.Playlists + }; + + public static bool IsUserSpecific(Folder folder) + { + var collectionFolder = folder as ICollectionFolder; + + if (collectionFolder == null) + { + return false; + } + + var supportsUserSpecific = folder as ISupportsUserSpecificView; + if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView) + { + return true; + } + + return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + public static bool IsEligibleForGrouping(Folder folder) + { + var collectionFolder = folder as ICollectionFolder; + return collectionFolder != null && IsEligibleForGrouping(collectionFolder.CollectionType); + } + + private static string[] ViewTypesEligibleForGrouping = new string[] + { + MediaBrowser.Model.Entities.CollectionType.Movies, + MediaBrowser.Model.Entities.CollectionType.TvShows, + string.Empty + }; + + public static bool IsEligibleForGrouping(string viewType) + { + return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + private static string[] OriginalFolderViewTypes = new string[] + { + MediaBrowser.Model.Entities.CollectionType.Games, + MediaBrowser.Model.Entities.CollectionType.Books, + MediaBrowser.Model.Entities.CollectionType.MusicVideos, + MediaBrowser.Model.Entities.CollectionType.HomeVideos, + MediaBrowser.Model.Entities.CollectionType.Photos, + MediaBrowser.Model.Entities.CollectionType.Music, + MediaBrowser.Model.Entities.CollectionType.BoxSets + }; + + public static bool EnableOriginalFolder(string viewType) + { + return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService) + { + return Task.FromResult(true); + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + } +} diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs new file mode 100644 index 000000000..36035a2bb --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -0,0 +1,1070 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Entities +{ + public class UserViewBuilder + { + private readonly IUserViewManager _userViewManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IUserDataManager _userDataManager; + private readonly ITVSeriesManager _tvSeriesManager; + private readonly IServerConfigurationManager _config; + private readonly IPlaylistManager _playlistManager; + + public UserViewBuilder(IUserViewManager userViewManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager) + { + _userViewManager = userViewManager; + _libraryManager = libraryManager; + _logger = logger; + _userDataManager = userDataManager; + _tvSeriesManager = tvSeriesManager; + _config = config; + _playlistManager = playlistManager; + } + + public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) + { + var user = query.User; + + //if (query.IncludeItemTypes != null && + // query.IncludeItemTypes.Length == 1 && + // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) + //{ + // if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + // { + // return await FindPlaylists(queryParent, user, query).ConfigureAwait(false); + // } + //} + + switch (viewType) + { + case CollectionType.Folders: + return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query); + + case CollectionType.TvShows: + return GetTvView(queryParent, user, query); + + case CollectionType.Movies: + return GetMovieFolders(queryParent, user, query); + + case SpecialFolder.TvShowSeries: + return GetTvSeries(queryParent, user, query); + + case SpecialFolder.TvGenres: + return GetTvGenres(queryParent, user, query); + + case SpecialFolder.TvGenre: + return GetTvGenreItems(queryParent, displayParent, user, query); + + case SpecialFolder.TvResume: + return GetTvResume(queryParent, user, query); + + case SpecialFolder.TvNextUp: + return GetTvNextUp(queryParent, query); + + case SpecialFolder.TvLatest: + return GetTvLatest(queryParent, user, query); + + case SpecialFolder.MovieFavorites: + return GetFavoriteMovies(queryParent, user, query); + + case SpecialFolder.MovieLatest: + return GetMovieLatest(queryParent, user, query); + + case SpecialFolder.MovieGenres: + return GetMovieGenres(queryParent, user, query); + + case SpecialFolder.MovieGenre: + return GetMovieGenreItems(queryParent, displayParent, user, query); + + case SpecialFolder.MovieResume: + return GetMovieResume(queryParent, user, query); + + case SpecialFolder.MovieMovies: + return GetMovieMovies(queryParent, user, query); + + case SpecialFolder.MovieCollections: + return GetMovieCollections(queryParent, user, query); + + case SpecialFolder.TvFavoriteEpisodes: + return GetFavoriteEpisodes(queryParent, user, query); + + case SpecialFolder.TvFavoriteSeries: + return GetFavoriteSeries(queryParent, user, query); + + default: + { + if (queryParent is UserView) + { + return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query); + } + return queryParent.GetItems(query); + } + } + } + + private int GetSpecialItemsLimit() + { + return 50; + } + + private QueryResult<BaseItem> GetMovieFolders(Folder parent, User user, InternalItemsQuery query) + { + if (query.Recursive) + { + query.Recursive = true; + query.SetUser(user); + + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + } + + return parent.QueryRecursive(query); + } + + var list = new List<BaseItem>(); + + list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent)); + list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent)); + list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent)); + list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent)); + list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent)); + list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)); + + return GetResult(list, parent, query); + } + + private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetFavoriteSeries(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Series).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetFavoriteEpisodes(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetMovieMovies(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query) + { + query.Parent = null; + query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; + query.SetUser(user); + query.Recursive = true; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.IsResumable = true; + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> ConvertToResult(List<BaseItem> items) + { + var arr = items.ToArray(); + return new QueryResult<BaseItem> + { + Items = arr, + TotalRecordCount = arr.Length + }; + } + + private QueryResult<BaseItem> GetMovieGenres(Folder parent, User user, InternalItemsQuery query) + { + var genres = parent.QueryRecursive(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Movie).Name }, + Recursive = true, + EnableTotalRecordCount = false + + }).Items + .SelectMany(i => i.Genres) + .DistinctNames() + .Select(i => + { + try + { + return _libraryManager.GetGenre(i); + } + catch + { + // Full exception logged at lower levels + _logger.Error("Error getting genre"); + return null; + } + + }) + .Where(i => i != null) + .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent)); + + return GetResult(genres, parent, query); + } + + private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = queryParent; + query.GenreIds = new[] { displayParent.Id }; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Movie).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetTvView(Folder parent, User user, InternalItemsQuery query) + { + if (query.Recursive) + { + query.Recursive = true; + query.SetUser(user); + + if (query.IncludeItemTypes.Length == 0) + { + query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name }; + } + + return parent.QueryRecursive(query); + } + + var list = new List<BaseItem>(); + + list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent)); + list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent)); + list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent)); + list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent)); + list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent)); + list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent)); + list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)); + + return GetResult(list, parent, query); + } + + private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IsVirtualItem = false; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query) + { + var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); + + var result = _tvSeriesManager.GetNextUp(new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + + }, parentFolders, query.DtoOptions); + + return result; + } + + private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query) + { + query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.IsResumable = true; + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.Limit = GetSpecialItemsLimit(); + query.IncludeItemTypes = new[] { typeof(Episode).Name }; + + return ConvertToResult(_libraryManager.GetItemList(query)); + } + + private QueryResult<BaseItem> GetTvSeries(Folder parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Series).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetTvGenres(Folder parent, User user, InternalItemsQuery query) + { + var genres = parent.QueryRecursive(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true, + EnableTotalRecordCount = false + + }).Items + .SelectMany(i => i.Genres) + .DistinctNames() + .Select(i => + { + try + { + return _libraryManager.GetGenre(i); + } + catch + { + // Full exception logged at lower levels + _logger.Error("Error getting genre"); + return null; + } + + }) + .Where(i => i != null) + .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent)); + + return GetResult(genres, parent, query); + } + + private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = queryParent; + query.GenreIds = new[] { displayParent.Id }; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Series).Name }; + + return _libraryManager.GetItemsResult(query); + } + + private QueryResult<BaseItem> GetResult<T>(QueryResult<T> result) + where T : BaseItem + { + return new QueryResult<BaseItem> + { + Items = result.Items, + TotalRecordCount = result.TotalRecordCount + }; + } + + private QueryResult<BaseItem> GetResult<T>(IEnumerable<T> items, + BaseItem queryParent, + InternalItemsQuery query) + where T : BaseItem + { + items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager)); + + return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config); + } + + public static bool FilterItem(BaseItem item, InternalItemsQuery query) + { + return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager); + } + + public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, + BaseItem queryParent, + int? totalRecordLimit, + InternalItemsQuery query, + ILibraryManager libraryManager, + IServerConfigurationManager configurationManager) + { + var user = query.User; + + // This must be the last filter + if (!string.IsNullOrEmpty(query.AdjacentTo)) + { + items = FilterForAdjacency(items.ToList(), query.AdjacentTo); + } + + return SortAndPage(items, totalRecordLimit, query, libraryManager, true); + } + + public static QueryResult<BaseItem> SortAndPage(IEnumerable<BaseItem> items, + int? totalRecordLimit, + InternalItemsQuery query, + ILibraryManager libraryManager, bool enableSorting) + { + if (enableSorting) + { + if (query.OrderBy.Length > 0) + { + items = libraryManager.Sort(items, query.User, query.OrderBy); + } + } + + var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); + var totalCount = itemsArray.Length; + + if (query.Limit.HasValue) + { + itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray(); + } + else if (query.StartIndex.HasValue) + { + itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray(); + } + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = itemsArray + }; + } + + public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) + { + if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.IsVirtualItem.HasValue && item.IsVirtualItem != query.IsVirtualItem.Value) + { + return false; + } + + if (query.IsFolder.HasValue && query.IsFolder.Value != item.IsFolder) + { + return false; + } + + UserItemData userData = null; + + if (query.IsLiked.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + + if (!userData.Likes.HasValue || userData.Likes != query.IsLiked.Value) + { + return false; + } + } + + if (query.IsFavoriteOrLiked.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + var isFavoriteOrLiked = userData.IsFavorite || (userData.Likes ?? false); + + if (isFavoriteOrLiked != query.IsFavoriteOrLiked.Value) + { + return false; + } + } + + if (query.IsFavorite.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + + if (userData.IsFavorite != query.IsFavorite.Value) + { + return false; + } + } + + if (query.IsResumable.HasValue) + { + userData = userData ?? userDataManager.GetUserData(user, item); + var isResumable = userData.PlaybackPositionTicks > 0; + + if (isResumable != query.IsResumable.Value) + { + return false; + } + } + + if (query.IsPlayed.HasValue) + { + if (item.IsPlayed(user) != query.IsPlayed.Value) + { + return false; + } + } + + // Filter by Video3DFormat + if (query.Is3D.HasValue) + { + var val = query.Is3D.Value; + var video = item as Video; + + if (video == null || val != video.Video3DFormat.HasValue) + { + return false; + } + } + + /* + * fuck - fix this + if (query.IsHD.HasValue) + { + if (item.IsHD != query.IsHD.Value) + { + return false; + } + } + */ + + if (query.IsLocked.HasValue) + { + var val = query.IsLocked.Value; + if (item.IsLocked != val) + { + return false; + } + } + + if (query.HasOverview.HasValue) + { + var filterValue = query.HasOverview.Value; + + var hasValue = !string.IsNullOrEmpty(item.Overview); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasImdbId.HasValue) + { + var filterValue = query.HasImdbId.Value; + + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Imdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasTmdbId.HasValue) + { + var filterValue = query.HasTmdbId.Value; + + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasTvdbId.HasValue) + { + var filterValue = query.HasTvdbId.Value; + + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.HasOfficialRating.HasValue) + { + var filterValue = query.HasOfficialRating.Value; + + var hasValue = !string.IsNullOrEmpty(item.OfficialRating); + + if (hasValue != filterValue) + { + return false; + } + } + + if (query.IsPlaceHolder.HasValue) + { + var filterValue = query.IsPlaceHolder.Value; + + var isPlaceHolder = false; + + var hasPlaceHolder = item as ISupportsPlaceHolders; + + if (hasPlaceHolder != null) + { + isPlaceHolder = hasPlaceHolder.IsPlaceHolder; + } + + if (isPlaceHolder != filterValue) + { + return false; + } + } + + if (query.HasSpecialFeature.HasValue) + { + var filterValue = query.HasSpecialFeature.Value; + + var movie = item as IHasSpecialFeatures; + + if (movie != null) + { + var ok = filterValue + ? movie.SpecialFeatureIds.Length > 0 + : movie.SpecialFeatureIds.Length == 0; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (query.HasSubtitles.HasValue) + { + var val = query.HasSubtitles.Value; + + var video = item as Video; + + if (video == null || val != video.HasSubtitles) + { + return false; + } + } + + if (query.HasParentalRating.HasValue) + { + var val = query.HasParentalRating.Value; + + var rating = item.CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = item.OfficialRating; + } + + if (val) + { + if (string.IsNullOrEmpty(rating)) + { + return false; + } + } + else + { + if (!string.IsNullOrEmpty(rating)) + { + return false; + } + } + } + + if (query.HasTrailer.HasValue) + { + var val = query.HasTrailer.Value; + var trailerCount = 0; + + var hasTrailers = item as IHasTrailers; + if (hasTrailers != null) + { + trailerCount = hasTrailers.GetTrailerIds().Count; + } + + var ok = val ? trailerCount > 0 : trailerCount == 0; + + if (!ok) + { + return false; + } + } + + if (query.HasThemeSong.HasValue) + { + var filterValue = query.HasThemeSong.Value; + + var themeCount = item.ThemeSongIds.Length; + var ok = filterValue ? themeCount > 0 : themeCount == 0; + + if (!ok) + { + return false; + } + } + + if (query.HasThemeVideo.HasValue) + { + var filterValue = query.HasThemeVideo.Value; + + var themeCount = item.ThemeVideoIds.Length; + var ok = filterValue ? themeCount > 0 : themeCount == 0; + + if (!ok) + { + return false; + } + } + + // Apply genre filter + if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + + // Filter by VideoType + if (query.VideoTypes.Length > 0) + { + var video = item as Video; + if (video == null || !query.VideoTypes.Contains(video.VideoType)) + { + return false; + } + } + + if (query.ImageTypes.Length > 0 && !query.ImageTypes.Any(item.HasImage)) + { + return false; + } + + // Apply studio filter + if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id => + { + var studioItem = libraryManager.GetItemById(id); + return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + + // Apply genre filter + if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => + { + var genreItem = libraryManager.GetItemById(id); + return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + + // Apply year filter + if (query.Years.Length > 0) + { + if (!(item.ProductionYear.HasValue && query.Years.Contains(item.ProductionYear.Value))) + { + return false; + } + } + + // Apply official rating filter + if (query.OfficialRatings.Length > 0 && !query.OfficialRatings.Contains(item.OfficialRating ?? string.Empty)) + { + return false; + } + + if (query.ItemIds.Length > 0) + { + if (!query.ItemIds.Contains(item.Id)) + { + return false; + } + } + + // Apply tag filter + var tags = query.Tags; + if (tags.Length > 0) + { + if (!tags.Any(v => item.Tags.Contains(v, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + } + + if (query.MinPlayers.HasValue) + { + var filterValue = query.MinPlayers.Value; + + var game = item as Game; + + if (game != null) + { + var players = game.PlayersSupported ?? 1; + + var ok = players >= filterValue; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (query.MaxPlayers.HasValue) + { + var filterValue = query.MaxPlayers.Value; + + var game = item as Game; + + if (game != null) + { + var players = game.PlayersSupported ?? 1; + + var ok = players <= filterValue; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (query.MinCommunityRating.HasValue) + { + var val = query.MinCommunityRating.Value; + + if (!(item.CommunityRating.HasValue && item.CommunityRating >= val)) + { + return false; + } + } + + if (query.MinCriticRating.HasValue) + { + var val = query.MinCriticRating.Value; + + if (!(item.CriticRating.HasValue && item.CriticRating >= val)) + { + return false; + } + } + + if (query.MinIndexNumber.HasValue) + { + var val = query.MinIndexNumber.Value; + + if (!(item.IndexNumber.HasValue && item.IndexNumber.Value >= val)) + { + return false; + } + } + + if (query.MinPremiereDate.HasValue) + { + var val = query.MinPremiereDate.Value; + + if (!(item.PremiereDate.HasValue && item.PremiereDate.Value >= val)) + { + return false; + } + } + + if (query.MaxPremiereDate.HasValue) + { + var val = query.MaxPremiereDate.Value; + + if (!(item.PremiereDate.HasValue && item.PremiereDate.Value <= val)) + { + return false; + } + } + + if (query.ParentIndexNumber.HasValue) + { + var filterValue = query.ParentIndexNumber.Value; + + if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value != filterValue) + { + return false; + } + } + + if (query.SeriesStatuses.Length > 0) + { + var ok = new[] { item }.OfType<Series>().Any(p => p.Status.HasValue && query.SeriesStatuses.Contains(p.Status.Value)); + if (!ok) + { + return false; + } + } + + if (query.AiredDuringSeason.HasValue) + { + var episode = item as Episode; + + if (episode == null) + { + return false; + } + + if (!Series.FilterEpisodesBySeason(new[] { episode }, query.AiredDuringSeason.Value, true).Any()) + { + return false; + } + } + + return true; + } + + private IEnumerable<BaseItem> GetMediaFolders(User user) + { + if (user == null) + { + return _libraryManager.RootFolder + .Children + .OfType<Folder>() + .Where(UserView.IsEligibleForGrouping); + } + return _libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType<Folder>() + .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i)); + } + + private BaseItem[] GetMediaFolders(User user, IEnumerable<string> viewTypes) + { + if (user == null) + { + return GetMediaFolders(null) + .Where(i => + { + var folder = i as ICollectionFolder; + + return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + }).ToArray(); + } + return GetMediaFolders(user) + .Where(i => + { + var folder = i as ICollectionFolder; + + return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + }).ToArray(); + } + + private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<string> viewTypes) + { + if (parent == null || parent is UserView) + { + return GetMediaFolders(user, viewTypes); + } + + return new BaseItem[] { parent }; + } + + private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent) + { + return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N"), type, sortName); + } + + private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) + { + return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName); + } + + public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, string adjacentToId) + { + var adjacentToIdGuid = new Guid(adjacentToId); + var adjacentToItem = list.FirstOrDefault(i => i.Id == adjacentToIdGuid); + + var index = list.IndexOf(adjacentToItem); + + var previousId = Guid.Empty; + var nextId = Guid.Empty; + + if (index > 0) + { + previousId = list[index - 1].Id; + } + + if (index < list.Count - 1) + { + nextId = list[index + 1].Id; + } + + return list.Where(i => i.Id == previousId || i.Id == nextId || i.Id == adjacentToIdGuid); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs new file mode 100644 index 000000000..65f5b8382 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -0,0 +1,626 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.LiveTv; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Video + /// </summary> + public class Video : BaseItem, + IHasAspectRatio, + ISupportsPlaceHolders, + IHasMediaSources + { + [IgnoreDataMember] + public string PrimaryVersionId { get; set; } + + public string[] AdditionalParts { get; set; } + public string[] LocalAlternateVersions { get; set; } + public LinkedChild[] LinkedAlternateVersions { get; set; } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get { return true; } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsPositionTicksResume + { + get + { + var extraType = ExtraType; + if (extraType.HasValue) + { + if (extraType.Value == Model.Entities.ExtraType.Sample) + { + return false; + } + if (extraType.Value == Model.Entities.ExtraType.ThemeVideo) + { + return false; + } + if (extraType.Value == Model.Entities.ExtraType.Trailer) + { + return false; + } + } + return true; + } + } + + public void SetPrimaryVersionId(string id) + { + if (string.IsNullOrEmpty(id)) + { + PrimaryVersionId = null; + } + else + { + PrimaryVersionId = id; + } + + PresentationUniqueKey = CreatePresentationUniqueKey(); + } + + public override string CreatePresentationUniqueKey() + { + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + return PrimaryVersionId; + } + + return base.CreatePresentationUniqueKey(); + } + + [IgnoreDataMember] + public override bool SupportsThemeMedia + { + get { return true; } + } + + /// <summary> + /// Gets or sets the timestamp. + /// </summary> + /// <value>The timestamp.</value> + public TransportStreamTimestamp? Timestamp { get; set; } + + /// <summary> + /// Gets or sets the subtitle paths. + /// </summary> + /// <value>The subtitle paths.</value> + public string[] SubtitleFiles { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has subtitles. + /// </summary> + /// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value> + public bool HasSubtitles { get; set; } + + public bool IsPlaceHolder { get; set; } + public bool IsShortcut { get; set; } + public string ShortcutPath { get; set; } + + /// <summary> + /// Gets or sets the default index of the video stream. + /// </summary> + /// <value>The default index of the video stream.</value> + public int? DefaultVideoStreamIndex { get; set; } + + /// <summary> + /// Gets or sets the type of the video. + /// </summary> + /// <value>The type of the video.</value> + public VideoType VideoType { get; set; } + + /// <summary> + /// Gets or sets the type of the iso. + /// </summary> + /// <value>The type of the iso.</value> + public IsoType? IsoType { get; set; } + + /// <summary> + /// Gets or sets the video3 D format. + /// </summary> + /// <value>The video3 D format.</value> + public Video3DFormat? Video3DFormat { get; set; } + + public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder) + { + var videoType = VideoType; + + if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay) + { + videoType = VideoType.BluRay; + } + else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd) + { + videoType = VideoType.Dvd; + } + else + { + return new string[] {}; + } + return mediaEncoder.GetPlayableStreamFileNames(Path, videoType); + } + + /// <summary> + /// Gets or sets the aspect ratio. + /// </summary> + /// <value>The aspect ratio.</value> + public string AspectRatio { get; set; } + + public Video() + { + AdditionalParts = new string[] {}; + LocalAlternateVersions = new string[] {}; + SubtitleFiles = new string[] {}; + LinkedAlternateVersions = EmptyLinkedChildArray; + } + + public override bool CanDownload() + { + if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay) + { + return false; + } + + return IsFileProtocol; + } + + [IgnoreDataMember] + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + + [IgnoreDataMember] + public int MediaSourceCount + { + get + { + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + var item = LibraryManager.GetItemById(PrimaryVersionId) as Video; + if (item != null) + { + return item.MediaSourceCount; + } + } + return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1; + } + } + + [IgnoreDataMember] + public bool IsStacked + { + get { return AdditionalParts.Length > 0; } + } + + [IgnoreDataMember] + public override bool HasLocalAlternateVersions + { + get { return LocalAlternateVersions.Length > 0; } + } + + public IEnumerable<Guid> GetAdditionalPartIds() + { + return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + + public IEnumerable<Guid> GetLocalAlternateVersionIds() + { + return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + + public static ILiveTvManager LiveTvManager { get; set; } + + [IgnoreDataMember] + public override SourceType SourceType + { + get + { + if (IsActiveRecording()) + { + return SourceType.LiveTV; + } + + return base.SourceType; + } + } + + protected override bool IsActiveRecording() + { + return LiveTvManager.GetActiveRecordingInfo(Path) != null; + } + + public override bool CanDelete() + { + if (IsActiveRecording()) + { + return false; + } + + return base.CanDelete(); + } + + [IgnoreDataMember] + public bool IsCompleteMedia + { + get + { + if (SourceType == SourceType.Channel) + { + return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + } + + return !IsActiveRecording(); + } + } + + [IgnoreDataMember] + protected virtual bool EnableDefaultVideoUserDataKeys + { + get + { + return true; + } + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (EnableDefaultVideoUserDataKeys) + { + if (ExtraType.HasValue) + { + var key = this.GetProviderId(MetadataProviders.Tmdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, GetUserDataKey(key)); + } + + key = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, GetUserDataKey(key)); + } + } + else + { + var key = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + + key = this.GetProviderId(MetadataProviders.Tmdb); + if (!string.IsNullOrEmpty(key)) + { + list.Insert(0, key); + } + } + } + + return list; + } + + private string GetUserDataKey(string providerId) + { + var key = providerId + "-" + ExtraType.ToString().ToLower(); + + // Make sure different trailers have their own data. + if (RunTimeTicks.HasValue) + { + key += "-" + RunTimeTicks.Value.ToString(CultureInfo.InvariantCulture); + } + + return key; + } + + public IEnumerable<Video> GetLinkedAlternateVersions() + { + return LinkedAlternateVersions + .Select(GetLinkedChild) + .Where(i => i != null) + .OfType<Video>() + .OrderBy(i => i.SortName); + } + + /// <summary> + /// Gets the additional parts. + /// </summary> + /// <returns>IEnumerable{Video}.</returns> + public IEnumerable<Video> GetAdditionalParts() + { + return GetAdditionalPartIds() + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null) + .OfType<Video>() + .OrderBy(i => i.SortName); + } + + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + if (IsStacked) + { + return FileSystem.GetDirectoryName(Path); + } + + if (!IsPlaceHolder) + { + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return Path; + } + } + + return base.ContainingFolderPath; + } + } + + [IgnoreDataMember] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return System.IO.Path.GetFileName(Path); + } + + return System.IO.Path.GetFileNameWithoutExtension(Path); + } + + return null; + } + } + + internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) + { + var updateType = base.UpdateFromResolvedItem(newItem); + + var newVideo = newItem as Video; + if (newVideo != null) + { + if (!AdditionalParts.SequenceEqual(newVideo.AdditionalParts, StringComparer.Ordinal)) + { + AdditionalParts = newVideo.AdditionalParts; + updateType |= ItemUpdateType.MetadataImport; + } + if (!LocalAlternateVersions.SequenceEqual(newVideo.LocalAlternateVersions, StringComparer.Ordinal)) + { + LocalAlternateVersions = newVideo.LocalAlternateVersions; + updateType |= ItemUpdateType.MetadataImport; + } + if (VideoType != newVideo.VideoType) + { + VideoType = newVideo.VideoType; + updateType |= ItemUpdateType.MetadataImport; + } + } + + return updateType; + } + + public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType) + { + if (videoType == VideoType.Dvd) + { + return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true) + .OrderByDescending(i => i.Length) + .ThenBy(i => i.FullName) + .Take(1) + .Select(i => i.FullName) + .ToArray(); + } + if (videoType == VideoType.BluRay) + { + return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true) + .OrderByDescending(i => i.Length) + .ThenBy(i => i.FullName) + .Take(1) + .Select(i => i.FullName) + .ToArray(); + } + return new string[] {}; + } + + /// <summary> + /// Gets a value indicating whether [is3 D]. + /// </summary> + /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool Is3D + { + get { return Video3DFormat.HasValue; } + } + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [IgnoreDataMember] + public override string MediaType + { + get + { + return Model.Entities.MediaType.Video; + } + } + + protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) + { + var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + if (IsStacked) + { + var tasks = AdditionalParts + .Select(i => RefreshMetadataForOwnedVideo(options, true, i, cancellationToken)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + // Must have a parent to have additional parts or alternate versions + // In other words, it must be part of the Parent/Child tree + // The additional parts won't have additional parts themselves + if (IsFileProtocol && SupportsOwnedItems) + { + if (!IsStacked) + { + RefreshLinkedAlternateVersions(); + + var tasks = LocalAlternateVersions + .Select(i => RefreshMetadataForOwnedVideo(options, false, i, cancellationToken)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + + return hasChanges; + } + + private void RefreshLinkedAlternateVersions() + { + foreach (var child in LinkedAlternateVersions) + { + // Reset the cached value + if (child.ItemId.HasValue && child.ItemId.Value.Equals(Guid.Empty)) + { + child.ItemId = null; + } + } + } + + public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + { + base.UpdateToRepository(updateReason, cancellationToken); + + var localAlternates = GetLocalAlternateVersionIds() + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null); + + foreach (var item in localAlternates) + { + item.ImageInfos = ImageInfos; + item.Overview = Overview; + item.ProductionYear = ProductionYear; + item.PremiereDate = PremiereDate; + item.CommunityRating = CommunityRating; + item.OfficialRating = OfficialRating; + item.Genres = Genres; + item.ProviderIds = ProviderIds; + + item.UpdateToRepository(ItemUpdateType.MetadataDownload, cancellationToken); + } + } + + public override IEnumerable<FileSystemMetadata> GetDeletePaths() + { + if (!IsInMixedFolder) + { + return new[] { + new FileSystemMetadata + { + FullName = ContainingFolderPath, + IsDirectory = true + } + }; + } + + return base.GetDeletePaths(); + } + + public virtual MediaStream GetDefaultVideoStream() + { + if (!DefaultVideoStreamIndex.HasValue) + { + return null; + } + + return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + { + ItemId = Id, + Index = DefaultVideoStreamIndex.Value + + }).FirstOrDefault(); + } + + protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources() + { + var list = new List<Tuple<BaseItem, MediaSourceType>>(); + + list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default)); + list.AddRange(GetLinkedAlternateVersions().Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping))); + + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + var primary = LibraryManager.GetItemById(PrimaryVersionId) as Video; + if (primary != null) + { + var existingIds = list.Select(i => i.Item1.Id).ToList(); + list.Add(new Tuple<BaseItem, MediaSourceType>(primary, MediaSourceType.Grouping)); + list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping))); + } + } + + var localAlternates = list + .SelectMany(i => + { + var video = i.Item1 as Video; + return video == null ? new List<Guid>() : video.GetLocalAlternateVersionIds(); + }) + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .ToList(); + + list.AddRange(localAlternates.Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Default))); + + return list; + } + + public static bool IsHD (Video video) { + return video.Height >= 720; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs new file mode 100644 index 000000000..81e030cea --- /dev/null +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Class Year + /// </summary> + public class Year : BaseItem, IItemByName + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + list.Insert(0, "Year-" + Name); + return list; + } + + /// <summary> + /// Returns the folder containing the item. + /// If the item is a folder, it returns the folder itself + /// </summary> + /// <value>The containing folder path.</value> + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + return Path; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + double value = 2; + value /= 3; + + return value; + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + public override bool CanDelete() + { + return false; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + { + int year; + + var usCulture = new CultureInfo("en-US"); + + if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out year)) + { + return new List<BaseItem>(); + } + + query.Years = new[] { year }; + + return LibraryManager.GetItemList(query); + } + + public int? GetYearValue() + { + int i; + + if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out i)) + { + return i; + } + + return null; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + return false; + } + } + + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// <summary> + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// </summary> + public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + { + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } + } +} diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs new file mode 100644 index 000000000..60e7815db --- /dev/null +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.Globalization; + +namespace MediaBrowser.Controller.Extensions +{ + /// <summary> + /// Class BaseExtensions + /// </summary> + public static class StringExtensions + { + public static ILocalizationManager LocalizationManager { get; set; } + + public static string RemoveDiacritics(this string text) + { + return LocalizationManager.RemoveDiacritics(text); + } + } +} diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs new file mode 100644 index 000000000..e1fabee4f --- /dev/null +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -0,0 +1,123 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.IO +{ + /// <summary> + /// Provides low level File access that is much faster than the File/Directory api's + /// </summary> + public static class FileData + { + private static Dictionary<string, FileSystemMetadata> GetFileSystemDictionary(FileSystemMetadata[] list) + { + var dict = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); + + foreach (var file in list) + { + dict[file.FullName] = file; + } + return dict; + } + + /// <summary> + /// Gets the filtered file system entries. + /// </summary> + /// <param name="directoryService">The directory service.</param> + /// <param name="path">The path.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="logger">The logger.</param> + /// <param name="args">The args.</param> + /// <param name="flattenFolderDepth">The flatten folder depth.</param> + /// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param> + /// <returns>Dictionary{System.StringFileSystemInfo}.</returns> + /// <exception cref="System.ArgumentNullException">path</exception> + public static FileSystemMetadata[] GetFilteredFileSystemEntries(IDirectoryService directoryService, + string path, + IFileSystem fileSystem, + IServerApplicationHost appHost, + ILogger logger, + ItemResolveArgs args, + int flattenFolderDepth = 0, + bool resolveShortcuts = true) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + if (args == null) + { + throw new ArgumentNullException("args"); + } + + var entries = directoryService.GetFileSystemEntries(path); + + if (!resolveShortcuts && flattenFolderDepth == 0) + { + return entries; + } + + var dict = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in entries) + { + var isDirectory = entry.IsDirectory; + + var fullName = entry.FullName; + + if (resolveShortcuts && fileSystem.IsShortcut(fullName)) + { + try + { + var newPath = appHost.ExpandVirtualPath(fileSystem.ResolveShortcut(fullName)); + + if (string.IsNullOrEmpty(newPath)) + { + //invalid shortcut - could be old or target could just be unavailable + logger.Warn("Encountered invalid shortcut: " + fullName); + continue; + } + + // Don't check if it exists here because that could return false for network shares. + var data = fileSystem.GetDirectoryInfo(newPath); + + // add to our physical locations + args.AddAdditionalLocation(newPath); + + dict[newPath] = data; + } + catch (Exception ex) + { + logger.ErrorException("Error resolving shortcut from {0}", ex, fullName); + } + } + else if (flattenFolderDepth > 0 && isDirectory) + { + foreach (var child in GetFilteredFileSystemEntries(directoryService, fullName, fileSystem, appHost, logger, args, flattenFolderDepth: flattenFolderDepth - 1, resolveShortcuts: resolveShortcuts)) + { + dict[child.FullName] = child; + } + } + else + { + dict[fullName] = entry; + } + } + + var returnResult = new FileSystemMetadata[dict.Count]; + var index = 0; + var values = dict.Values; + foreach (var value in values) + { + returnResult[index] = value; + index++; + } + return returnResult; + } + + } + +} diff --git a/MediaBrowser.Controller/IResourceFileManager.cs b/MediaBrowser.Controller/IResourceFileManager.cs new file mode 100644 index 000000000..ae73f4b4c --- /dev/null +++ b/MediaBrowser.Controller/IResourceFileManager.cs @@ -0,0 +1,32 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Reflection; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller +{ + public interface IResourceFileManager + { + Task<object> GetStaticFileResult(IRequest request, string basePath, string virtualPath, string contentType, TimeSpan? cacheDuration); + + Stream GetResourceFileStream(string basePath, string virtualPath); + + string ReadAllText(string basePath, string virtualPath); + } +} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs new file mode 100644 index 000000000..830c160e3 --- /dev/null +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -0,0 +1,100 @@ +using MediaBrowser.Common; +using MediaBrowser.Model.System; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; +using System.Threading; + +namespace MediaBrowser.Controller +{ + /// <summary> + /// Interface IServerApplicationHost + /// </summary> + public interface IServerApplicationHost : IApplicationHost + { + event EventHandler HasUpdateAvailableChanged; + + /// <summary> + /// Gets the system info. + /// </summary> + /// <returns>SystemInfo.</returns> + Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken); + + Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken); + + /// <summary> + /// Gets a value indicating whether [supports automatic run at startup]. + /// </summary> + /// <value><c>true</c> if [supports automatic run at startup]; otherwise, <c>false</c>.</value> + bool SupportsAutoRunAtStartup { get; } + + bool CanLaunchWebBrowser { get; } + + /// <summary> + /// Gets the HTTP server port. + /// </summary> + /// <value>The HTTP server port.</value> + int HttpPort { get; } + + /// <summary> + /// Gets the HTTPS port. + /// </summary> + /// <value>The HTTPS port.</value> + int HttpsPort { get; } + + /// <summary> + /// Gets a value indicating whether [supports HTTPS]. + /// </summary> + /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value> + bool EnableHttps { get; } + + /// <summary> + /// Gets a value indicating whether this instance has update available. + /// </summary> + /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> + bool HasUpdateAvailable { get; } + + /// <summary> + /// Gets the name of the friendly. + /// </summary> + /// <value>The name of the friendly.</value> + string FriendlyName { get; } + + /// <summary> + /// Gets the local ip address. + /// </summary> + /// <value>The local ip address.</value> + Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken); + + /// <summary> + /// Gets the local API URL. + /// </summary> + /// <value>The local API URL.</value> + Task<string> GetLocalApiUrl(CancellationToken cancellationToken); + + /// <summary> + /// Gets the local API URL. + /// </summary> + /// <param name="host">The host.</param> + /// <returns>System.String.</returns> + string GetLocalApiUrl(string host); + + /// <summary> + /// Gets the local API URL. + /// </summary> + string GetLocalApiUrl(IpAddressInfo address); + + void LaunchUrl(string url); + + void EnableLoopback(string appName); + + string PackageRuntime { get; } + + WakeOnLanInfo[] GetWakeOnLanInfo(); + + string ExpandVirtualPath(string path); + string ReverseVirtualPath(string path); + } +} diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs new file mode 100644 index 000000000..5fb7968dd --- /dev/null +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -0,0 +1,109 @@ +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Controller +{ + public interface IServerApplicationPaths : IApplicationPaths + { + /// <summary> + /// Gets the path to the base root media directory + /// </summary> + /// <value>The root folder path.</value> + string RootFolderPath { get; } + + /// <summary> + /// Gets the application resources path. This is the path to the folder containing resources that are deployed as part of the application + /// For example, this folder contains dashboard-ui and swagger-ui + /// </summary> + /// <value>The application resources path.</value> + string ApplicationResourcesPath { get; } + + /// <summary> + /// Gets the path to the default user view directory. Used if no specific user view is defined. + /// </summary> + /// <value>The default user views path.</value> + string DefaultUserViewsPath { get; } + + /// <summary> + /// Gets the path to localization data. + /// </summary> + /// <value>The localization path.</value> + string LocalizationPath { get; } + + /// <summary> + /// Gets the path to the People directory + /// </summary> + /// <value>The people path.</value> + string PeoplePath { get; } + + /// <summary> + /// Gets the path to the Genre directory + /// </summary> + /// <value>The genre path.</value> + string GenrePath { get; } + + /// <summary> + /// Gets the music genre path. + /// </summary> + /// <value>The music genre path.</value> + string MusicGenrePath { get; } + + /// <summary> + /// Gets the game genre path. + /// </summary> + /// <value>The game genre path.</value> + string GameGenrePath { get; } + + /// <summary> + /// Gets the path to the Studio directory + /// </summary> + /// <value>The studio path.</value> + string StudioPath { get; } + + /// <summary> + /// Gets the path to the Year directory + /// </summary> + /// <value>The year path.</value> + string YearPath { get; } + + /// <summary> + /// Gets the path to the General IBN directory + /// </summary> + /// <value>The general path.</value> + string GeneralPath { get; } + + /// <summary> + /// Gets the path to the Ratings IBN directory + /// </summary> + /// <value>The ratings path.</value> + string RatingsPath { get; } + + /// <summary> + /// Gets the media info images path. + /// </summary> + /// <value>The media info images path.</value> + string MediaInfoImagesPath { get; } + + /// <summary> + /// Gets the path to the user configuration directory + /// </summary> + /// <value>The user configuration directory path.</value> + string UserConfigurationDirectoryPath { get; } + + /// <summary> + /// Gets the transcoding temporary path. + /// </summary> + /// <value>The transcoding temporary path.</value> + string TranscodingTempPath { get; } + + /// <summary> + /// Gets the internal metadata path. + /// </summary> + /// <value>The internal metadata path.</value> + string InternalMetadataPath { get; } + string VirtualInternalMetadataPath { get; } + + string ArtistsPath { get; } + + string GetTranscodingTempPath(); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs new file mode 100644 index 000000000..822fc3dc3 --- /dev/null +++ b/MediaBrowser.Controller/Library/DeleteOptions.cs @@ -0,0 +1,14 @@ + +namespace MediaBrowser.Controller.Library +{ + public class DeleteOptions + { + public bool DeleteFileLocation { get; set; } + public bool DeleteFromExternalProvider { get; set; } + + public DeleteOptions() + { + DeleteFromExternalProvider = true; + } + } +} diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs new file mode 100644 index 000000000..611aab387 --- /dev/null +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -0,0 +1,32 @@ +using MediaBrowser.Controller.Entities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class BaseIntroProvider + /// </summary> + public interface IIntroProvider + { + /// <summary> + /// Gets the intros. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{System.String}.</returns> + Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user); + + /// <summary> + /// Gets all intro files. + /// </summary> + /// <returns>IEnumerable{System.String}.</returns> + IEnumerable<string> GetAllIntroFiles(); + + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + } +} diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs new file mode 100644 index 000000000..d572716fa --- /dev/null +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -0,0 +1,550 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Interface ILibraryManager + /// </summary> + public interface ILibraryManager + { + /// <summary> + /// Resolves the path. + /// </summary> + /// <param name="fileInfo">The file information.</param> + /// <param name="parent">The parent.</param> + /// <returns>BaseItem.</returns> + BaseItem ResolvePath(FileSystemMetadata fileInfo, + Folder parent = null); + + /// <summary> + /// Resolves a set of files into a list of BaseItem + /// </summary> + IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, + IDirectoryService directoryService, + Folder parent, + LibraryOptions libraryOptions, + string collectionType = null); + + /// <summary> + /// Gets the root folder. + /// </summary> + /// <value>The root folder.</value> + AggregateFolder RootFolder { get; } + + /// <summary> + /// Gets a Person + /// </summary> + /// <param name="name">The name.</param> + /// <returns>Task{Person}.</returns> + Person GetPerson(string name); + + /// <summary> + /// Finds the by path. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BaseItem.</returns> + BaseItem FindByPath(string path, bool? isFolder); + + /// <summary> + /// Gets the artist. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>Task{Artist}.</returns> + MusicArtist GetArtist(string name); + MusicArtist GetArtist(string name, DtoOptions options); + /// <summary> + /// Gets a Studio + /// </summary> + /// <param name="name">The name.</param> + /// <returns>Task{Studio}.</returns> + Studio GetStudio(string name); + + /// <summary> + /// Gets a Genre + /// </summary> + /// <param name="name">The name.</param> + /// <returns>Task{Genre}.</returns> + Genre GetGenre(string name); + + /// <summary> + /// Gets the genre. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>Task{MusicGenre}.</returns> + MusicGenre GetMusicGenre(string name); + + /// <summary> + /// Gets the game genre. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>Task{GameGenre}.</returns> + GameGenre GetGameGenre(string name); + + /// <summary> + /// Gets a Year + /// </summary> + /// <param name="value">The value.</param> + /// <returns>Task{Year}.</returns> + /// <exception cref="System.ArgumentOutOfRangeException"></exception> + Year GetYear(int value); + + /// <summary> + /// Validate and refresh the People sub-set of the IBN. + /// The items are stored in the db but not loaded into memory until actually requested by an operation. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress); + + /// <summary> + /// Reloads the root media folder + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken); + + /// <summary> + /// Queues the library scan. + /// </summary> + void QueueLibraryScan(); + + void UpdateImages(BaseItem item); + + /// <summary> + /// Gets the default view. + /// </summary> + /// <returns>IEnumerable{VirtualFolderInfo}.</returns> + List<VirtualFolderInfo> GetVirtualFolders(); + + List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState); + + /// <summary> + /// Gets the item by id. + /// </summary> + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + BaseItem GetItemById(Guid id); + + /// <summary> + /// Gets the intros. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{System.String}.</returns> + Task<IEnumerable<Video>> GetIntros(BaseItem item, User user); + + /// <summary> + /// Gets all intro files. + /// </summary> + /// <returns>IEnumerable{System.String}.</returns> + IEnumerable<string> GetAllIntroFiles(); + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="rules">The rules.</param> + /// <param name="pluginFolders">The plugin folders.</param> + /// <param name="resolvers">The resolvers.</param> + /// <param name="introProviders">The intro providers.</param> + /// <param name="itemComparers">The item comparers.</param> + /// <param name="postscanTasks">The postscan tasks.</param> + void AddParts(IEnumerable<IResolverIgnoreRule> rules, + IEnumerable<IItemResolver> resolvers, + IEnumerable<IIntroProvider> introProviders, + IEnumerable<IBaseItemComparer> itemComparers, + IEnumerable<ILibraryPostScanTask> postscanTasks); + + /// <summary> + /// Sorts the specified items. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="user">The user.</param> + /// <param name="sortBy">The sort by.</param> + /// <param name="sortOrder">The sort order.</param> + /// <returns>IEnumerable{BaseItem}.</returns> + IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder); + IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy); + + /// <summary> + /// Gets the user root folder. + /// </summary> + /// <returns>UserRootFolder.</returns> + Folder GetUserRootFolder(); + + /// <summary> + /// Creates the item. + /// </summary> + void CreateItem(BaseItem item, BaseItem parent); + + /// <summary> + /// Creates the items. + /// </summary> + void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken); + + /// <summary> + /// Updates the item. + /// </summary> + void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + + /// <summary> + /// Retrieves the item. + /// </summary> + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + BaseItem RetrieveItem(Guid id); + + bool IsScanRunning { get; } + + /// <summary> + /// Occurs when [item added]. + /// </summary> + event EventHandler<ItemChangeEventArgs> ItemAdded; + + /// <summary> + /// Occurs when [item updated]. + /// </summary> + event EventHandler<ItemChangeEventArgs> ItemUpdated; + /// <summary> + /// Occurs when [item removed]. + /// </summary> + event EventHandler<ItemChangeEventArgs> ItemRemoved; + + /// <summary> + /// Finds the type of the collection. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + string GetContentType(BaseItem item); + + /// <summary> + /// Gets the type of the inherited content. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + string GetInheritedContentType(BaseItem item); + + /// <summary> + /// Gets the type of the configured content. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + string GetConfiguredContentType(BaseItem item); + + /// <summary> + /// Gets the type of the configured content. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>System.String.</returns> + string GetConfiguredContentType(string path); + + /// <summary> + /// Normalizes the root path list. + /// </summary> + /// <param name="paths">The paths.</param> + /// <returns>IEnumerable{System.String}.</returns> + List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths); + + /// <summary> + /// Registers the item. + /// </summary> + /// <param name="item">The item.</param> + void RegisterItem(BaseItem item); + + /// <summary> + /// Deletes the item. + /// </summary> + void DeleteItem(BaseItem item, DeleteOptions options); + + /// <summary> + /// Deletes the item. + /// </summary> + void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem); + + /// <summary> + /// Deletes the item. + /// </summary> + void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem); + + /// <summary> + /// Gets the named view. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="name">The name.</param> + /// <param name="parentId">The parent identifier.</param> + /// <param name="viewType">Type of the view.</param> + /// <param name="sortName">Name of the sort.</param> + UserView GetNamedView(User user, + string name, + Guid parentId, + string viewType, + string sortNamen); + + /// <summary> + /// Gets the named view. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="name">The name.</param> + /// <param name="viewType">Type of the view.</param> + /// <param name="sortName">Name of the sort.</param> + UserView GetNamedView(User user, + string name, + string viewType, + string sortName); + + /// <summary> + /// Gets the named view. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="viewType">Type of the view.</param> + /// <param name="sortName">Name of the sort.</param> + UserView GetNamedView(string name, + string viewType, + string sortName); + + /// <summary> + /// Gets the named view. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="parentId">The parent identifier.</param> + /// <param name="viewType">Type of the view.</param> + /// <param name="sortName">Name of the sort.</param> + /// <param name="uniqueId">The unique identifier.</param> + UserView GetNamedView(string name, + Guid parentId, + string viewType, + string sortName, + string uniqueId); + + /// <summary> + /// Gets the shadow view. + /// </summary> + /// <param name="parent">The parent.</param> + /// <param name="viewType">Type of the view.</param> + /// <param name="sortName">Name of the sort.</param> + UserView GetShadowView(BaseItem parent, + string viewType, + string sortName); + + /// <summary> + /// Determines whether [is video file] [the specified path]. + /// </summary> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [is video file] [the specified path]; otherwise, <c>false</c>.</returns> + bool IsVideoFile(string path); + + /// <summary> + /// Determines whether [is audio file] [the specified path]. + /// </summary> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns> + bool IsAudioFile(string path); + + bool IsAudioFile(string path, LibraryOptions libraryOptions); + bool IsVideoFile(string path, LibraryOptions libraryOptions); + + /// <summary> + /// Gets the season number from path. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>System.Nullable<System.Int32>.</returns> + int? GetSeasonNumberFromPath(string path); + + /// <summary> + /// Fills the missing episode numbers from path. + /// </summary> + bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh); + + /// <summary> + /// Parses the name. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>ItemInfo.</returns> + ItemLookupInfo ParseName(string name); + + /// <summary> + /// Gets the new item identifier. + /// </summary> + /// <param name="key">The key.</param> + /// <param name="type">The type.</param> + /// <returns>Guid.</returns> + Guid GetNewItemId(string key, Type type); + + /// <summary> + /// Finds the trailers. + /// </summary> + /// <param name="owner">The owner.</param> + /// <param name="fileSystemChildren">The file system children.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns>IEnumerable<Trailer>.</returns> + IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, + IDirectoryService directoryService); + + /// <summary> + /// Finds the extras. + /// </summary> + /// <param name="owner">The owner.</param> + /// <param name="fileSystemChildren">The file system children.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns>IEnumerable<Video>.</returns> + IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, + IDirectoryService directoryService); + + /// <summary> + /// Gets the collection folders. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable<Folder>.</returns> + List<Folder> GetCollectionFolders(BaseItem item); + + List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren); + + LibraryOptions GetLibraryOptions(BaseItem item); + + /// <summary> + /// Gets the people. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>List<PersonInfo>.</returns> + List<PersonInfo> GetPeople(BaseItem item); + + /// <summary> + /// Gets the people. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<PersonInfo>.</returns> + List<PersonInfo> GetPeople(InternalPeopleQuery query); + + /// <summary> + /// Gets the people items. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<Person>.</returns> + List<Person> GetPeopleItems(InternalPeopleQuery query); + + /// <summary> + /// Updates the people. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="people">The people.</param> + void UpdatePeople(BaseItem item, List<PersonInfo> people); + + /// <summary> + /// Gets the item ids. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<Guid>.</returns> + List<Guid> GetItemIds(InternalItemsQuery query); + + /// <summary> + /// Gets the people names. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<System.String>.</returns> + List<string> GetPeopleNames(InternalPeopleQuery query); + + /// <summary> + /// Queries the items. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>QueryResult<BaseItem>.</returns> + QueryResult<BaseItem> QueryItems(InternalItemsQuery query); + + string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem = null); + + /// <summary> + /// Substitutes the path. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="from">From.</param> + /// <param name="to">To.</param> + /// <returns>System.String.</returns> + string SubstitutePath(string path, string from, string to); + + /// <summary> + /// Converts the image to local. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>Task.</returns> + Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex); + + /// <summary> + /// Gets the items. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>QueryResult<BaseItem>.</returns> + List<BaseItem> GetItemList(InternalItemsQuery query); + + List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent); + + /// <summary> + /// Gets the items. + /// </summary> + List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents); + + /// <summary> + /// Gets the items result. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>QueryResult<BaseItem>.</returns> + QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query); + + /// <summary> + /// Ignores the file. + /// </summary> + /// <param name="file">The file.</param> + /// <param name="parent">The parent.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool IgnoreFile(FileSystemMetadata file, BaseItem parent); + + Guid GetStudioId(string name); + + Guid GetGenreId(string name); + + Guid GetMusicGenreId(string name); + + Guid GetGameGenreId(string name); + + Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary); + Task RemoveVirtualFolder(string name, bool refreshLibrary); + void AddMediaPath(string virtualFolderName, MediaPathInfo path); + void UpdateMediaPath(string virtualFolderName, MediaPathInfo path); + void RemoveMediaPath(string virtualFolderName, string path); + + QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query); + + int GetCount(InternalItemsQuery query); + + void AddExternalSubtitleStreams(List<MediaStream> streams, + string videoPath, + string[] files); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs new file mode 100644 index 000000000..e965e47d6 --- /dev/null +++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs @@ -0,0 +1,43 @@ +using System; + +namespace MediaBrowser.Controller.Library +{ + public interface ILibraryMonitor : IDisposable + { + /// <summary> + /// Starts this instance. + /// </summary> + void Start(); + + /// <summary> + /// Stops this instance. + /// </summary> + void Stop(); + + /// <summary> + /// Reports the file system change beginning. + /// </summary> + /// <param name="path">The path.</param> + void ReportFileSystemChangeBeginning(string path); + + /// <summary> + /// Reports the file system change complete. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="refreshPath">if set to <c>true</c> [refresh path].</param> + void ReportFileSystemChangeComplete(string path, bool refreshPath); + + /// <summary> + /// Reports the file system changed. + /// </summary> + /// <param name="path">The path.</param> + void ReportFileSystemChanged(string path); + + /// <summary> + /// Determines whether [is path locked] [the specified path]. + /// </summary> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [is path locked] [the specified path]; otherwise, <c>false</c>.</returns> + bool IsPathLocked(string path); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs new file mode 100644 index 000000000..694422907 --- /dev/null +++ b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// An interface for tasks that run after the media library scan + /// </summary> + public interface ILibraryPostScanTask + { + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task Run(IProgress<double> progress, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs new file mode 100644 index 000000000..e00da7340 --- /dev/null +++ b/MediaBrowser.Controller/Library/ILiveStream.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Library +{ + public interface ILiveStream + { + Task Open(CancellationToken openCancellationToken); + Task Close(); + int ConsumerCount { get; set; } + string OriginalStreamId { get; set; } + string TunerHostId { get; } + bool EnableStreamSharing { get; } + MediaSourceInfo MediaSource { get; set; } + string UniqueId { get; } + } +} diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs new file mode 100644 index 000000000..8541c4fd9 --- /dev/null +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -0,0 +1,100 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.IO; + +namespace MediaBrowser.Controller.Library +{ + public interface IMediaSourceManager + { + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="providers">The providers.</param> + void AddParts(IEnumerable<IMediaSourceProvider> providers); + + /// <summary> + /// Gets the media streams. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <returns>IEnumerable<MediaStream>.</returns> + List<MediaStream> GetMediaStreams(Guid itemId); + /// <summary> + /// Gets the media streams. + /// </summary> + /// <param name="mediaSourceId">The media source identifier.</param> + /// <returns>IEnumerable<MediaStream>.</returns> + List<MediaStream> GetMediaStreams(string mediaSourceId); + /// <summary> + /// Gets the media streams. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable<MediaStream>.</returns> + List<MediaStream> GetMediaStreams(MediaStreamQuery query); + + /// <summary> + /// Gets the playack media sources. + /// </summary> + Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); + + /// <summary> + /// Gets the static media sources. + /// </summary> + List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null); + + /// <summary> + /// Gets the static media source. + /// </summary> + Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken); + + /// <summary> + /// Opens the media source. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<MediaSourceInfo>.</returns> + Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken); + + Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Gets the live stream. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<MediaSourceInfo>.</returns> + Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken); + + Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken); + + /// <summary> + /// Closes the media source. + /// </summary> + /// <param name="id">The live stream identifier.</param> + /// <returns>Task.</returns> + Task CloseLiveStream(string id); + + Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken); + + bool SupportsDirectStream(string path, MediaProtocol protocol); + + MediaProtocol GetPathProtocol(string path); + + void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user); + + Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken); + + Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken); + } + + public interface IDirectStreamProvider + { + Task CopyToAsync(Stream stream, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs new file mode 100644 index 000000000..eec138532 --- /dev/null +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System; + +namespace MediaBrowser.Controller.Library +{ + public interface IMediaSourceProvider + { + /// <summary> + /// Gets the media sources. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<IEnumerable<MediaSourceInfo>>.</returns> + Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken); + + /// <summary> + /// Opens the media source. + /// </summary> + Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Library/IMetadataFileSaver.cs b/MediaBrowser.Controller/Library/IMetadataFileSaver.cs new file mode 100644 index 000000000..e66fbcbc8 --- /dev/null +++ b/MediaBrowser.Controller/Library/IMetadataFileSaver.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Library +{ + public interface IMetadataFileSaver : IMetadataSaver + { + /// <summary> + /// Gets the save path. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + string GetSavePath(BaseItem item); + } + + public interface IConfigurableProvider + { + bool IsEnabled { get; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs new file mode 100644 index 000000000..f71afa656 --- /dev/null +++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs @@ -0,0 +1,33 @@ +using MediaBrowser.Controller.Entities; +using System.Threading; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Interface IMetadataSaver + /// </summary> + public interface IMetadataSaver + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Determines whether [is enabled for] [the specified item]. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="updateType">Type of the update.</param> + /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns> + bool IsEnabledFor(BaseItem item, ItemUpdateType updateType); + + /// <summary> + /// Saves the specified item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void Save(BaseItem item, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs new file mode 100644 index 000000000..535e6df7e --- /dev/null +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using System.Collections.Generic; +using MediaBrowser.Controller.Dto; + +namespace MediaBrowser.Controller.Library +{ + public interface IMusicManager + { + /// <summary> + /// Gets the instant mix from song. + /// </summary> + List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions); + + /// <summary> + /// Gets the instant mix from artist. + /// </summary> + List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions); + + /// <summary> + /// Gets the instant mix from genre. + /// </summary> + List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions); + } +} diff --git a/MediaBrowser.Controller/Library/ISearchEngine.cs b/MediaBrowser.Controller/Library/ISearchEngine.cs new file mode 100644 index 000000000..715f16407 --- /dev/null +++ b/MediaBrowser.Controller/Library/ISearchEngine.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Search; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Interface ILibrarySearchEngine + /// </summary> + public interface ISearchEngine + { + /// <summary> + /// Gets the search hints. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>Task{IEnumerable{SearchHintInfo}}.</returns> + QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query); + } +} diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs new file mode 100644 index 000000000..11d77f81a --- /dev/null +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; +using System.Threading; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Interface IUserDataManager + /// </summary> + public interface IUserDataManager + { + /// <summary> + /// Occurs when [user data saved]. + /// </summary> + event EventHandler<UserDataSaveEventArgs> UserDataSaved; + + /// <summary> + /// Saves the user data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="item">The item.</param> + /// <param name="userData">The user data.</param> + /// <param name="reason">The reason.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + void SaveUserData(User userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + + UserItemData GetUserData(User user, BaseItem item); + + UserItemData GetUserData(string userId, BaseItem item); + UserItemData GetUserData(Guid userId, BaseItem item); + + /// <summary> + /// Gets the user data dto. + /// </summary> + UserItemDataDto GetUserDataDto(BaseItem item, User user); + + UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options); + + /// <summary> + /// Get all user data for the given user + /// </summary> + /// <param name="userId"></param> + /// <returns></returns> + List<UserItemData> GetAllUserData(Guid userId); + + /// <summary> + /// Save the all provided user data for the given user + /// </summary> + /// <param name="userId"></param> + /// <param name="userData"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken); + + /// <summary> + /// Updates playstate for an item and returns true or false indicating if it was played to completion + /// </summary> + bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks); + } +} diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs new file mode 100644 index 000000000..d29b164ef --- /dev/null +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -0,0 +1,207 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Events; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Users; +using MediaBrowser.Controller.Authentication; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Interface IUserManager + /// </summary> + public interface IUserManager + { + /// <summary> + /// Gets the users. + /// </summary> + /// <value>The users.</value> + IEnumerable<User> Users { get; } + + /// <summary> + /// Occurs when [user updated]. + /// </summary> + event EventHandler<GenericEventArgs<User>> UserUpdated; + + /// <summary> + /// Occurs when [user deleted]. + /// </summary> + event EventHandler<GenericEventArgs<User>> UserDeleted; + + event EventHandler<GenericEventArgs<User>> UserCreated; + event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; + event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; + event EventHandler<GenericEventArgs<User>> UserPasswordChanged; + event EventHandler<GenericEventArgs<User>> UserLockedOut; + + /// <summary> + /// Gets a User by Id + /// </summary> + /// <param name="id">The id.</param> + /// <returns>User.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + User GetUserById(Guid id); + + /// <summary> + /// Gets the user by identifier. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>User.</returns> + User GetUserById(string id); + + /// <summary> + /// Gets the name of the user by. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>User.</returns> + User GetUserByName(string name); + + /// <summary> + /// Refreshes metadata for each user + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task RefreshUsersMetadata(CancellationToken cancellationToken); + + /// <summary> + /// Renames the user. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="newName">The new name.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.ArgumentException"></exception> + Task RenameUser(User user, string newName); + + /// <summary> + /// Updates the user. + /// </summary> + /// <param name="user">The user.</param> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.ArgumentException"></exception> + void UpdateUser(User user); + + /// <summary> + /// Creates the user. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>User.</returns> + /// <exception cref="System.ArgumentNullException">name</exception> + /// <exception cref="System.ArgumentException"></exception> + Task<User> CreateUser(string name); + + /// <summary> + /// Deletes the user. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.ArgumentException"></exception> + Task DeleteUser(User user); + + /// <summary> + /// Resets the password. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + Task ResetPassword(User user); + + /// <summary> + /// Gets the offline user dto. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>UserDto.</returns> + UserDto GetOfflineUserDto(User user); + + /// <summary> + /// Resets the easy password. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + void ResetEasyPassword(User user); + + /// <summary> + /// Changes the password. + /// </summary> + Task ChangePassword(User user, string newPassword); + + /// <summary> + /// Changes the easy password. + /// </summary> + void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1); + + /// <summary> + /// Gets the user dto. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="remoteEndPoint">The remote end point.</param> + /// <returns>UserDto.</returns> + UserDto GetUserDto(User user, string remoteEndPoint = null); + + /// <summary> + /// Authenticates the user. + /// </summary> + Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); + + /// <summary> + /// Starts the forgot password process. + /// </summary> + /// <param name="enteredUsername">The entered username.</param> + /// <param name="isInNetwork">if set to <c>true</c> [is in network].</param> + /// <returns>ForgotPasswordResult.</returns> + Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork); + + /// <summary> + /// Redeems the password reset pin. + /// </summary> + /// <param name="pin">The pin.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + Task<PinRedeemResult> RedeemPasswordResetPin(string pin); + + /// <summary> + /// Gets the user policy. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>UserPolicy.</returns> + UserPolicy GetUserPolicy(User user); + + /// <summary> + /// Gets the user configuration. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>UserConfiguration.</returns> + UserConfiguration GetUserConfiguration(User user); + + /// <summary> + /// Updates the configuration. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <param name="newConfiguration">The new configuration.</param> + /// <returns>Task.</returns> + void UpdateConfiguration(Guid userId, UserConfiguration newConfiguration); + + void UpdateConfiguration(User user, UserConfiguration newConfiguration); + + /// <summary> + /// Updates the user policy. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <param name="userPolicy">The user policy.</param> + void UpdateUserPolicy(Guid userId, UserPolicy userPolicy); + + /// <summary> + /// Makes the valid username. + /// </summary> + /// <param name="username">The username.</param> + /// <returns>System.String.</returns> + string MakeValidUsername(string username); + + void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders); + + NameIdPair[] GetAuthenticationProviders(); + } +} diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs new file mode 100644 index 000000000..f4649777c --- /dev/null +++ b/MediaBrowser.Controller/Library/IUserViewManager.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; + +namespace MediaBrowser.Controller.Library +{ + public interface IUserViewManager + { + Folder[] GetUserViews(UserViewQuery query); + UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName); + + List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options); + } +} diff --git a/MediaBrowser.Controller/Library/IntroInfo.cs b/MediaBrowser.Controller/Library/IntroInfo.cs new file mode 100644 index 000000000..d0e61d0f0 --- /dev/null +++ b/MediaBrowser.Controller/Library/IntroInfo.cs @@ -0,0 +1,19 @@ +using System; + +namespace MediaBrowser.Controller.Library +{ + public class IntroInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + + /// <summary> + /// Gets or sets the item id. + /// </summary> + /// <value>The item id.</value> + public Guid? ItemId { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs new file mode 100644 index 000000000..e671490d3 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class ItemChangeEventArgs + /// </summary> + public class ItemChangeEventArgs + { + /// <summary> + /// Gets or sets the item. + /// </summary> + /// <value>The item.</value> + public BaseItem Item { get; set; } + + public BaseItem Parent { get; set; } + + /// <summary> + /// Gets or sets the item. + /// </summary> + /// <value>The item.</value> + public ItemUpdateType UpdateReason { get; set; } + } +} diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs new file mode 100644 index 000000000..7197425f3 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -0,0 +1,281 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// These are arguments relating to the file system that are collected once and then referred to + /// whenever needed. Primarily for entity resolution. + /// </summary> + public class ItemResolveArgs : EventArgs + { + /// <summary> + /// The _app paths + /// </summary> + private readonly IServerApplicationPaths _appPaths; + + public IDirectoryService DirectoryService { get; private set; } + + /// <summary> + /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="directoryService">The directory service.</param> + public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService) + { + _appPaths = appPaths; + DirectoryService = directoryService; + } + + /// <summary> + /// Gets the file system children. + /// </summary> + /// <value>The file system children.</value> + public FileSystemMetadata[] FileSystemChildren { get; set; } + + public LibraryOptions LibraryOptions { get; set; } + + public LibraryOptions GetLibraryOptions() + { + return LibraryOptions ?? (LibraryOptions = (Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent))); + } + + /// <summary> + /// Gets or sets the parent. + /// </summary> + /// <value>The parent.</value> + public Folder Parent { get; set; } + + /// <summary> + /// Gets or sets the file info. + /// </summary> + /// <value>The file info.</value> + public FileSystemMetadata FileInfo { get; set; } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance is directory. + /// </summary> + /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value> + public bool IsDirectory + { + get + { + return FileInfo.IsDirectory; + } + } + + /// <summary> + /// Gets a value indicating whether this instance is vf. + /// </summary> + /// <value><c>true</c> if this instance is vf; otherwise, <c>false</c>.</value> + public bool IsVf + { + // we should be considered a virtual folder if we are a child of one of the children of the system root folder. + // this is a bit of a trick to determine that... the directory name of a sub-child of the root will start with + // the root but not be equal to it + get + { + if (!IsDirectory) + { + return false; + } + + var parentDir = BaseItem.FileSystem.GetDirectoryName(Path) ?? string.Empty; + + return parentDir.Length > _appPaths.RootFolderPath.Length + && parentDir.StartsWith(_appPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase); + + } + } + + /// <summary> + /// Gets a value indicating whether this instance is physical root. + /// </summary> + /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value> + public bool IsPhysicalRoot + { + get + { + return IsDirectory && BaseItem.FileSystem.AreEqual(Path, _appPaths.RootFolderPath); + } + } + + /// <summary> + /// Gets or sets the additional locations. + /// </summary> + /// <value>The additional locations.</value> + private List<string> AdditionalLocations { get; set; } + + public bool HasParent<T>() + where T : Folder + { + var parent = Parent; + + if (parent != null) + { + var item = parent as T; + + // Just in case the user decided to nest episodes. + // Not officially supported but in some cases we can handle it. + if (item == null) + { + var parents = parent.GetParents(); + foreach (var currentParent in parents) + { + if (currentParent is T) + { + return true; + } + } + } + + return item != null; + + } + return false; + } + + /// <summary> + /// Adds the additional location. + /// </summary> + /// <param name="path">The path.</param> + /// <exception cref="System.ArgumentNullException"></exception> + public void AddAdditionalLocation(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + if (AdditionalLocations == null) + { + AdditionalLocations = new List<string>(); + } + + AdditionalLocations.Add(path); + } + + /// <summary> + /// Gets the physical locations. + /// </summary> + /// <value>The physical locations.</value> + public string[] PhysicalLocations + { + get + { + var paths = string.IsNullOrEmpty(Path) ? new string[] { } : new[] { Path }; + return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray(); + } + } + + /// <summary> + /// Gets the name of the file system entry by. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>FileSystemInfo.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public FileSystemMetadata GetFileSystemEntryByName(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(); + } + + return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name)); + } + + /// <summary> + /// Gets the file system entry by path. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>FileSystemInfo.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public FileSystemMetadata GetFileSystemEntryByPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + foreach (var file in FileSystemChildren) + { + if (string.Equals(file.FullName, path, StringComparison.Ordinal)) + { + return file; + } + } + + return null; + } + + /// <summary> + /// Determines whether [contains file system entry by name] [the specified name]. + /// </summary> + /// <param name="name">The name.</param> + /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns> + public bool ContainsFileSystemEntryByName(string name) + { + return GetFileSystemEntryByName(name) != null; + } + + public string GetCollectionType() + { + return CollectionType; + } + + public string CollectionType { get; set; } + + #region Equality Overrides + + /// <summary> + /// Determines whether the specified <see cref="System.Object" /> is equal to this instance. + /// </summary> + /// <param name="obj">The object to compare with the current object.</param> + /// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns> + public override bool Equals(object obj) + { + return Equals(obj as ItemResolveArgs); + } + + /// <summary> + /// Returns a hash code for this instance. + /// </summary> + /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns> + public override int GetHashCode() + { + return Path.GetHashCode(); + } + + /// <summary> + /// Equalses the specified args. + /// </summary> + /// <param name="args">The args.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + protected bool Equals(ItemResolveArgs args) + { + if (args != null) + { + if (args.Path == null && Path == null) return true; + return args.Path != null && BaseItem.FileSystem.AreEqual(args.Path, Path); + } + return false; + } + + #endregion + } + +} diff --git a/MediaBrowser.Controller/Library/ItemUpdateType.cs b/MediaBrowser.Controller/Library/ItemUpdateType.cs new file mode 100644 index 000000000..cf6263356 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemUpdateType.cs @@ -0,0 +1,14 @@ +using System; + +namespace MediaBrowser.Controller.Library +{ + [Flags] + public enum ItemUpdateType + { + None = 1, + MetadataImport = 2, + ImageUpdate = 4, + MetadataDownload = 8, + MetadataEdit = 16 + } +} diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs new file mode 100644 index 000000000..ec69bea6e --- /dev/null +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -0,0 +1,13 @@ +using System; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Library +{ + public static class LibraryManagerExtensions + { + public static BaseItem GetItemById(this ILibraryManager manager, string id) + { + return manager.GetItemById(new Guid(id)); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs new file mode 100644 index 000000000..dc2fa0f99 --- /dev/null +++ b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Library +{ + public class MetadataConfigurationStore : IConfigurationFactory + { + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new ConfigurationStore[] + { + new ConfigurationStore + { + Key = "metadata", + ConfigurationType = typeof(MetadataConfiguration) + } + }; + } + } + + public static class MetadataConfigurationExtensions + { + public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration<MetadataConfiguration>("metadata"); + } + } +} diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs new file mode 100644 index 000000000..bab334a6d --- /dev/null +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Controller.Library +{ + public static class NameExtensions + { + private static string RemoveDiacritics(string name) + { + if (name == null) + { + return string.Empty; + } + + //return name; + return name.RemoveDiacritics(); + } + + public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) + { + return names.DistinctBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs new file mode 100644 index 000000000..00d9932a7 --- /dev/null +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Holds information about a playback progress event + /// </summary> + public class PlaybackProgressEventArgs : EventArgs + { + public List<User> Users { get; set; } + public long? PlaybackPositionTicks { get; set; } + public BaseItem Item { get; set; } + public BaseItemDto MediaInfo { get; set; } + public string MediaSourceId { get; set; } + public bool IsPaused { get; set; } + public bool IsAutomated { get; set; } + + public string DeviceId { get; set; } + public string DeviceName { get; set; } + public string ClientName { get; set; } + + public string PlaySessionId { get; set; } + public SessionInfo Session { get; set; } + + public PlaybackProgressEventArgs() + { + Users = new List<User>(); + } + } +} diff --git a/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs new file mode 100644 index 000000000..b0f6799fc --- /dev/null +++ b/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Controller.Library +{ + public class PlaybackStopEventArgs : PlaybackProgressEventArgs + { + /// <summary> + /// Gets or sets a value indicating whether [played to completion]. + /// </summary> + /// <value><c>true</c> if [played to completion]; otherwise, <c>false</c>.</value> + public bool PlayedToCompletion { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs new file mode 100644 index 000000000..3957c3020 --- /dev/null +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -0,0 +1,76 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Diagnostics; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class Profiler + /// </summary> + public class Profiler : IDisposable + { + /// <summary> + /// The name + /// </summary> + readonly string _name; + /// <summary> + /// The stopwatch + /// </summary> + readonly Stopwatch _stopwatch; + + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="Profiler" /> class. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="logger">The logger.</param> + public Profiler(string name, ILogger logger) + { + this._name = name; + + _logger = logger; + + _stopwatch = new Stopwatch(); + _stopwatch.Start(); + } + #region IDisposable Members + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + _stopwatch.Stop(); + string message; + if (_stopwatch.ElapsedMilliseconds > 300000) + { + message = string.Format("{0} took {1} minutes.", + _name, ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F")); + } + else + { + message = string.Format("{0} took {1} seconds.", + _name, ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000")); + } + _logger.Info(message); + } + } + + #endregion + } +} diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs new file mode 100644 index 000000000..f832811c2 --- /dev/null +++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs @@ -0,0 +1,22 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class SearchHintInfo + /// </summary> + public class SearchHintInfo + { + /// <summary> + /// Gets or sets the item. + /// </summary> + /// <value>The item.</value> + public BaseItem Item { get; set; } + + /// <summary> + /// Gets or sets the matched term. + /// </summary> + /// <value>The matched term.</value> + public string MatchedTerm { get; set; } + } +} diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs new file mode 100644 index 000000000..dc95ba112 --- /dev/null +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class TVUtils + /// </summary> + public static class TVUtils + { + /// <summary> + /// The TVDB API key + /// </summary> + public static readonly string TvdbApiKey = "B89CE93890E9419B"; + public static readonly string TvdbBaseUrl = "https://www.thetvdb.com/"; + /// <summary> + /// The banner URL + /// </summary> + public static readonly string BannerUrl = TvdbBaseUrl + "banners/"; + + /// <summary> + /// Gets the air days. + /// </summary> + /// <param name="day">The day.</param> + /// <returns>List{DayOfWeek}.</returns> + public static DayOfWeek[] GetAirDays(string day) + { + if (!string.IsNullOrEmpty(day)) + { + if (string.Equals(day, "Daily", StringComparison.OrdinalIgnoreCase)) + { + return new DayOfWeek[] + { + DayOfWeek.Sunday, + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday + }; + } + + DayOfWeek value; + + if (Enum.TryParse(day, true, out value)) + { + return new DayOfWeek[] + { + value + }; + } + + return new DayOfWeek[]{}; + } + return null; + } + } +} diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs new file mode 100644 index 000000000..d921a7077 --- /dev/null +++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs @@ -0,0 +1,39 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class UserDataSaveEventArgs + /// </summary> + public class UserDataSaveEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + public Guid UserId { get; set; } + + public List<string> Keys { get; set; } + + /// <summary> + /// Gets or sets the save reason. + /// </summary> + /// <value>The save reason.</value> + public UserDataSaveReason SaveReason { get; set; } + + /// <summary> + /// Gets or sets the user data. + /// </summary> + /// <value>The user data.</value> + public UserItemData UserData { get; set; } + + /// <summary> + /// Gets or sets the item. + /// </summary> + /// <value>The item.</value> + public BaseItem Item { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs new file mode 100644 index 000000000..c000da852 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -0,0 +1,74 @@ +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.LiveTv +{ + /// <summary> + /// Class ChannelInfo + /// </summary> + public class ChannelInfo + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the number. + /// </summary> + /// <value>The number.</value> + public string Number { get; set; } + + /// <summary> + /// Get or sets the Id. + /// </summary> + /// <value>The id of the channel.</value> + public string Id { get; set; } + + public string Path { get; set; } + + public string TunerChannelId { get; set; } + + public string CallSign { get; set; } + + /// <summary> + /// Gets or sets the tuner host identifier. + /// </summary> + /// <value>The tuner host identifier.</value> + public string TunerHostId { get; set; } + + /// <summary> + /// Gets or sets the type of the channel. + /// </summary> + /// <value>The type of the channel.</value> + public ChannelType ChannelType { get; set; } + + /// <summary> + /// Supply the image path if it can be accessed directly from the file system + /// </summary> + /// <value>The image path.</value> + public string ImagePath { get; set; } + + /// <summary> + /// Supply the image url if it can be downloaded + /// </summary> + /// <value>The image URL.</value> + public string ImageUrl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has image. + /// </summary> + /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> + public bool? HasImage { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance is favorite. + /// </summary> + /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value> + public bool? IsFavorite { get; set; } + + public bool? IsHD { get; set; } + public string AudioCodec { get; set; } + public string VideoCodec { get; set; } + public string[] Tags { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs new file mode 100644 index 000000000..faf4a34df --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.LiveTv +{ + public interface IListingsProvider + { + string Name { get; } + string Type { get; } + Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); + Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings); + Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location); + Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs new file mode 100644 index 000000000..a7f675034 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -0,0 +1,293 @@ +using System; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Querying; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Events; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.LiveTv +{ + /// <summary> + /// Manages all live tv services installed on the server + /// </summary> + public interface ILiveTvManager + { + /// <summary> + /// Gets the services. + /// </summary> + /// <value>The services.</value> + IReadOnlyList<ILiveTvService> Services { get; } + + /// <summary> + /// Gets the new timer defaults asynchronous. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{TimerInfo}.</returns> + Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken); + + /// <summary> + /// Gets the new timer defaults. + /// </summary> + /// <param name="programId">The program identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{SeriesTimerInfoDto}.</returns> + Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken); + + /// <summary> + /// Cancels the timer. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>Task.</returns> + Task CancelTimer(string id); + + /// <summary> + /// Cancels the series timer. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>Task.</returns> + Task CancelSeriesTimer(string id); + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="services">The services.</param> + /// <param name="tunerHosts">The tuner hosts.</param> + /// <param name="listingProviders">The listing providers.</param> + void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders); + + /// <summary> + /// Gets the timer. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{TimerInfoDto}.</returns> + Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken); + + /// <summary> + /// Gets the series timer. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{TimerInfoDto}.</returns> + Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken); + + /// <summary> + /// Gets the recordings. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="options">The options.</param> + QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options); + + /// <summary> + /// Gets the timers. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{QueryResult{TimerInfoDto}}.</returns> + Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the series timers. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{QueryResult{SeriesTimerInfoDto}}.</returns> + Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel stream. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="mediaSourceId">The media source identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{StreamResponseInfo}.</returns> + Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + + /// <summary> + /// Gets the program. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="user">The user.</param> + /// <returns>Task{ProgramInfoDto}.</returns> + Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null); + + /// <summary> + /// Gets the programs. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>IEnumerable{ProgramInfo}.</returns> + Task<QueryResult<BaseItemDto>> GetPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken); + + /// <summary> + /// Updates the timer. + /// </summary> + /// <param name="timer">The timer.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken); + + /// <summary> + /// Updates the timer. + /// </summary> + /// <param name="timer">The timer.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken); + + /// <summary> + /// Creates the timer. + /// </summary> + /// <param name="timer">The timer.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CreateTimer(TimerInfoDto timer, CancellationToken cancellationToken); + + /// <summary> + /// Creates the series timer. + /// </summary> + /// <param name="timer">The timer.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken); + + /// <summary> + /// Gets the guide information. + /// </summary> + /// <returns>GuideInfo.</returns> + GuideInfo GetGuideInfo(); + + /// <summary> + /// Gets the recommended programs. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken); + + /// <summary> + /// Gets the recommended programs internal. + /// </summary> + QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken); + + /// <summary> + /// Gets the live tv information. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{LiveTvInfo}.</returns> + LiveTvInfo GetLiveTvInfo(CancellationToken cancellationToken); + + /// <summary> + /// Resets the tuner. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ResetTuner(string id, CancellationToken cancellationToken); + + /// <summary> + /// Gets the live tv folder. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + Folder GetInternalLiveTvFolder(CancellationToken cancellationToken); + + /// <summary> + /// Gets the enabled users. + /// </summary> + /// <returns>IEnumerable{User}.</returns> + IEnumerable<User> GetEnabledUsers(); + + /// <summary> + /// Gets the internal channels. + /// </summary> + QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel media sources. + /// </summary> + Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken); + + /// <summary> + /// Adds the information to program dto. + /// </summary> + /// <param name="programs">The programs.</param> + /// <param name="fields">The fields.</param> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> programs, ItemFields[] fields, User user = null); + + /// <summary> + /// Saves the tuner host. + /// </summary> + Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); + /// <summary> + /// Saves the listing provider. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="validateLogin">if set to <c>true</c> [validate login].</param> + /// <param name="validateListings">if set to <c>true</c> [validate listings].</param> + /// <returns>Task.</returns> + Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings); + + void DeleteListingsProvider(string id); + + Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber); + + TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, NameValuePair[] mappings, List<ChannelInfo> providerChannels); + + /// <summary> + /// Gets the lineups. + /// </summary> + /// <param name="providerType">Type of the provider.</param> + /// <param name="providerId">The provider identifier.</param> + /// <param name="country">The country.</param> + /// <param name="location">The location.</param> + /// <returns>Task<List<NameIdPair>>.</returns> + Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location); + + /// <summary> + /// Adds the channel information. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="options">The options.</param> + /// <param name="user">The user.</param> + void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> items, DtoOptions options, User user); + + Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); + Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); + + IListingsProvider[] ListingProviders { get; } + + List<NameIdPair> GetTunerHostTypes(); + Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); + + event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; + event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; + event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; + event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; + + string GetEmbyTvActiveRecordingPath(string id); + + ActiveRecordingInfo GetActiveRecordingInfo(string path); + + void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null); + + List<BaseItem> GetRecordingFolders(User user); + } + + public class ActiveRecordingInfo + { + public string Id { get; set; } + public string Path { get; set; } + public TimerInfo Timer { get; set; } + public CancellationTokenSource CancellationTokenSource { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs new file mode 100644 index 000000000..601fb69aa --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -0,0 +1,190 @@ +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Dto; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.LiveTv +{ + /// <summary> + /// Represents a single live tv back end (next pvr, media portal, etc). + /// </summary> + public interface ILiveTvService + { + /// <summary> + /// Occurs when [data source changed]. + /// </summary> + event EventHandler DataSourceChanged; + + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the home page URL. + /// </summary> + /// <value>The home page URL.</value> + string HomePageUrl { get; } + + /// <summary> + /// Gets the channels async. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{ChannelInfo}}.</returns> + Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken); + + /// <summary> + /// Cancels the timer asynchronous. + /// </summary> + /// <param name="timerId">The timer identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CancelTimerAsync(string timerId, CancellationToken cancellationToken); + + /// <summary> + /// Cancels the series timer asynchronous. + /// </summary> + /// <param name="timerId">The timer identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken); + + /// <summary> + /// Creates the timer asynchronous. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken); + + /// <summary> + /// Creates the series timer asynchronous. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken); + + /// <summary> + /// Updates the timer asynchronous. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken); + + /// <summary> + /// Updates the series timer asynchronous. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken); + + /// <summary> + /// Gets the recordings asynchronous. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{RecordingInfo}}.</returns> + Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken); + + /// <summary> + /// Gets the new timer defaults asynchronous. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="program">The program.</param> + /// <returns>Task{SeriesTimerInfo}.</returns> + Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null); + + /// <summary> + /// Gets the series timers asynchronous. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{SeriesTimerInfo}}.</returns> + Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken); + + /// <summary> + /// Gets the programs asynchronous. + /// </summary> + /// <param name="channelId">The channel identifier.</param> + /// <param name="startDateUtc">The start date UTC.</param> + /// <param name="endDateUtc">The end date UTC.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{ProgramInfo}}.</returns> + Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel stream. + /// </summary> + /// <param name="channelId">The channel identifier.</param> + /// <param name="streamId">The stream identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel stream media sources. + /// </summary> + /// <param name="channelId">The channel identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<List<MediaSourceInfo>>.</returns> + Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken); + + /// <summary> + /// Closes the live stream. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task CloseLiveStream(string id, CancellationToken cancellationToken); + + /// <summary> + /// Records the live stream. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task RecordLiveStream(string id, CancellationToken cancellationToken); + + /// <summary> + /// Resets the tuner. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ResetTuner(string id, CancellationToken cancellationToken); + } + + public interface ISupportsNewTimerIds + { + /// <summary> + /// Creates the timer asynchronous. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken); + + /// <summary> + /// Creates the series timer asynchronous. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken); + } + + public interface ISupportsDirectStreamProvider + { + Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + } + + public interface ISupportsUpdatingDefaults + { + Task UpdateTimerDefaults(SeriesTimerInfo info, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs new file mode 100644 index 000000000..d5a0e2115 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -0,0 +1,62 @@ +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.LiveTv +{ + public interface ITunerHost + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + /// <summary> + /// Gets the type. + /// </summary> + /// <value>The type.</value> + string Type { get; } + /// <summary> + /// Gets the channels. + /// </summary> + /// <returns>Task<IEnumerable<ChannelInfo>>.</returns> + Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken); + /// <summary> + /// Gets the tuner infos. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<List<LiveTvTunerInfo>>.</returns> + Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken); + /// <summary> + /// Gets the channel stream. + /// </summary> + /// <param name="channelId">The channel identifier.</param> + /// <param name="streamId">The stream identifier.</param> + Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + /// <summary> + /// Gets the channel stream media sources. + /// </summary> + /// <param name="channelId">The channel identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<List<MediaSourceInfo>>.</returns> + Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken); + + Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken); + bool IsSupported + { + get; + } + } + public interface IConfigurableTunerHost + { + /// <summary> + /// Validates the specified information. + /// </summary> + /// <param name="info">The information.</param> + /// <returns>Task.</returns> + Task Validate(TunerHostInfo info); + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs new file mode 100644 index 000000000..9e2d29eb6 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -0,0 +1,197 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; +using System.Globalization; +using MediaBrowser.Model.Serialization; +using System; +using System.Linq; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvChannel : BaseItem, IHasMediaSources, IHasProgramAttributes + { + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName) + { + list.Insert(0, GetClientTypeName() + "-" + Name); + } + + return list; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.LiveTvChannel; + } + + [IgnoreDataMember] + public override bool SupportsPositionTicksResume + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override SourceType SourceType + { + get { return SourceType.LiveTV; } + } + + [IgnoreDataMember] + public override bool EnableRememberingTrackSelections + { + get + { + return false; + } + } + + /// <summary> + /// Gets or sets the number. + /// </summary> + /// <value>The number.</value> + public string Number { get; set; } + + /// <summary> + /// Gets or sets the type of the channel. + /// </summary> + /// <value>The type of the channel.</value> + public ChannelType ChannelType { get; set; } + + [IgnoreDataMember] + public override LocationType LocationType + { + get + { + // TODO: This should be removed + return LocationType.Remote; + } + } + + protected override string CreateSortName() + { + if (!string.IsNullOrEmpty(Number)) + { + double number = 0; + + if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + { + return string.Format("{0:00000.0}", number) + "-" + (Name ?? string.Empty); + } + } + + return (Number ?? string.Empty) + "-" + (Name ?? string.Empty); + } + + [IgnoreDataMember] + public override string MediaType + { + get + { + return ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + } + } + + public override string GetClientTypeName() + { + return "TvChannel"; + } + + public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems) + { + return new List<BaseItem>(); + } + + public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + { + var list = new List<MediaSourceInfo>(); + + var info = new MediaSourceInfo + { + Id = Id.ToString("N"), + Protocol = PathProtocol ?? MediaProtocol.File, + MediaStreams = new List<MediaStream>(), + Name = Name, + Path = Path, + RunTimeTicks = RunTimeTicks, + Type = MediaSourceType.Placeholder, + IsInfiniteStream = RunTimeTicks == null + }; + + list.Add(info); + + return list; + } + + public override List<MediaStream> GetMediaStreams() + { + return new List<MediaStream>(); + } + + protected override string GetInternalMetadataPath(string basePath) + { + return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N"), "metadata"); + } + + public override bool CanDelete() + { + return false; + } + + [IgnoreDataMember] + public bool IsMovie { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is sports. + /// </summary> + /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsSports { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is series. + /// </summary> + /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is news. + /// </summary> + /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsNews { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is kids. + /// </summary> + /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsKids + { + get + { + return Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + } + } + + [IgnoreDataMember] + public bool IsRepeat { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + /// <value>The episode title.</value> + [IgnoreDataMember] + public string EpisodeTitle { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs new file mode 100644 index 000000000..a7735ad80 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs @@ -0,0 +1,20 @@ +using System; + +namespace MediaBrowser.Controller.LiveTv +{ + /// <summary> + /// Class LiveTvConflictException. + /// </summary> + public class LiveTvConflictException : Exception + { + public LiveTvConflictException() + { + + } + public LiveTvConflictException(string message) + : base(message) + { + + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs new file mode 100644 index 000000000..fa3aab4f2 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -0,0 +1,349 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes + { + public LiveTvProgram() + { + IsVirtualItem = true; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + if (!IsSeries) + { + 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); + } + } + else if (!string.IsNullOrEmpty(EpisodeTitle)) + { + var name = GetClientTypeName(); + + list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty)); + } + + return list; + } + + public static double GetDefaultPrimaryImageAspectRatio(IHasProgramAttributes item) + { + var serviceName = item.ServiceName; + + if (item.IsMovie) + { + if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase)) + { + double value = 2; + value /= 3; + + return value; + } + else + { + double value = 16; + value /= 9; + + return value; + } + } + else + { + if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase)) + { + double value = 2; + value /= 3; + + return value; + } + else + { + double value = 16; + value /= 9; + + return value; + } + } + } + + private static string EmbyServiceName = "Emby"; + public override double GetDefaultPrimaryImageAspectRatio() + { + return GetDefaultPrimaryImageAspectRatio(this); + } + + [IgnoreDataMember] + public override SourceType SourceType + { + get { return SourceType.LiveTV; } + } + + /// <summary> + /// The start date of the program, in UTC. + /// </summary> + [IgnoreDataMember] + public DateTime StartDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is repeat. + /// </summary> + /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsRepeat { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + /// <value>The episode title.</value> + [IgnoreDataMember] + public string EpisodeTitle { get; set; } + + [IgnoreDataMember] + public string ShowId { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is movie. + /// </summary> + /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsMovie { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is sports. + /// </summary> + /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsSports + { + get + { + return Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase); + } + } + + /// <summary> + /// Gets or sets a value indicating whether this instance is series. + /// </summary> + /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is live. + /// </summary> + /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsLive + { + get + { + return Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); + } + } + + /// <summary> + /// Gets or sets a value indicating whether this instance is news. + /// </summary> + /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsNews + { + get + { + return Tags.Contains("News", StringComparer.OrdinalIgnoreCase); + } + } + + /// <summary> + /// Gets or sets a value indicating whether this instance is kids. + /// </summary> + /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsKids + { + get + { + return Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + } + } + + /// <summary> + /// Gets or sets a value indicating whether this instance is premiere. + /// </summary> + /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsPremiere + { + get + { + return Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); + } + } + + /// <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 string MediaType + //{ + // get + // { + // return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio; + // } + //} + + [IgnoreDataMember] + public bool IsAiring + { + get + { + var now = DateTime.UtcNow; + + return now >= StartDate && now < EndDate; + } + } + + [IgnoreDataMember] + public bool HasAired + { + get + { + var now = DateTime.UtcNow; + + return now >= EndDate; + } + } + + public override string GetClientTypeName() + { + return "Program"; + } + + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.LiveTvProgram; + } + + protected override string GetInternalMetadataPath(string basePath) + { + return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N")); + } + + public override bool CanDelete() + { + return false; + } + + [IgnoreDataMember] + public override bool SupportsPeople + { + get + { + // Optimization + if (IsNews || IsSports) + { + return false; + } + + return base.SupportsPeople; + } + } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } + + private LiveTvOptions GetConfiguration() + { + return ConfigurationManager.GetConfiguration<LiveTvOptions>("livetv"); + } + + private ListingsProviderInfo GetListingsProviderInfo() + { + if (string.Equals(ServiceName, "Emby", StringComparison.OrdinalIgnoreCase)) + { + var config = GetConfiguration(); + + return config.ListingProviders.FirstOrDefault(i => !string.IsNullOrEmpty(i.MoviePrefix)); + } + + return null; + } + + protected override string GetNameForMetadataLookup() + { + var name = base.GetNameForMetadataLookup(); + + var listings = GetListingsProviderInfo(); + + if (listings != null) + { + if (!string.IsNullOrEmpty(listings.MoviePrefix) && name.StartsWith(listings.MoviePrefix, StringComparison.OrdinalIgnoreCase)) + { + name = name.Substring(listings.MoviePrefix.Length).Trim(); + } + } + + return name; + } + + public override List<ExternalUrl> GetRelatedUrls() + { + var list = base.GetRelatedUrls(); + + var imdbId = this.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdbId)) + { + if (IsMovie) + { + list.Add(new ExternalUrl + { + Name = "Trakt", + Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + }); + } + } + + return list; + } + + public string SeriesName { get; set;} + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs new file mode 100644 index 000000000..4da238acf --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs @@ -0,0 +1,49 @@ +using MediaBrowser.Model.LiveTv; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvServiceStatusInfo + { + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public LiveTvServiceStatus Status { get; set; } + + /// <summary> + /// Gets or sets the status message. + /// </summary> + /// <value>The status message.</value> + public string StatusMessage { get; set; } + + /// <summary> + /// Gets or sets the version. + /// </summary> + /// <value>The version.</value> + public string Version { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has update available. + /// </summary> + /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> + public bool HasUpdateAvailable { get; set; } + + /// <summary> + /// Gets or sets the tuners. + /// </summary> + /// <value>The tuners.</value> + public List<LiveTvTunerInfo> Tuners { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance is visible. + /// </summary> + /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value> + public bool IsVisible { get; set; } + + public LiveTvServiceStatusInfo() + { + Tuners = new List<LiveTvTunerInfo>(); + IsVisible = true; + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs new file mode 100644 index 000000000..5c001f288 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs @@ -0,0 +1,73 @@ +using MediaBrowser.Model.LiveTv; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvTunerInfo + { + /// <summary> + /// Gets or sets the type of the source. + /// </summary> + /// <value>The type of the source.</value> + public string SourceType { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the URL. + /// </summary> + /// <value>The URL.</value> + public string Url { get; set; } + + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public LiveTvTunerStatus Status { get; set; } + + /// <summary> + /// Gets or sets the channel identifier. + /// </summary> + /// <value>The channel identifier.</value> + public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the recording identifier. + /// </summary> + /// <value>The recording identifier.</value> + public string RecordingId { get; set; } + + /// <summary> + /// Gets or sets the name of the program. + /// </summary> + /// <value>The name of the program.</value> + public string ProgramName { get; set; } + + /// <summary> + /// Gets or sets the clients. + /// </summary> + /// <value>The clients.</value> + public List<string> Clients { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance can reset. + /// </summary> + /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value> + public bool CanReset { get; set; } + + public LiveTvTunerInfo() + { + Clients = new List<string>(); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs new file mode 100644 index 000000000..9e3cbdded --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -0,0 +1,213 @@ +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.LiveTv +{ + public class ProgramInfo + { + /// <summary> + /// Id of the program. + /// </summary> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the channel identifier. + /// </summary> + /// <value>The channel identifier.</value> + public string ChannelId { get; set; } + + /// <summary> + /// Name of the program + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the official rating. + /// </summary> + /// <value>The official rating.</value> + public string OfficialRating { get; set; } + + /// <summary> + /// Gets or sets the overview. + /// </summary> + /// <value>The overview.</value> + public string Overview { get; set; } + /// <summary> + /// Gets or sets the short overview. + /// </summary> + /// <value>The short overview.</value> + public string ShortOverview { get; set; } + + /// <summary> + /// The start date of the program, in UTC. + /// </summary> + public DateTime StartDate { get; set; } + + /// <summary> + /// The end date of the program, in UTC. + /// </summary> + public DateTime EndDate { get; set; } + + /// <summary> + /// Genre of the program. + /// </summary> + public List<string> Genres { get; set; } + + /// <summary> + /// Gets or sets the original air date. + /// </summary> + /// <value>The original air date.</value> + public DateTime? OriginalAirDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is hd. + /// </summary> + /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> + public bool? IsHD { get; set; } + + public bool? Is3D { get; set; } + + /// <summary> + /// Gets or sets the audio. + /// </summary> + /// <value>The audio.</value> + public ProgramAudio? Audio { get; set; } + + /// <summary> + /// Gets or sets the community rating. + /// </summary> + /// <value>The community rating.</value> + public float? CommunityRating { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is repeat. + /// </summary> + /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> + public bool IsRepeat { get; set; } + + public bool IsSubjectToBlackout { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + /// <value>The episode title.</value> + public string EpisodeTitle { get; set; } + + /// <summary> + /// Supply the image path if it can be accessed directly from the file system + /// </summary> + /// <value>The image path.</value> + public string ImagePath { get; set; } + + /// <summary> + /// Supply the image url if it can be downloaded + /// </summary> + /// <value>The image URL.</value> + public string ImageUrl { get; set; } + + public string ThumbImageUrl { get; set; } + + public string LogoImageUrl { get; set; } + + public string BackdropImageUrl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has image. + /// </summary> + /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> + public bool? HasImage { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is movie. + /// </summary> + /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> + public bool IsMovie { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is sports. + /// </summary> + /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> + public bool IsSports { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is series. + /// </summary> + /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is live. + /// </summary> + /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> + public bool IsLive { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is news. + /// </summary> + /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> + public bool IsNews { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is kids. + /// </summary> + /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> + public bool IsKids { get; set; } + + public bool IsEducational { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is premiere. + /// </summary> + /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> + public bool IsPremiere { get; set; } + + /// <summary> + /// Gets or sets the production year. + /// </summary> + /// <value>The production year.</value> + public int? ProductionYear { get; set; } + /// <summary> + /// Gets or sets the home page URL. + /// </summary> + /// <value>The home page URL.</value> + public string HomePageUrl { get; set; } + /// <summary> + /// Gets or sets the series identifier. + /// </summary> + /// <value>The series identifier.</value> + public string SeriesId { get; set; } + /// <summary> + /// Gets or sets the show identifier. + /// </summary> + /// <value>The show identifier.</value> + public string ShowId { get; set; } + /// <summary> + /// Gets or sets the season number. + /// </summary> + /// <value>The season number.</value> + public int? SeasonNumber { get; set; } + /// <summary> + /// Gets or sets the episode number. + /// </summary> + /// <value>The episode number.</value> + public int? EpisodeNumber { get; set; } + /// <summary> + /// Gets or sets the etag. + /// </summary> + /// <value>The etag.</value> + public string Etag { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + public Dictionary<string, string> SeriesProviderIds { get; set; } + + public ProgramInfo() + { + Genres = new List<string>(); + + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs new file mode 100644 index 000000000..3006b9bbe --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -0,0 +1,205 @@ +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.LiveTv +{ + public class RecordingInfo + { + /// <summary> + /// Id of the recording. + /// </summary> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the series timer identifier. + /// </summary> + /// <value>The series timer identifier.</value> + public string SeriesTimerId { get; set; } + + /// <summary> + /// Gets or sets the timer identifier. + /// </summary> + /// <value>The timer identifier.</value> + public string TimerId { get; set; } + + /// <summary> + /// ChannelId of the recording. + /// </summary> + public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the type of the channel. + /// </summary> + /// <value>The type of the channel.</value> + public ChannelType ChannelType { get; set; } + + /// <summary> + /// Name of the recording. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + + /// <summary> + /// Gets or sets the URL. + /// </summary> + /// <value>The URL.</value> + public string Url { get; set; } + + /// <summary> + /// Gets or sets the overview. + /// </summary> + /// <value>The overview.</value> + public string Overview { get; set; } + + /// <summary> + /// The start date of the recording, in UTC. + /// </summary> + public DateTime StartDate { get; set; } + + /// <summary> + /// The end date of the recording, in UTC. + /// </summary> + public DateTime EndDate { get; set; } + + /// <summary> + /// Gets or sets the program identifier. + /// </summary> + /// <value>The program identifier.</value> + public string ProgramId { get; set; } + + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public RecordingStatus Status { get; set; } + + /// <summary> + /// Genre of the program. + /// </summary> + public List<string> Genres { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is repeat. + /// </summary> + /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> + public bool IsRepeat { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + /// <value>The episode title.</value> + public string EpisodeTitle { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is hd. + /// </summary> + /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> + public bool? IsHD { get; set; } + + /// <summary> + /// Gets or sets the audio. + /// </summary> + /// <value>The audio.</value> + public ProgramAudio? Audio { get; set; } + + /// <summary> + /// Gets or sets the original air date. + /// </summary> + /// <value>The original air date.</value> + public DateTime? OriginalAirDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is movie. + /// </summary> + /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> + public bool IsMovie { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is sports. + /// </summary> + /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> + public bool IsSports { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is series. + /// </summary> + /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is live. + /// </summary> + /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> + public bool IsLive { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is news. + /// </summary> + /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> + public bool IsNews { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is kids. + /// </summary> + /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> + public bool IsKids { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is premiere. + /// </summary> + /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> + public bool IsPremiere { get; set; } + + /// <summary> + /// Gets or sets the official rating. + /// </summary> + /// <value>The official rating.</value> + public string OfficialRating { get; set; } + + /// <summary> + /// Gets or sets the community rating. + /// </summary> + /// <value>The community rating.</value> + public float? CommunityRating { get; set; } + + /// <summary> + /// Supply the image path if it can be accessed directly from the file system + /// </summary> + /// <value>The image path.</value> + public string ImagePath { get; set; } + + /// <summary> + /// Supply the image url if it can be downloaded + /// </summary> + /// <value>The image URL.</value> + public string ImageUrl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has image. + /// </summary> + /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> + public bool? HasImage { get; set; } + /// <summary> + /// Gets or sets the show identifier. + /// </summary> + /// <value>The show identifier.</value> + public string ShowId { get; set; } + + /// <summary> + /// Gets or sets the date last updated. + /// </summary> + /// <value>The date last updated.</value> + public DateTime DateLastUpdated { get; set; } + + public RecordingInfo() + { + Genres = new List<string>(); + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs new file mode 100644 index 000000000..90ea329fe --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs @@ -0,0 +1,12 @@ +using MediaBrowser.Model.LiveTv; +using System; + +namespace MediaBrowser.Controller.LiveTv +{ + public class RecordingStatusChangedEventArgs : EventArgs + { + public string RecordingId { get; set; } + + public RecordingStatus NewStatus { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs new file mode 100644 index 000000000..5c73ed833 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.LiveTv +{ + public class SeriesTimerInfo + { + /// <summary> + /// Id of the recording. + /// </summary> + public string Id { get; set; } + + /// <summary> + /// ChannelId of the recording. + /// </summary> + public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the program identifier. + /// </summary> + /// <value>The program identifier.</value> + public string ProgramId { get; set; } + + /// <summary> + /// Name of the recording. + /// </summary> + public string Name { get; set; } + + public string ServiceName { get; set; } + + /// <summary> + /// Description of the recording. + /// </summary> + public string Overview { get; set; } + + /// <summary> + /// The start date of the recording, in UTC. + /// </summary> + public DateTime StartDate { get; set; } + + /// <summary> + /// The end date of the recording, in UTC. + /// </summary> + public DateTime EndDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether [record any time]. + /// </summary> + /// <value><c>true</c> if [record any time]; otherwise, <c>false</c>.</value> + public bool RecordAnyTime { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether [record any channel]. + /// </summary> + /// <value><c>true</c> if [record any channel]; otherwise, <c>false</c>.</value> + public bool RecordAnyChannel { get; set; } + + public int KeepUpTo { get; set; } + public KeepUntil KeepUntil { get; set; } + + public bool SkipEpisodesInLibrary { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether [record new only]. + /// </summary> + /// <value><c>true</c> if [record new only]; otherwise, <c>false</c>.</value> + public bool RecordNewOnly { get; set; } + + /// <summary> + /// Gets or sets the days. + /// </summary> + /// <value>The days.</value> + public List<DayOfWeek> Days { get; set; } + + /// <summary> + /// Gets or sets the priority. + /// </summary> + /// <value>The priority.</value> + public int Priority { get; set; } + + /// <summary> + /// Gets or sets the pre padding seconds. + /// </summary> + /// <value>The pre padding seconds.</value> + public int PrePaddingSeconds { get; set; } + + /// <summary> + /// Gets or sets the post padding seconds. + /// </summary> + /// <value>The post padding seconds.</value> + public int PostPaddingSeconds { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is pre padding required. + /// </summary> + /// <value><c>true</c> if this instance is pre padding required; otherwise, <c>false</c>.</value> + public bool IsPrePaddingRequired { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is post padding required. + /// </summary> + /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value> + public bool IsPostPaddingRequired { get; set; } + + /// <summary> + /// Gets or sets the series identifier. + /// </summary> + /// <value>The series identifier.</value> + public string SeriesId { get; set; } + + public SeriesTimerInfo() + { + Days = new List<DayOfWeek>(); + SkipEpisodesInLibrary = true; + KeepUntil = KeepUntil.UntilDeleted; + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs new file mode 100644 index 000000000..5b71a26a2 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -0,0 +1,10 @@ +using System; + +namespace MediaBrowser.Controller.LiveTv +{ + public class TimerEventInfo + { + public string Id { get; set; } + public Guid ProgramId { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs new file mode 100644 index 000000000..baf0b0b13 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -0,0 +1,175 @@ +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Controller.LiveTv +{ + public class TimerInfo + { + public TimerInfo() + { + Genres = new string[] { }; + KeepUntil = KeepUntil.UntilDeleted; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + Tags = new string[] { }; + } + + public Dictionary<string, string> ProviderIds { get; set; } + public Dictionary<string, string> SeriesProviderIds { get; set; } + public string[] Tags { get; set; } + + /// <summary> + /// Id of the recording. + /// </summary> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the series timer identifier. + /// </summary> + /// <value>The series timer identifier.</value> + public string SeriesTimerId { get; set; } + + /// <summary> + /// ChannelId of the recording. + /// </summary> + public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the program identifier. + /// </summary> + /// <value>The program identifier.</value> + public string ProgramId { get; set; } + + public string ShowId { get; set; } + + /// <summary> + /// Name of the recording. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Description of the recording. + /// </summary> + public string Overview { get; set; } + + public string SeriesId { get; set; } + + /// <summary> + /// The start date of the recording, in UTC. + /// </summary> + public DateTime StartDate { get; set; } + + /// <summary> + /// The end date of the recording, in UTC. + /// </summary> + public DateTime EndDate { get; set; } + + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public RecordingStatus Status { get; set; } + + /// <summary> + /// Gets or sets the pre padding seconds. + /// </summary> + /// <value>The pre padding seconds.</value> + public int PrePaddingSeconds { get; set; } + + /// <summary> + /// Gets or sets the post padding seconds. + /// </summary> + /// <value>The post padding seconds.</value> + public int PostPaddingSeconds { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is pre padding required. + /// </summary> + /// <value><c>true</c> if this instance is pre padding required; otherwise, <c>false</c>.</value> + public bool IsPrePaddingRequired { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is post padding required. + /// </summary> + /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value> + public bool IsPostPaddingRequired { get; set; } + + public bool IsManual { get; set; } + + /// <summary> + /// Gets or sets the priority. + /// </summary> + /// <value>The priority.</value> + public int Priority { get; set; } + + public int RetryCount { get; set; } + + // Program properties + public int? SeasonNumber { get; set; } + /// <summary> + /// Gets or sets the episode number. + /// </summary> + /// <value>The episode number.</value> + public int? EpisodeNumber { get; set; } + public bool IsMovie { get; set; } + public bool IsKids + { + get + { + return Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + } + } + public bool IsSports + { + get + { + return Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase); + } + } + public bool IsNews + { + get + { + return Tags.Contains("News", StringComparer.OrdinalIgnoreCase); + } + } + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is live. + /// </summary> + /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> + [IgnoreDataMember] + public bool IsLive + { + get + { + return Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); + } + } + + [IgnoreDataMember] + public bool IsPremiere + { + get + { + return Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); + } + } + + public int? ProductionYear { get; set; } + public string EpisodeTitle { get; set; } + public DateTime? OriginalAirDate { get; set; } + public bool IsProgramSeries { get; set; } + public bool IsRepeat { get; set; } + public string HomePageUrl { get; set; } + public float? CommunityRating { get; set; } + public string OfficialRating { get; set; } + public string[] Genres { get; set; } + public string RecordingPath { get; set; } + public KeepUntil KeepUntil { get; set; } + } +} diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs new file mode 100644 index 000000000..3b2df0471 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs @@ -0,0 +1,10 @@ +namespace MediaBrowser.Controller.LiveTv +{ + public class TunerChannelMapping + { + public string Name { get; set; } + public string ProviderChannelName { get; set; } + public string ProviderChannelId { get; set; } + public string Id { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj new file mode 100644 index 000000000..8e816080c --- /dev/null +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <ItemGroup> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + </ItemGroup> + + <ItemGroup> + <Compile Include="..\SharedVersion.cs"/> + </ItemGroup> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + </PropertyGroup> + +</Project> diff --git a/MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id b/MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id new file mode 100644 index 000000000..3c0ca209e --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id @@ -0,0 +1 @@ +4366f8d8bf9886d71e3e9ddae8480d953caf02cf
\ No newline at end of file diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs new file mode 100644 index 000000000..881e318ce --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -0,0 +1,2503 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Reflection; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class EncodingHelper + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + + public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder) + { + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + } + + public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var defaultEncoder = "libx264"; + + // Only use alternative encoders for video files. + // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully + // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. + if (state.VideoType == VideoType.VideoFile) + { + var hwType = encodingOptions.HardwareAccelerationType; + + if (!encodingOptions.EnableHardwareEncoding) + { + hwType = null; + } + + if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_qsv", defaultEncoder); + } + + if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_nvenc", defaultEncoder); + } + if (string.Equals(hwType, "amf", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_amf", defaultEncoder); + } + if (string.Equals(hwType, "omx", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_omx", defaultEncoder); + } + if (string.Equals(hwType, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_v4l2m2m", defaultEncoder); + } + if (string.Equals(hwType, "mediacodec", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_mediacodec", defaultEncoder); + } + if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(encodingOptions.VaapiDevice)) + { + if (IsVaapiSupported(state)) + { + return GetAvailableEncoder("h264_vaapi", defaultEncoder); + } + } + } + + return defaultEncoder; + } + + private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder) + { + if (_mediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; + } + return defaultEncoder; + } + + private bool IsVaapiSupported(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + + if (videoStream != null) + { + // vaapi will throw an error with this input + // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99. + if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + return true; + } + + /// <summary> + /// Gets the name of the output video codec + /// </summary> + public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var codec = state.OutputVideoCodec; + + if (!string.IsNullOrEmpty(codec)) + { + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + return GetH264Encoder(state, encodingOptions); + } + if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) + { + return "libvpx"; + } + if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase)) + { + return "wmv2"; + } + if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase)) + { + return "libtheora"; + } + + return codec.ToLower(); + } + + return "copy"; + } + + /// <summary> + /// Gets the user agent param. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + public string GetUserAgentParam(EncodingJobInfo state) + { + string useragent = null; + + state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); + + if (!string.IsNullOrEmpty(useragent)) + { + return "-user_agent \"" + useragent + "\""; + } + + return string.Empty; + } + + public string GetInputFormat(string container) + { + if (string.IsNullOrEmpty(container)) + { + return null; + } + + container = container.Replace("mkv", "matroska", StringComparison.OrdinalIgnoreCase); + + if (string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase)) + { + return "mpegts"; + } + + // For these need to find out the ffmpeg names + if (string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "wmv", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "mpg", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "mpeg", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "rec", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "ogm", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "divx", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "tp", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "rmvb", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(container, "rtp", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Seeing reported failures here, not sure yet if this is related to specfying input format + if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // obviously don't do this for strm files + if (string.Equals(container, "strm", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return container; + } + + public string GetDecoderFromCodec(string codec) + { + // For these need to find out the ffmpeg names + if (string.Equals(codec, "mp2", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(codec, "aac_latm", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + if (_mediaEncoder.SupportsDecoder(codec)) + { + return codec; + } + + return null; + } + + /// <summary> + /// Infers the audio codec based on the url + /// </summary> + public string InferAudioCodec(string container) + { + var ext = "." + (container ?? string.Empty); + + if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase)) + { + return "mp3"; + } + if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) + { + return "aac"; + } + if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) + { + return "wma"; + } + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase)) + { + return "vorbis"; + } + if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase)) + { + return "vorbis"; + } + if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) + { + return "vorbis"; + } + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) + { + return "vorbis"; + } + if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) + { + return "vorbis"; + } + + return "copy"; + } + + /// <summary> + /// Infers the video codec. + /// </summary> + /// <param name="url">The URL.</param> + /// <returns>System.Nullable{VideoCodecs}.</returns> + public string InferVideoCodec(string url) + { + var ext = Path.GetExtension(url); + + if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) + { + return "wmv"; + } + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) + { + return "vpx"; + } + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) + { + return "theora"; + } + if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) + { + return "h264"; + } + + return "copy"; + } + + public int GetVideoProfileScore(string profile) + { + var list = new [] + { + "Constrained Baseline", + "Baseline", + "Extended", + "Main", + "High", + "Progressive High", + "Constrained High" + }; + + // strip spaces because they may be stripped out on the query string + return Array.FindIndex(list, t => string.Equals(t.Replace(" ", ""), profile.Replace(" ", ""), StringComparison.OrdinalIgnoreCase)); + } + + public string GetInputPathArgument(EncodingJobInfo state) + { + var protocol = state.InputProtocol; + var mediaPath = state.MediaPath ?? string.Empty; + + var inputPath = new[] { mediaPath }; + + if (state.IsInputVideo) + { + if (!(state.VideoType == VideoType.Iso && state.IsoMount == null)) + { + inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames); + } + } + + return _mediaEncoder.GetInputArgument(inputPath, protocol); + } + + /// <summary> + /// Gets the audio encoder. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + public string GetAudioEncoder(EncodingJobInfo state) + { + var codec = state.OutputAudioCodec; + + if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) + { + return "aac -strict experimental"; + } + if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return "libmp3lame"; + } + if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase)) + { + return "libvorbis"; + } + if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase)) + { + return "wmav2"; + } + if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) + { + return "libopus"; + } + + return codec.ToLower(); + } + + /// <summary> + /// Gets the input argument. + /// </summary> + public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var request = state.BaseRequest; + + var arg = string.Format("-i {0}", GetInputPathArgument(state)); + + if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) + { + if (state.VideoStream != null && state.VideoStream.Width.HasValue) + { + // This is hacky but not sure how to get the exact subtitle resolution + double height = state.VideoStream.Width.Value; + height /= 16; + height *= 9; + + arg += string.Format(" -canvas_size {0}:{1}", state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture), Convert.ToInt32(height).ToString(CultureInfo.InvariantCulture)); + } + + var subtitlePath = state.SubtitleStream.Path; + + if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) + { + var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); + if (_fileSystem.FileExists(idxFile)) + { + subtitlePath = idxFile; + } + } + + arg += " -i \"" + subtitlePath + "\""; + } + } + + if (state.IsVideoRequest) + { + if (GetVideoEncoder(state, encodingOptions).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1) + { + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hwOutputFormat = "vaapi"; + + if (hasGraphicalSubs) + { + hwOutputFormat = "yuv420p"; + } + + arg = "-hwaccel vaapi -hwaccel_output_format " + hwOutputFormat + " -vaapi_device " + encodingOptions.VaapiDevice + " " + arg; + } + } + + return arg.Trim(); + } + + /// <summary> + /// Determines whether the specified stream is H264. + /// </summary> + /// <param name="stream">The stream.</param> + /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns> + public bool IsH264(MediaStream stream) + { + var codec = stream.Codec ?? string.Empty; + + return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || + codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; + } + + public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) + { + var bitrate = state.OutputVideoBitrate; + + if (bitrate.HasValue) + { + if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) + { + // With vpx when crf is used, b:v becomes a max rate + // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. + return string.Format(" -maxrate:v {0} -bufsize:v {1} -b:v {0}", bitrate.Value.ToString(_usCulture), (bitrate.Value * 2).ToString(_usCulture)); + } + + if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) + { + return string.Format(" -b:v {0}", bitrate.Value.ToString(_usCulture)); + } + + if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + // h264 + return string.Format(" -maxrate {0} -bufsize {1}", + bitrate.Value.ToString(_usCulture), + (bitrate.Value * 2).ToString(_usCulture)); + } + + // h264 + return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", + bitrate.Value.ToString(_usCulture), + (bitrate.Value * 2).ToString(_usCulture)); + } + + return string.Empty; + } + + public string NormalizeTranscodingLevel(string videoCodec, string level) + { + double requestLevel; + + // Clients may direct play higher than level 41, but there's no reason to transcode higher + if (double.TryParse(level, NumberStyles.Any, _usCulture, out requestLevel)) + { + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + if (requestLevel > 41) + { + return "41"; + } + } + } + + return level; + } + + /// <summary> + /// Gets the text subtitle param. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + public string GetTextSubtitleParam(EncodingJobInfo state) + { + var seconds = Math.Round(TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds); + + // hls always copies timestamps + var setPtsParam = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive + ? string.Empty + : string.Format(",setpts=PTS -{0}/TB", seconds.ToString(_usCulture)); + + string fallbackFontParam = string.Empty; + + var mediaPath = state.MediaPath ?? string.Empty; + + return string.Format("subtitles='{0}:si={1}'{2}{3}", + _mediaEncoder.EscapeSubtitleFilterPath(mediaPath), + state.InternalSubtitleStreamOffset.ToString(_usCulture), + fallbackFontParam, + setPtsParam); + } + + public double? GetFramerateParam(EncodingJobInfo state) + { + var request = state.BaseRequest; + + if (request.Framerate.HasValue) + { + return request.Framerate.Value; + } + + var maxrate = request.MaxFramerate; + + if (maxrate.HasValue && state.VideoStream != null) + { + var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; + + if (contentRate.HasValue && contentRate.Value > maxrate.Value) + { + return maxrate; + } + } + + return null; + } + + /// <summary> + /// Gets the video bitrate to specify on the command line + /// </summary> + public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultH264Preset) + { + var param = string.Empty; + + var isVc1 = state.VideoStream != null && + string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase); + + if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(encodingOptions.H264Preset)) + { + param += "-preset " + encodingOptions.H264Preset; + } + else + { + param += "-preset " + defaultH264Preset; + } + + if (encodingOptions.H264Crf >= 0 && encodingOptions.H264Crf <= 51) + { + param += " -crf " + encodingOptions.H264Crf.ToString(CultureInfo.InvariantCulture); + } + else + { + param += " -crf 23"; + } + } + + else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) + { + param += "-preset fast"; + + param += " -crf 28"; + } + + // h264 (h264_qsv) + else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + string[] valid_h264_qsv = new string[] { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; + + if (valid_h264_qsv.Contains(encodingOptions.H264Preset, StringComparer.OrdinalIgnoreCase)) + { + param += "-preset " + encodingOptions.H264Preset; + } + else + { + param += "-preset 7"; + } + + param += " -look_ahead 0"; + + } + + // h264 (h264_nvenc) + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + { + switch (encodingOptions.H264Preset) + { + case "veryslow": + + param += "-preset slow"; //lossless is only supported on maxwell and newer(2014+) + break; + + case "slow": + case "slower": + param += "-preset slow"; + break; + + case "medium": + param += "-preset medium"; + break; + + case "fast": + case "faster": + case "veryfast": + case "superfast": + case "ultrafast": + param += "-preset fast"; + break; + + default: + param += "-preset default"; + break; + } + } + + // webm + else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) + { + // Values 0-3, 0 being highest quality but slower + var profileScore = 0; + + string crf; + var qmin = "0"; + var qmax = "50"; + + crf = "10"; + + if (isVc1) + { + profileScore++; + } + + // Max of 2 + profileScore = Math.Min(profileScore, 2); + + // http://www.webmproject.org/docs/encoder-parameters/ + param += string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", + profileScore.ToString(_usCulture), + crf, + qmin, + qmax); + } + + else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase)) + { + param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; + } + + // asf/wmv + else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) + { + param += "-qmin 2"; + } + + else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase)) + { + param += "-mbd 2"; + } + + param += GetVideoBitrateParam(state, videoEncoder); + + var framerate = GetFramerateParam(state); + if (framerate.HasValue) + { + param += string.Format(" -r {0}", framerate.Value.ToString(_usCulture)); + } + + var targetVideoCodec = state.ActualOutputVideoCodec; + + var request = state.BaseRequest; + var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault(); + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + param += " -profile:v 578"; + } + else if (!string.IsNullOrEmpty(profile)) + { + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + { + // not supported by h264_omx + param += " -profile:v " + profile; + } + } + + var level = state.GetRequestedLevel(targetVideoCodec); + + if (!string.IsNullOrEmpty(level)) + { + level = NormalizeTranscodingLevel(state.OutputVideoCodec, level); + + // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format + // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307 + if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + { + switch (level) + { + case "30": + param += " -level 3.0"; + break; + case "31": + param += " -level 3.1"; + break; + case "32": + param += " -level 3.2"; + break; + case "40": + param += " -level 4.0"; + break; + case "41": + param += " -level 4.1"; + break; + case "42": + param += " -level 4.2"; + break; + case "50": + param += " -level 5.0"; + break; + case "51": + param += " -level 5.1"; + break; + case "52": + param += " -level 5.2"; + break; + default: + param += " -level " + level; + break; + } + } + // nvenc doesn't decode with param -level set ?! + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + { + //param += ""; + } + else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)) + { + param += " -level " + level; + } + } + + if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + { + param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none"; + } + + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + { + param = "-pix_fmt yuv420p " + param; + } + + if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + { + param = "-pix_fmt nv21 " + param; + } + + return param; + } + + public bool CanStreamCopyVideo(EncodingJobInfo state, MediaStream videoStream) + { + var request = state.BaseRequest; + + if (!request.AllowVideoStreamCopy) + { + return false; + } + + if (videoStream.IsInterlaced) + { + if (state.DeInterlace(videoStream.Codec, false)) + { + return false; + } + } + + if (videoStream.IsAnamorphic ?? false) + { + if (request.RequireNonAnamorphic) + { + return false; + } + } + + // Can't stream copy if we're burning in subtitles + if (request.SubtitleStreamIndex.HasValue) + { + if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + return false; + } + } + + if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value && request.RequireAvc) + { + return false; + } + } + + // Source and target codecs must match + if (string.IsNullOrEmpty(videoStream.Codec) || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + var requestedProfiles = state.GetRequestedProfiles(videoStream.Codec); + + // If client is requesting a specific video profile, it must match the source + if (requestedProfiles.Length > 0) + { + if (string.IsNullOrEmpty(videoStream.Profile)) + { + //return false; + } + + var requestedProfile = requestedProfiles[0]; + // strip spaces because they may be stripped out on the query string as well + if (!string.IsNullOrEmpty(videoStream.Profile) && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", ""), StringComparer.OrdinalIgnoreCase)) + { + var currentScore = GetVideoProfileScore(videoStream.Profile); + var requestedScore = GetVideoProfileScore(requestedProfile); + + if (currentScore == -1 || currentScore > requestedScore) + { + return false; + } + } + } + + // Video width must fall within requested value + if (request.MaxWidth.HasValue) + { + if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value) + { + return false; + } + } + + // Video height must fall within requested value + if (request.MaxHeight.HasValue) + { + if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value) + { + return false; + } + } + + // Video framerate must fall within requested value + var requestedFramerate = request.MaxFramerate ?? request.Framerate; + if (requestedFramerate.HasValue) + { + var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate; + + if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value) + { + return false; + } + } + + // Video bitrate must fall within requested value + if (request.VideoBitRate.HasValue) + { + if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value) + { + return false; + } + } + + var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec); + if (maxBitDepth.HasValue) + { + if (videoStream.BitDepth.HasValue && videoStream.BitDepth.Value > maxBitDepth.Value) + { + return false; + } + } + + var maxRefFrames = state.GetRequestedMaxRefFrames(videoStream.Codec); + if (maxRefFrames.HasValue) + { + if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > maxRefFrames.Value) + { + return false; + } + } + + // If a specific level was requested, the source must match or be less than + var level = state.GetRequestedLevel(videoStream.Codec); + if (!string.IsNullOrEmpty(level)) + { + double requestLevel; + + if (double.TryParse(level, NumberStyles.Any, _usCulture, out requestLevel)) + { + if (!videoStream.Level.HasValue) + { + //return false; + } + + if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel) + { + return false; + } + } + } + + if (string.Equals(state.InputContainer, "avi", StringComparison.OrdinalIgnoreCase) && + string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) && + !(videoStream.IsAVC ?? false)) + { + // see Coach S01E01 - Kelly and the Professor(0).avi + return false; + } + + return request.EnableAutoStreamCopy; + } + + public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, string[] supportedAudioCodecs) + { + var request = state.BaseRequest; + + if (!request.AllowAudioStreamCopy) + { + return false; + } + + var maxBitDepth = state.GetRequestedAudioBitDepth(audioStream.Codec); + if (maxBitDepth.HasValue) + { + if (audioStream.BitDepth.HasValue && audioStream.BitDepth.Value > maxBitDepth.Value) + { + return false; + } + } + + // Source and target codecs must match + if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + // Channels must fall within requested value + var channels = state.GetRequestedAudioChannels(audioStream.Codec); + if (channels.HasValue) + { + if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0) + { + return false; + } + if (audioStream.Channels.Value > channels.Value) + { + return false; + } + } + + // Sample rate must fall within requested value + if (request.AudioSampleRate.HasValue) + { + if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0) + { + return false; + } + if (audioStream.SampleRate.Value > request.AudioSampleRate.Value) + { + return false; + } + } + + // Video bitrate must fall within requested value + if (request.AudioBitRate.HasValue) + { + if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0) + { + return false; + } + if (audioStream.BitRate.Value > request.AudioBitRate.Value) + { + return false; + } + } + + return request.EnableAutoStreamCopy; + } + + public int? GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec) + { + var bitrate = request.VideoBitRate; + + if (videoStream != null) + { + var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue && + request.Height.Value > videoStream.Height.Value && request.Width.HasValue && videoStream.Width.HasValue && + request.Width.Value > videoStream.Width.Value; + + // Don't allow bitrate increases unless upscaling + if (!isUpscaling) + { + if (bitrate.HasValue && videoStream.BitRate.HasValue) + { + bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value); + } + } + } + + if (bitrate.HasValue) + { + var inputVideoCodec = videoStream == null ? null : videoStream.Codec; + bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); + + // If a max bitrate was requested, don't let the scaled bitrate exceed it + if (request.VideoBitRate.HasValue) + { + bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + } + } + + return bitrate; + } + + private int GetMinBitrate(int sourceBitrate, int requestedBitrate) + { + if (sourceBitrate <= 2000000) + { + sourceBitrate = Convert.ToInt32(sourceBitrate * 2.5); + } + else if (sourceBitrate <= 3000000) + { + sourceBitrate = Convert.ToInt32(sourceBitrate * 2); + } + + var bitrate = Math.Min(sourceBitrate, requestedBitrate); + + return bitrate; + } + + public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) + { + if (request.AudioBitRate.HasValue) + { + // Make sure we don't request a bitrate higher than the source + var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value; + + // Don't encode any higher than this + return Math.Min(384000, request.AudioBitRate.Value); + //return Math.Min(currentBitrate, request.AudioBitRate.Value); + } + + return null; + } + + public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls) + { + var channels = state.OutputAudioChannels; + + var filters = new List<string>(); + + // Boost volume to 200% when downsampling from 6ch to 2ch + if (channels.HasValue && channels.Value <= 2) + { + if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !encodingOptions.DownMixAudioBoost.Equals(1)) + { + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture)); + } + } + + var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; + if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) + { + var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; + + filters.Add(string.Format("asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(_usCulture))); + } + + if (filters.Count > 0) + { + return "-af \"" + string.Join(",", filters.ToArray()) + "\""; + } + + return string.Empty; + } + + /// <summary> + /// Gets the number of audio channels to specify on the command line + /// </summary> + /// <param name="request">The request.</param> + /// <param name="audioStream">The audio stream.</param> + /// <param name="outputAudioCodec">The output audio codec.</param> + /// <returns>System.Nullable{System.Int32}.</returns> + public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec) + { + var request = state.BaseRequest; + + var inputChannels = audioStream == null + ? null + : audioStream.Channels; + + if (inputChannels <= 0) + { + inputChannels = null; + } + + int? transcoderChannelLimit = null; + var codec = outputAudioCodec ?? string.Empty; + + if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) + { + // wmav2 currently only supports two channel output + transcoderChannelLimit = 2; + } + + else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1) + { + // libmp3lame currently only supports two channel output + transcoderChannelLimit = 2; + } + else + { + // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels + transcoderChannelLimit = 6; + } + + var isTranscodingAudio = !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); + + int? resultChannels = state.GetRequestedAudioChannels(codec); + if (isTranscodingAudio) + { + resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels); + } + + if (inputChannels.HasValue) + { + resultChannels = resultChannels.HasValue + ? Math.Min(resultChannels.Value, inputChannels.Value) + : inputChannels.Value; + } + + if (isTranscodingAudio && transcoderChannelLimit.HasValue) + { + resultChannels = resultChannels.HasValue + ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value) + : transcoderChannelLimit.Value; + } + + return resultChannels; + } + + private int? GetMinValue(int? val1, int? val2) + { + if (!val1.HasValue) + { + return val2; + } + if (!val2.HasValue) + { + return val1; + } + + return Math.Min(val1.Value, val2.Value); + } + + /// <summary> + /// Enforces the resolution limit. + /// </summary> + /// <param name="state">The state.</param> + public void EnforceResolutionLimit(EncodingJobInfo state) + { + var videoRequest = state.BaseRequest; + + // Switch the incoming params to be ceilings rather than fixed values + videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width; + videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height; + + videoRequest.Width = null; + videoRequest.Height = null; + } + + /// <summary> + /// Gets the fast seek command line parameter. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.String.</returns> + /// <value>The fast seek command line parameter.</value> + public string GetFastSeekCommandLineParameter(BaseEncodingJobOptions request) + { + var time = request.StartTimeTicks ?? 0; + + if (time > 0) + { + return string.Format("-ss {0}", _mediaEncoder.GetTimeParameter(time)); + } + + return string.Empty; + } + + /// <summary> + /// Gets the map args. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + public string GetMapArgs(EncodingJobInfo state) + { + // If we don't have known media info + // If input is video, use -sn to drop subtitles + // Otherwise just return empty + if (state.VideoStream == null && state.AudioStream == null) + { + return state.IsInputVideo ? "-sn" : string.Empty; + } + + // We have media info, but we don't know the stream indexes + if (state.VideoStream != null && state.VideoStream.Index == -1) + { + return "-sn"; + } + + // We have media info, but we don't know the stream indexes + if (state.AudioStream != null && state.AudioStream.Index == -1) + { + return state.IsInputVideo ? "-sn" : string.Empty; + } + + var args = string.Empty; + + if (state.VideoStream != null) + { + args += string.Format("-map 0:{0}", state.VideoStream.Index); + } + else + { + // No known video stream + args += "-vn"; + } + + if (state.AudioStream != null) + { + args += string.Format(" -map 0:{0}", state.AudioStream.Index); + } + + else + { + args += " -map -0:a"; + } + + var subtitleMethod = state.SubtitleDeliveryMethod; + if (state.SubtitleStream == null || subtitleMethod == SubtitleDeliveryMethod.Hls) + { + args += " -map -0:s"; + } + else if (subtitleMethod == SubtitleDeliveryMethod.Embed) + { + args += string.Format(" -map 0:{0}", state.SubtitleStream.Index); + } + else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) + { + args += " -map 1:0 -sn"; + } + + return args; + } + + /// <summary> + /// Determines which stream will be used for playback + /// </summary> + /// <param name="allStream">All stream.</param> + /// <param name="desiredIndex">Index of the desired.</param> + /// <param name="type">The type.</param> + /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param> + /// <returns>MediaStream.</returns> + public MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true) + { + var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList(); + + if (desiredIndex.HasValue) + { + var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value); + + if (stream != null) + { + return stream; + } + } + + if (returnFirstIfNoIndex && type == MediaStreamType.Audio) + { + return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ?? + streams.FirstOrDefault(); + } + + // Just return the first one + return returnFirstIfNoIndex ? streams.FirstOrDefault() : null; + } + + /// <summary> + /// Gets the internal graphical subtitle param. + /// </summary> + public string GetGraphicalSubtitleParam(EncodingJobInfo state, EncodingOptions options, string outputVideoCodec) + { + var outputSizeParam = string.Empty; + + var request = state.BaseRequest; + + // Add resolution params, if specified + if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) + { + outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); + + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + var index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = "," + outputSizeParam.Substring(index); + } + } + else + { + var index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = "," + outputSizeParam.Substring(index); + } + } + } + + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && outputSizeParam.Length == 0) + { + outputSizeParam = ",format=nv12|vaapi,hwupload"; + } + + var videoSizeParam = string.Empty; + + if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) + { + videoSizeParam = string.Format("scale={0}:{1}", state.VideoStream.Width.Value.ToString(_usCulture), state.VideoStream.Height.Value.ToString(_usCulture)); + + videoSizeParam += ":force_original_aspect_ratio=decrease"; + } + + var mapPrefix = state.SubtitleStream.IsExternal ? + 1 : + 0; + + var subtitleStreamIndex = state.SubtitleStream.IsExternal + ? 0 + : state.SubtitleStream.Index; + + return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"", + mapPrefix.ToString(_usCulture), + subtitleStreamIndex.ToString(_usCulture), + state.VideoStream.Index.ToString(_usCulture), + outputSizeParam, + videoSizeParam); + } + + private Tuple<int?, int?> GetFixedOutputSize(int? videoWidth, + int? videoHeight, + int? requestedWidth, + int? requestedHeight, + int? requestedMaxWidth, + int? requestedMaxHeight) + { + if (!videoWidth.HasValue && !requestedWidth.HasValue) + { + return new Tuple<int?, int?>(null, null); + } + if (!videoHeight.HasValue && !requestedHeight.HasValue) + { + return new Tuple<int?, int?>(null, null); + } + + decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth); + decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight); + decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth; + decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight; + decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth; + decimal maximumHeight = requestedMaxHeight.HasValue ? Convert.ToDecimal(requestedMaxHeight.Value) : outputHeight; + + if (outputWidth > maximumWidth || outputHeight > maximumHeight) + { + var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight); + outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale)); + outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale)); + } + + outputWidth = 2 * Math.Truncate(outputWidth / 2); + outputHeight = 2 * Math.Truncate(outputHeight / 2); + + return new Tuple<int?, int?>(Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); + } + + public List<string> GetScalingFilters(int? videoWidth, + int? videoHeight, + Video3DFormat? threedFormat, + string videoDecoder, + string videoEncoder, + int? requestedWidth, + int? requestedHeight, + int? requestedMaxWidth, + int? requestedMaxHeight) + { + var filters = new List<string>(); + var fixedOutputSize = GetFixedOutputSize(videoWidth, videoHeight, requestedWidth, requestedHeight, requestedMaxWidth, requestedMaxHeight); + + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + { + // Work around vaapi's reduced scaling features + var scaler = "scale_vaapi"; + + // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions + // (outputWidth, outputHeight). The user may request precise output dimensions or maximum + // output dimensions. Output dimensions are guaranteed to be even. + var outputWidth = fixedOutputSize.Item1.Value; + var outputHeight = fixedOutputSize.Item2.Value; + + if (!videoWidth.HasValue || outputWidth != videoWidth.Value || !videoHeight.HasValue || outputHeight != videoHeight.Value) + { + filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(_usCulture), outputHeight.ToString(_usCulture))); + } + } + else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + { + // Nothing to do, it's handled as an input resize filter + } + else + { + var isExynosV4L2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase); + + // If fixed dimensions were supplied + if (requestedWidth.HasValue && requestedHeight.HasValue) + { + if (isExynosV4L2) + { + var widthParam = requestedWidth.Value.ToString(_usCulture); + var heightParam = requestedHeight.Value.ToString(_usCulture); + + filters.Add(string.Format("scale=trunc({0}/64)*64:trunc({1}/2)*2", widthParam, heightParam)); + } + else + { + filters.Add(GetFixedSizeScalingFilter(threedFormat, requestedWidth.Value, requestedHeight.Value)); + } + } + + // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size + else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue) + { + var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture); + var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture); + + if (isExynosV4L2) + { + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/64)*64:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + } + else + { + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + } + } + + // If a fixed width was requested + else if (requestedWidth.HasValue) + { + if (threedFormat.HasValue) + { + // This method can handle 0 being passed in for the requested height + filters.Add(GetFixedSizeScalingFilter(threedFormat, requestedWidth.Value, 0)); + } + else + { + var widthParam = requestedWidth.Value.ToString(_usCulture); + + filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); + } + } + + // If a fixed height was requested + else if (requestedHeight.HasValue) + { + var heightParam = requestedHeight.Value.ToString(_usCulture); + + if (isExynosV4L2) + { + filters.Add(string.Format("scale=trunc(oh*a/64)*64:{0}", heightParam)); + } + else + { + filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); + } + } + + // If a max width was requested + else if (requestedMaxWidth.HasValue) + { + var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture); + + if (isExynosV4L2) + { + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/64)*64:trunc(ow/dar/2)*2", maxWidthParam)); + } + else + { + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); + } + } + + // If a max height was requested + else if (requestedMaxHeight.HasValue) + { + var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture); + + if (isExynosV4L2) + { + filters.Add(string.Format("scale=trunc(oh*a/64)*64:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + } + else + { + filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + } + } + } + + return filters; + } + + private string GetFixedSizeScalingFilter(Video3DFormat? threedFormat, int requestedWidth, int requestedHeight) + { + var widthParam = requestedWidth.ToString(_usCulture); + var heightParam = requestedHeight.ToString(_usCulture); + + string filter = null; + + if (threedFormat.HasValue) + { + switch (threedFormat.Value) + { + case Video3DFormat.HalfSideBySide: + filter = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. + break; + case Video3DFormat.FullSideBySide: + filter = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + //fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. + break; + case Video3DFormat.HalfTopAndBottom: + filter = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + //htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth + break; + case Video3DFormat.FullTopAndBottom: + filter = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2"; + // ftab crop height in half, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth + break; + default: + break; + } + } + + // default + if (filter == null) + { + if (requestedHeight > 0) + { + filter = "scale=trunc({0}/2)*2:trunc({1}/2)*2"; + } + else + { + filter = "scale={0}:trunc({0}/dar/2)*2"; + } + } + + return string.Format(filter, widthParam, heightParam); + } + + /// <summary> + /// If we're going to put a fixed size on the command line, this will calculate it + /// </summary> + public string GetOutputSizeParam(EncodingJobInfo state, + EncodingOptions options, + string outputVideoCodec, + bool allowTimeStampCopy = true) + { + // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ + + var request = state.BaseRequest; + + var filters = new List<string>(); + + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + filters.Add("format=nv12|vaapi"); + filters.Add("hwupload"); + } + + if (state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + filters.Add(string.Format("deinterlace_vaapi")); + } + + var videoStream = state.VideoStream; + + if (state.DeInterlace("h264", true) && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + var inputFramerate = videoStream == null ? null : videoStream.RealFrameRate; + + // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle + if (string.Equals(options.DeinterlaceMethod, "bobandweave", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) + { + filters.Add("yadif=1:-1:0"); + } + else + { + filters.Add("yadif=0:-1:0"); + } + } + + var inputWidth = videoStream == null ? null : videoStream.Width; + var inputHeight = videoStream == null ? null : videoStream.Height; + var threeDFormat = state.MediaSource.Video3DFormat; + + var videoDecoder = GetVideoDecoder(state, options); + + filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); + + var output = string.Empty; + + if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + var subParam = GetTextSubtitleParam(state); + + filters.Add(subParam); + + if (allowTimeStampCopy) + { + output += " -copyts"; + } + } + + if (filters.Count > 0) + { + output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); + } + + return output; + } + + + /// <summary> + /// Gets the number of threads. + /// </summary> + public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, string outputVideoCodec) + { + if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) + { + // per docs: + // -threads number of threads to use for encoding, can't be 0 [auto] with VP8 (recommended value : number of real cores - 1) + return Math.Max(Environment.ProcessorCount - 1, 1); + } + + var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount; + + // Automatic + if (threads <= 0 || threads >= Environment.ProcessorCount) + { + return 0; + } + + return threads; + } + + public void TryStreamCopy(EncodingJobInfo state) + { + if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream)) + { + state.OutputVideoCodec = "copy"; + } + else + { + var user = state.User; + + // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not + if (user != null && !user.Policy.EnableVideoPlaybackTranscoding) + { + state.OutputVideoCodec = "copy"; + } + } + + if (state.AudioStream != null && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) + { + state.OutputAudioCodec = "copy"; + } + else + { + var user = state.User; + + // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not + if (user != null && !user.Policy.EnableAudioPlaybackTranscoding) + { + state.OutputAudioCodec = "copy"; + } + } + } + + public static string GetProbeSizeArgument(int numInputFiles) + { + return numInputFiles > 1 ? "-probesize 1G" : ""; + } + + public static string GetAnalyzeDurationArgument(int numInputFiles) + { + return numInputFiles > 1 ? "-analyzeduration 200M" : ""; + } + + public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var inputModifier = string.Empty; + + var numInputFiles = state.PlayableStreamFileNames.Length > 0 ? state.PlayableStreamFileNames.Length : 1; + var probeSizeArgument = GetProbeSizeArgument(numInputFiles); + + string analyzeDurationArgument; + if (state.MediaSource.AnalyzeDurationMs.HasValue) + { + analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture); + } + else + { + analyzeDurationArgument = GetAnalyzeDurationArgument(numInputFiles); + } + + if (!string.IsNullOrEmpty(probeSizeArgument)) + { + inputModifier += " " + probeSizeArgument; + } + + if (!string.IsNullOrEmpty(analyzeDurationArgument)) + { + inputModifier += " " + analyzeDurationArgument; + } + + inputModifier = inputModifier.Trim(); + + var userAgentParam = GetUserAgentParam(state); + + if (!string.IsNullOrEmpty(userAgentParam)) + { + inputModifier += " " + userAgentParam; + } + + inputModifier = inputModifier.Trim(); + + inputModifier += " " + GetFastSeekCommandLineParameter(state.BaseRequest); + inputModifier = inputModifier.Trim(); + + if (state.InputProtocol == MediaProtocol.Rtsp) + { + inputModifier += " -rtsp_transport tcp -rtsp_transport udp -rtsp_flags prefer_tcp"; + } + + if (!string.IsNullOrEmpty(state.InputAudioSync)) + { + inputModifier += " -async " + state.InputAudioSync; + } + + if (!string.IsNullOrEmpty(state.InputVideoSync)) + { + inputModifier += " -vsync " + state.InputVideoSync; + } + + if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp) + { + inputModifier += " -re"; + } + + var flags = new List<string>(); + if (state.IgnoreInputDts) + { + flags.Add("+igndts"); + } + if (state.IgnoreInputIndex) + { + flags.Add("+ignidx"); + } + if (state.GenPtsInput) + { + flags.Add("+genpts"); + } + if (state.DiscardCorruptFramesInput) + { + flags.Add("+discardcorrupt"); + } + if (state.EnableFastSeekInput) + { + flags.Add("+fastseek"); + } + + if (flags.Count > 0) + { + inputModifier += " -fflags " + string.Join("", flags.ToArray()); + } + + var videoDecoder = GetVideoDecoder(state, encodingOptions); + if (!string.IsNullOrEmpty(videoDecoder)) + { + inputModifier += " " + videoDecoder; + + var videoStream = state.VideoStream; + var inputWidth = videoStream == null ? null : videoStream.Width; + var inputHeight = videoStream == null ? null : videoStream.Height; + var request = state.BaseRequest; + + var fixedOutputSize = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + + if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + { + inputModifier += string.Format(" -resize {0}x{1}", fixedOutputSize.Item1.Value.ToString(_usCulture), fixedOutputSize.Item2.Value.ToString(_usCulture)); + } + } + + if (state.IsVideoRequest) + { + var outputVideoCodec = GetVideoEncoder(state, encodingOptions); + + // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking + if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) && + state.TranscodingType != TranscodingJobType.Progressive && + state.EnableBreakOnNonKeyFrames(outputVideoCodec)) + { + inputModifier += " -noaccurate_seek"; + } + + if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + { + var inputFormat = GetInputFormat(state.InputContainer); + if (!string.IsNullOrEmpty(inputFormat)) + { + inputModifier += " -f " + inputFormat; + } + } + } + + if (state.MediaSource.RequiresLooping) + { + inputModifier += " -stream_loop -1"; + } + + return inputModifier; + } + + + public void AttachMediaSourceInfo(EncodingJobInfo state, + MediaSourceInfo mediaSource, + string requestedUrl) + { + if (state == null) + { + throw new ArgumentNullException("state"); + } + if (mediaSource == null) + { + throw new ArgumentNullException("mediaSource"); + } + + var path = mediaSource.Path; + var protocol = mediaSource.Protocol; + + if (!string.IsNullOrEmpty(mediaSource.EncoderPath) && mediaSource.EncoderProtocol.HasValue) + { + path = mediaSource.EncoderPath; + protocol = mediaSource.EncoderProtocol.Value; + } + + state.MediaPath = path; + state.InputProtocol = protocol; + state.InputContainer = mediaSource.Container; + state.RunTimeTicks = mediaSource.RunTimeTicks; + state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; + + state.IsoType = mediaSource.IsoType; + + if (mediaSource.VideoType.HasValue) + { + state.VideoType = mediaSource.VideoType.Value; + + if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd) + { + state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray(); + } + else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.BluRay) + { + state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.BluRay).Select(Path.GetFileName).ToArray(); + } + else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd) + { + state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray(); + } + else + { + state.PlayableStreamFileNames = new string[] { }; + } + } + else + { + state.PlayableStreamFileNames = new string[] { }; + } + + if (mediaSource.Timestamp.HasValue) + { + state.InputTimestamp = mediaSource.Timestamp.Value; + } + + state.RunTimeTicks = mediaSource.RunTimeTicks; + state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; + state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; + + if (state.ReadInputAtNativeFramerate || + mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) + { + state.InputVideoSync = "-1"; + state.InputAudioSync = "1"; + } + + if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase) || + string.Equals(mediaSource.Container, "asf", StringComparison.OrdinalIgnoreCase)) + { + // Seeing some stuttering when transcoding wma to audio-only HLS + state.InputAudioSync = "1"; + } + + var mediaStreams = mediaSource.MediaStreams; + + if (state.IsVideoRequest) + { + var videoRequest = state.BaseRequest; + + if (string.IsNullOrEmpty(videoRequest.VideoCodec)) + { + if (string.IsNullOrEmpty(requestedUrl)) + { + requestedUrl = "test." + videoRequest.OutputContainer; + } + + videoRequest.VideoCodec = InferVideoCodec(requestedUrl); + } + + state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video); + state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false); + state.SubtitleDeliveryMethod = videoRequest.SubtitleMethod; + state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio); + + if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal) + { + state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream); + } + + EnforceResolutionLimit(state); + + NormalizeSubtitleEmbed(state); + } + else + { + state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); + } + + state.MediaSource = mediaSource; + } + + private void NormalizeSubtitleEmbed(EncodingJobInfo state) + { + if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) + { + return; + } + + // This is tricky to remux in, after converting to dvdsub it's not positioned correctly + // Therefore, let's just burn it in + if (string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)) + { + state.SubtitleDeliveryMethod = SubtitleDeliveryMethod.Encode; + } + } + + /// <summary> + /// Gets the name of the output video codec + /// </summary> + protected string GetVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions) + { + if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return GetVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions); + } + + public string GetVideoDecoder(VideoType videoType, MediaStream videoStream, EncodingOptions encodingOptions) + { + // Only use alternative encoders for video files. + // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully + // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. + if (videoType != VideoType.VideoFile) + { + return null; + } + + if (videoStream != null && + !string.IsNullOrEmpty(videoStream.Codec) && + !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + { + if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + { + switch (videoStream.Codec.ToLower()) + { + case "avc": + case "h264": + if (_mediaEncoder.SupportsDecoder("h264_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) + { + // qsv decoder does not support 10-bit input + if ((videoStream.BitDepth ?? 8) > 8) + { + return null; + } + return "-c:v h264_qsv "; + } + break; + case "hevc": + case "h265": + if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) + { + //return "-c:v hevc_qsv -load_plugin hevc_hw "; + return "-c:v hevc_qsv "; + } + break; + case "mpeg2video": + if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg2_qsv "; + } + break; + case "vc1": + if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v vc1_qsv "; + } + break; + } + } + + else if (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + switch (videoStream.Codec.ToLower()) + { + case "avc": + case "h264": + if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v h264_cuvid "; + } + break; + case "hevc": + case "h265": + if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v hevc_cuvid "; + } + break; + case "mpeg2video": + if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg2_cuvid "; + } + break; + case "vc1": + if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v vc1_cuvid "; + } + break; + case "mpeg4": + if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg4_cuvid "; + } + break; + } + } + + else if (string.Equals(encodingOptions.HardwareAccelerationType, "mediacodec", StringComparison.OrdinalIgnoreCase)) + { + switch (videoStream.Codec.ToLower()) + { + case "avc": + case "h264": + if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v h264_mediacodec "; + } + break; + case "hevc": + case "h265": + if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v hevc_mediacodec "; + } + break; + case "mpeg2video": + if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg2_mediacodec "; + } + break; + case "mpeg4": + if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg4_mediacodec "; + } + break; + case "vp8": + if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v vp8_mediacodec "; + } + break; + case "vp9": + if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v vp9_mediacodec "; + } + break; + } + } + + else if (string.Equals(encodingOptions.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase)) + { + switch (videoStream.Codec.ToLower()) + { + case "avc": + case "h264": + if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v h264_mmal"; + } + break; + case "mpeg2video": + if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg2_mmal"; + } + break; + } + } + + else if (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + { + switch (videoStream.Codec.ToLower()) + { + case "avc": + case "h264": + if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v h264_amf"; + } + break; + case "mpeg2video": + if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) + { + return "-c:v mpeg2_mmal"; + } + break; + } + } + } + + // leave blank so ffmpeg will decide + return null; + } + + public string GetSubtitleEmbedArguments(EncodingJobInfo state) + { + if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) + { + return string.Empty; + } + + var format = state.SupportedSubtitleCodecs.FirstOrDefault(); + string codec; + + if (string.IsNullOrEmpty(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + codec = "copy"; + } + else + { + codec = format; + } + + var args = " -codec:s:0 " + codec; + + args += " -disposition:s:0 default"; + + return args; + } + + public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset) + { + // Get the output codec name + var videoCodec = GetVideoEncoder(state, encodingOptions); + + var format = string.Empty; + var keyFrame = string.Empty; + + if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) && + state.BaseRequest.Context == EncodingContext.Streaming) + { + // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js + format = " -f mp4 -movflags frag_keyframe+empty_moov"; + } + + var threads = GetNumberOfThreads(state, encodingOptions, videoCodec); + + var inputModifier = GetInputModifier(state, encodingOptions); + + return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", + inputModifier, + GetInputArgument(state, encodingOptions), + keyFrame, + GetMapArgs(state), + GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset), + threads, + GetProgressiveVideoAudioArguments(state, encodingOptions), + GetSubtitleEmbedArguments(state), + format, + outputPath + ).Trim(); + } + + public string GetOutputFFlags(EncodingJobInfo state) + { + var flags = new List<string>(); + if (state.GenPtsOutput) + { + flags.Add("+genpts"); + } + + if (flags.Count > 0) + { + return " -fflags " + string.Join("", flags.ToArray()); + } + + return string.Empty; + } + + public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset) + { + var args = "-codec:v:0 " + videoCodec; + + if (state.BaseRequest.EnableMpegtsM2TsMode) + { + args += " -mpegts_m2ts_mode 1"; + } + + if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (state.VideoStream != null && IsH264(state.VideoStream) && + string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && + !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + { + args += " -bsf:v h264_mp4toannexb"; + } + + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) + { + args += " -copyts -avoid_negative_ts disabled -start_at_zero"; + } + + if (!state.RunTimeTicks.HasValue) + { + args += " -flags -global_header -fflags +genpts"; + } + } + else + { + var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", + 5.ToString(_usCulture)); + + args += keyFrameArg; + + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + var hasCopyTs = false; + // Add resolution params, if specified + if (!hasGraphicalSubs) + { + var outputSizeParam = GetOutputSizeParam(state, encodingOptions, videoCodec); + args += outputSizeParam; + hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; + } + + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) + { + if (!hasCopyTs) + { + args += " -copyts"; + } + args += " -avoid_negative_ts disabled -start_at_zero"; + } + + // This is for internal graphical subs + if (hasGraphicalSubs) + { + args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + } + + var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset); + + if (!string.IsNullOrEmpty(qualityParam)) + { + args += " " + qualityParam.Trim(); + } + + if (!state.RunTimeTicks.HasValue) + { + args += " -flags -global_header"; + } + } + + if (!string.IsNullOrEmpty(state.OutputVideoSync)) + { + args += " -vsync " + state.OutputVideoSync; + } + + args += GetOutputFFlags(state); + + return args; + } + + public string GetProgressiveVideoAudioArguments(EncodingJobInfo state, EncodingOptions encodingOptions) + { + // If the video doesn't have an audio stream, return a default. + if (state.AudioStream == null && state.VideoStream != null) + { + return string.Empty; + } + + // Get the output codec name + var codec = GetAudioEncoder(state); + + var args = "-codec:a:0 " + codec; + + if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return args; + } + + // Add the number of audio channels + var channels = state.OutputAudioChannels; + + if (channels.HasValue) + { + args += " -ac " + channels.Value; + } + + var bitrate = state.OutputAudioBitrate; + + if (bitrate.HasValue) + { + args += " -ab " + bitrate.Value.ToString(_usCulture); + } + + if (state.OutputAudioSampleRate.HasValue) + { + args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture); + } + + args += " " + GetAudioFilterParam(state, encodingOptions, false); + + return args; + } + + public string GetProgressiveAudioFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath) + { + var audioTranscodeParams = new List<string>(); + + var bitrate = state.OutputAudioBitrate; + + if (bitrate.HasValue) + { + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture)); + } + + if (state.OutputAudioChannels.HasValue) + { + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(_usCulture)); + } + + // opus will fail on 44100 + if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + { + if (state.OutputAudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture)); + } + } + + var albumCoverInput = string.Empty; + var mapArgs = string.Empty; + var metadata = string.Empty; + var vn = string.Empty; + + var hasArt = !string.IsNullOrEmpty(state.AlbumCoverPath); + hasArt = false; + + if (hasArt) + { + albumCoverInput = " -i \"" + state.AlbumCoverPath + "\""; + mapArgs = " -map 0:a -map 1:v -c:1:v copy"; + metadata = " -metadata:s:v title=\"Album cover\" -metadata:s:v comment=\"Cover(Front)\""; + } + else + { + vn = " -vn"; + } + + var threads = GetNumberOfThreads(state, encodingOptions, null); + + var inputModifier = GetInputModifier(state, encodingOptions); + + return string.Format("{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"", + inputModifier, + GetInputArgument(state, encodingOptions), + threads, + vn, + string.Join(" ", audioTranscodeParams.ToArray()), + outputPath, + metadata, + albumCoverInput, + mapArgs).Trim(); + } + + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs new file mode 100644 index 000000000..3d2871e65 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -0,0 +1,775 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.MediaEncoding +{ + // For now, a common base class until the API and MediaEncoding classes are unified + public abstract class EncodingJobInfo + { + private readonly ILogger _logger; + + public MediaStream VideoStream { get; set; } + public VideoType VideoType { get; set; } + public Dictionary<string, string> RemoteHttpHeaders { get; set; } + public string OutputVideoCodec { get; set; } + public MediaProtocol InputProtocol { get; set; } + public string MediaPath { get; set; } + public bool IsInputVideo { get; set; } + public IIsoMount IsoMount { get; set; } + public string[] PlayableStreamFileNames { get; set; } + public string OutputAudioCodec { get; set; } + public int? OutputVideoBitrate { get; set; } + public MediaStream SubtitleStream { get; set; } + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + public string[] SupportedSubtitleCodecs { get; set; } + + public int InternalSubtitleStreamOffset { get; set; } + public MediaSourceInfo MediaSource { get; set; } + public User User { get; set; } + + public long? RunTimeTicks { get; set; } + + public bool ReadInputAtNativeFramerate { get; set; } + + private TranscodeReason[] _transcodeReasons = null; + public TranscodeReason[] TranscodeReasons + { + get + { + if (_transcodeReasons == null) + { + _transcodeReasons = (BaseRequest.TranscodeReasons ?? string.Empty) + .Split(',') + .Where(i => !string.IsNullOrEmpty(i)) + .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true)) + .ToArray(); + } + + return _transcodeReasons; + } + } + + public bool IgnoreInputDts + { + get + { + return MediaSource.IgnoreDts; + } + } + + public bool IgnoreInputIndex + { + get + { + return MediaSource.IgnoreIndex; + } + } + + public bool GenPtsInput + { + get + { + return MediaSource.GenPtsInput; + } + } + + public bool DiscardCorruptFramesInput + { + get + { + return false; + } + } + + public bool EnableFastSeekInput + { + get + { + return false; + } + } + + public bool GenPtsOutput + { + get + { + return false; + } + } + + public string OutputContainer { get; set; } + + public string OutputVideoSync + { + get + { + // For live tv + in progress recordings + if (string.Equals(InputContainer, "mpegts", StringComparison.OrdinalIgnoreCase) || string.Equals(InputContainer, "ts", StringComparison.OrdinalIgnoreCase)) + { + if (!MediaSource.RunTimeTicks.HasValue) + { + return "cfr"; + } + } + + return "-1"; + } + } + + public string AlbumCoverPath { get; set; } + + public string InputAudioSync { get; set; } + public string InputVideoSync { get; set; } + public TransportStreamTimestamp InputTimestamp { get; set; } + + public MediaStream AudioStream { get; set; } + public string[] SupportedAudioCodecs { get; set; } + public string[] SupportedVideoCodecs { get; set; } + public string InputContainer { get; set; } + public IsoType? IsoType { get; set; } + + public BaseEncodingJobOptions BaseRequest { get; set; } + + public long? StartTimeTicks + { + get { return BaseRequest.StartTimeTicks; } + } + + public bool CopyTimestamps + { + get { return BaseRequest.CopyTimestamps; } + } + + public int? OutputAudioBitrate; + public int? OutputAudioChannels; + + public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced) + { + var videoStream = VideoStream; + var isInputInterlaced = videoStream != null && videoStream.IsInterlaced; + + if (!isInputInterlaced) + { + return false; + } + + // Support general param + if (BaseRequest.DeInterlace) + { + return true; + } + + if (!string.IsNullOrEmpty(videoCodec)) + { + if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + if (forceDeinterlaceIfSourceIsInterlaced) + { + if (isInputInterlaced) + { + return true; + } + } + + return false; + } + + public string[] GetRequestedProfiles(string codec) + { + if (!string.IsNullOrEmpty(BaseRequest.Profile)) + { + return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + if (!string.IsNullOrEmpty(codec)) + { + var profile = BaseRequest.GetOption(codec, "profile"); + + if (!string.IsNullOrEmpty(profile)) + { + return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + return new string[] { }; + } + + public string GetRequestedLevel(string codec) + { + if (!string.IsNullOrEmpty(BaseRequest.Level)) + { + return BaseRequest.Level; + } + + if (!string.IsNullOrEmpty(codec)) + { + return BaseRequest.GetOption(codec, "level"); + } + + return null; + } + + public int? GetRequestedMaxRefFrames(string codec) + { + if (BaseRequest.MaxRefFrames.HasValue) + { + return BaseRequest.MaxRefFrames; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "maxrefframes"); + int result; + if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + } + + return null; + } + + public int? GetRequestedVideoBitDepth(string codec) + { + if (BaseRequest.MaxVideoBitDepth.HasValue) + { + return BaseRequest.MaxVideoBitDepth; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "videobitdepth"); + int result; + if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + } + + return null; + } + + public int? GetRequestedAudioBitDepth(string codec) + { + if (BaseRequest.MaxAudioBitDepth.HasValue) + { + return BaseRequest.MaxAudioBitDepth; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "audiobitdepth"); + int result; + if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + } + + return null; + } + + public int? GetRequestedAudioChannels(string codec) + { + if (BaseRequest.MaxAudioChannels.HasValue) + { + return BaseRequest.MaxAudioChannels; + } + if (BaseRequest.AudioChannels.HasValue) + { + return BaseRequest.AudioChannels; + } + + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "audiochannels"); + int result; + if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + } + + return null; + } + + public bool IsVideoRequest { get; set; } + public TranscodingJobType TranscodingType { get; set; } + + public EncodingJobInfo(ILogger logger, IMediaSourceManager unused, TranscodingJobType jobType) + { + _logger = logger; + TranscodingType = jobType; + RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + PlayableStreamFileNames = new string[] { }; + SupportedAudioCodecs = new string[] { }; + SupportedVideoCodecs = new string[] { }; + SupportedSubtitleCodecs = new string[] { }; + } + + public bool IsSegmentedLiveStream + { + get + { + return TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue; + } + } + + public bool EnableBreakOnNonKeyFrames(string videoCodec) + { + if (TranscodingType != TranscodingJobType.Progressive) + { + if (IsSegmentedLiveStream) + { + return false; + } + + return BaseRequest.BreakOnNonKeyFrames && string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + public int? TotalOutputBitrate + { + get + { + return (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0); + } + } + + public int? OutputWidth + { + get + { + if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) + { + var size = new ImageSize + { + Width = VideoStream.Width.Value, + Height = VideoStream.Height.Value + }; + + var newSize = DrawingUtils.Resize(size, + BaseRequest.Width ?? 0, + BaseRequest.Height ?? 0, + BaseRequest.MaxWidth ?? 0, + BaseRequest.MaxHeight ?? 0); + + return Convert.ToInt32(newSize.Width); + } + + if (!IsVideoRequest) + { + return null; + } + + return BaseRequest.MaxWidth ?? BaseRequest.Width; + } + } + + public int? OutputHeight + { + get + { + if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) + { + var size = new ImageSize + { + Width = VideoStream.Width.Value, + Height = VideoStream.Height.Value + }; + + var newSize = DrawingUtils.Resize(size, + BaseRequest.Width ?? 0, + BaseRequest.Height ?? 0, + BaseRequest.MaxWidth ?? 0, + BaseRequest.MaxHeight ?? 0); + + return Convert.ToInt32(newSize.Height); + } + + if (!IsVideoRequest) + { + return null; + } + + return BaseRequest.MaxHeight ?? BaseRequest.Height; + } + } + + public int? OutputAudioSampleRate + { + get + { + if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (AudioStream != null) + { + return AudioStream.SampleRate; + } + } + + else if (BaseRequest.AudioSampleRate.HasValue) + { + // Don't exceed what the encoder supports + // Seeing issues of attempting to encode to 88200 + return Math.Min(44100, BaseRequest.AudioSampleRate.Value); + } + + return null; + } + } + + public int? OutputAudioBitDepth + { + get + { + if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (AudioStream != null) + { + return AudioStream.BitDepth; + } + } + + //else if (BaseRequest.AudioSampleRate.HasValue) + //{ + // // Don't exceed what the encoder supports + // // Seeing issues of attempting to encode to 88200 + // return Math.Min(44100, BaseRequest.AudioSampleRate.Value); + //} + + return null; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public double? TargetVideoLevel + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.Level; + } + + var level = GetRequestedLevel(ActualOutputVideoCodec); + double result; + if (!string.IsNullOrEmpty(level) && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + return null; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public int? TargetVideoBitDepth + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.BitDepth; + } + + return null; + } + } + + /// <summary> + /// Gets the target reference frames. + /// </summary> + /// <value>The target reference frames.</value> + public int? TargetRefFrames + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.RefFrames; + } + + return null; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public float? TargetFramerate + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); + } + + return BaseRequest.MaxFramerate ?? BaseRequest.Framerate; + } + } + + public TransportStreamTimestamp TargetTimestamp + { + get + { + var defaultValue = string.Equals(OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ? + TransportStreamTimestamp.Valid : + TransportStreamTimestamp.None; + + return !BaseRequest.Static + ? defaultValue + : InputTimestamp; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public int? TargetPacketLength + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.PacketLength; + } + + return null; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public string TargetVideoProfile + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.Profile; + } + + var requestedProfile = GetRequestedProfiles(ActualOutputVideoCodec).FirstOrDefault(); + if (!string.IsNullOrEmpty(requestedProfile)) + { + return requestedProfile; + } + + return null; + } + } + + /// <summary> + /// Predicts the audio sample rate that will be in the output stream + /// </summary> + public string TargetVideoRange + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.VideoRange; + } + + return "SDR"; + } + } + + public string TargetAudioProfile + { + get + { + if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return AudioStream == null ? null : AudioStream.Profile; + } + + return null; + } + } + + public string TargetVideoCodecTag + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.CodecTag; + } + + return null; + } + } + + public bool? IsTargetAnamorphic + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.IsAnamorphic; + } + + return false; + } + } + + public string ActualOutputVideoCodec + { + get + { + var codec = OutputVideoCodec; + + if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + { + var stream = VideoStream; + + if (stream != null) + { + return stream.Codec; + } + + return null; + } + + return codec; + } + } + + public bool? IsTargetInterlaced + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? (bool?)null : VideoStream.IsInterlaced; + } + + if (DeInterlace(ActualOutputVideoCodec, true)) + { + return false; + } + + return VideoStream == null ? (bool?)null : VideoStream.IsInterlaced; + } + } + + public bool? IsTargetAVC + { + get + { + if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return VideoStream == null ? null : VideoStream.IsAVC; + } + + return false; + } + } + + public int? TargetVideoStreamCount + { + get + { + if (BaseRequest.Static) + { + return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); + } + return GetMediaStreamCount(MediaStreamType.Video, 1); + } + } + + public int? TargetAudioStreamCount + { + get + { + if (BaseRequest.Static) + { + return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); + } + return GetMediaStreamCount(MediaStreamType.Audio, 1); + } + } + + private int? GetMediaStreamCount(MediaStreamType type, int limit) + { + var count = MediaSource.GetStreamCount(type); + + if (count.HasValue) + { + count = Math.Min(count.Value, limit); + } + + return count; + } + + protected void DisposeIsoMount() + { + if (IsoMount != null) + { + try + { + IsoMount.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing iso mount", ex); + } + + IsoMount = null; + } + } + + public IProgress<double> Progress { get; set; } + public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) { + Progress.Report(percentComplete.Value); + } + + public virtual void Dispose () { + } + } + + /// <summary> + /// Enum TranscodingJobType + /// </summary> + public enum TranscodingJobType + { + /// <summary> + /// The progressive + /// </summary> + Progressive, + /// <summary> + /// The HLS + /// </summary> + Hls, + /// <summary> + /// The dash + /// </summary> + Dash + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs new file mode 100644 index 000000000..7333149c2 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class EncodingJobOptions : BaseEncodingJobOptions + { + public string OutputDirectory { get; set; } + + public string DeviceId { get; set; } + public string ItemId { get; set; } + public string MediaSourceId { get; set; } + public string AudioCodec { get; set; } + + public DeviceProfile DeviceProfile { get; set; } + + public bool ReadInputAtNativeFramerate { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance has fixed resolution. + /// </summary> + /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value> + public bool HasFixedResolution + { + get + { + return Width.HasValue || Height.HasValue; + } + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile) + { + OutputContainer = info.Container; + StartTimeTicks = info.StartPositionTicks; + MaxWidth = info.MaxWidth; + MaxHeight = info.MaxHeight; + MaxFramerate = info.MaxFramerate; + ItemId = info.ItemId.ToString(""); + MediaSourceId = info.MediaSourceId; + AudioCodec = info.TargetAudioCodec.FirstOrDefault(); + MaxAudioChannels = info.GlobalMaxAudioChannels; + AudioBitRate = info.AudioBitrate; + AudioSampleRate = info.TargetAudioSampleRate; + DeviceProfile = deviceProfile; + VideoCodec = info.TargetVideoCodec.FirstOrDefault(); + VideoBitRate = info.VideoBitrate; + AudioStreamIndex = info.AudioStreamIndex; + SubtitleMethod = info.SubtitleDeliveryMethod; + Context = info.Context; + TranscodingMaxAudioChannels = info.TranscodingMaxAudioChannels; + + if (info.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) + { + SubtitleStreamIndex = info.SubtitleStreamIndex; + } + StreamOptions = info.StreamOptions; + } + } + + // For now until api and media encoding layers are unified + public class BaseEncodingJobOptions + { + [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool EnableAutoStreamCopy { get; set; } + + public bool AllowVideoStreamCopy { get; set; } + public bool AllowAudioStreamCopy { get; set; } + public bool BreakOnNonKeyFrames { get; set; } + + /// <summary> + /// Gets or sets the audio sample rate. + /// </summary> + /// <value>The audio sample rate.</value> + [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? AudioSampleRate { get; set; } + + public int? MaxAudioBitDepth { get; set; } + + /// <summary> + /// Gets or sets the audio bit rate. + /// </summary> + /// <value>The audio bit rate.</value> + [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? AudioBitRate { get; set; } + + /// <summary> + /// Gets or sets the audio channels. + /// </summary> + /// <value>The audio channels.</value> + [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? AudioChannels { get; set; } + + [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MaxAudioChannels { get; set; } + + [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool Static { get; set; } + + /// <summary> + /// Gets or sets the profile. + /// </summary> + /// <value>The profile.</value> + [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Profile { get; set; } + + /// <summary> + /// Gets or sets the level. + /// </summary> + /// <value>The level.</value> + [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Level { get; set; } + + /// <summary> + /// Gets or sets the framerate. + /// </summary> + /// <value>The framerate.</value> + [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] + public float? Framerate { get; set; } + + [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] + public float? MaxFramerate { get; set; } + + [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool CopyTimestamps { get; set; } + + /// <summary> + /// Gets or sets the start time ticks. + /// </summary> + /// <value>The start time ticks.</value> + [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public long? StartTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + /// <value>The width.</value> + [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? Width { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + /// <value>The height.</value> + [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? Height { get; set; } + + /// <summary> + /// Gets or sets the width of the max. + /// </summary> + /// <value>The width of the max.</value> + [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MaxWidth { get; set; } + + /// <summary> + /// Gets or sets the height of the max. + /// </summary> + /// <value>The height of the max.</value> + [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MaxHeight { get; set; } + + /// <summary> + /// Gets or sets the video bit rate. + /// </summary> + /// <value>The video bit rate.</value> + [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? VideoBitRate { get; set; } + + /// <summary> + /// Gets or sets the index of the subtitle stream. + /// </summary> + /// <value>The index of the subtitle stream.</value> + [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? SubtitleStreamIndex { get; set; } + + [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public SubtitleDeliveryMethod SubtitleMethod { get; set; } + + [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MaxRefFrames { get; set; } + + [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MaxVideoBitDepth { get; set; } + public bool RequireAvc { get; set; } + public bool DeInterlace { get; set; } + public bool RequireNonAnamorphic { get; set; } + public int? TranscodingMaxAudioChannels { get; set; } + public int? CpuCoreLimit { get; set; } + public string OutputContainer { get; set; } + public string LiveStreamId { get; set; } + + public bool EnableMpegtsM2TsMode { get; set; } + + /// <summary> + /// Gets or sets the video codec. + /// </summary> + /// <value>The video codec.</value> + [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string VideoCodec { get; set; } + + public string SubtitleCodec { get; set; } + + public string TranscodeReasons { get; set; } + + /// <summary> + /// Gets or sets the index of the audio stream. + /// </summary> + /// <value>The index of the audio stream.</value> + [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? AudioStreamIndex { get; set; } + + /// <summary> + /// Gets or sets the index of the video stream. + /// </summary> + /// <value>The index of the video stream.</value> + [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? VideoStreamIndex { get; set; } + + public EncodingContext Context { get; set; } + + public Dictionary<string, string> StreamOptions { get; set; } + + public string GetOption(string qualifier, string name) + { + var value = GetOption(qualifier + "-" + name); + + if (string.IsNullOrEmpty(value)) + { + value = GetOption(name); + } + + return value; + } + + public string GetOption(string name) + { + string value; + if (StreamOptions.TryGetValue(name, out value)) + { + return value; + } + + return null; + } + + public BaseEncodingJobOptions() + { + EnableAutoStreamCopy = true; + AllowVideoStreamCopy = true; + AllowAudioStreamCopy = true; + Context = EncodingContext.Streaming; + StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public string TempDirectory { get; set; } + public string DeviceId { get; set; } + public Guid Id { get; set; } + public string Container { get; set; } + public DeviceProfile DeviceProfile { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs new file mode 100644 index 000000000..7d50efd5e --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Controller.Providers; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public interface IEncodingManager + { + /// <summary> + /// Refreshes the chapter images. + /// </summary> + Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs new file mode 100644 index 000000000..2b85b8975 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -0,0 +1,124 @@ +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.MediaEncoding +{ + /// <summary> + /// Interface IMediaEncoder + /// </summary> + public interface IMediaEncoder : ITranscoderSupport + { + string EncoderLocationType { get; } + + /// <summary> + /// Gets the encoder path. + /// </summary> + /// <value>The encoder path.</value> + string EncoderPath { get; } + + /// <summary> + /// Supportses the decoder. + /// </summary> + /// <param name="decoder">The decoder.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool SupportsDecoder(string decoder); + + /// <summary> + /// Extracts the audio image. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="imageStreamIndex">Index of the image stream.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken); + + /// <summary> + /// Extracts the video image. + /// </summary> + Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); + + Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); + + /// <summary> + /// Extracts the video images on interval. + /// </summary> + Task ExtractVideoImagesOnInterval(string[] inputFiles, + string container, + MediaStream videoStream, + MediaProtocol protocol, + Video3DFormat? threedFormat, + TimeSpan interval, + string targetDirectory, + string filenamePrefix, + int? maxWidth, + CancellationToken cancellationToken); + + /// <summary> + /// Gets the media info. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Gets the input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <param name="protocol">The protocol.</param> + /// <returns>System.String.</returns> + string GetInputArgument(string[] inputFiles, MediaProtocol protocol); + + /// <summary> + /// Gets the time parameter. + /// </summary> + /// <param name="ticks">The ticks.</param> + /// <returns>System.String.</returns> + string GetTimeParameter(long ticks); + + /// <summary> + /// Encodes the audio. + /// </summary> + /// <param name="options">The options.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task<string> EncodeAudio(EncodingJobOptions options, + IProgress<double> progress, + CancellationToken cancellationToken); + + /// <summary> + /// Encodes the video. + /// </summary> + /// <param name="options">The options.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<System.String>.</returns> + Task<string> EncodeVideo(EncodingJobOptions options, + IProgress<double> progress, + CancellationToken cancellationToken); + + Task ConvertImage(string inputPath, string outputPath); + + /// <summary> + /// Escapes the subtitle filter path. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>System.String.</returns> + string EscapeSubtitleFilterPath(string path); + + void Init(); + + void UpdateEncoderPath(string path, string pathType); + bool SupportsEncoder(string encoder); + + string[] GetPlayableStreamFileNames(string path, VideoType videoType); + IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber); + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs new file mode 100644 index 000000000..de7496d42 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -0,0 +1,33 @@ +using MediaBrowser.Model.MediaInfo; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public interface ISubtitleEncoder + { + /// <summary> + /// Gets the subtitles. + /// </summary> + /// <returns>Task{Stream}.</returns> + Task<Stream> GetSubtitles(BaseItem item, + string mediaSourceId, + int subtitleStreamIndex, + string outputFormat, + long startTimeTicks, + long endTimeTicks, + bool preserveOriginalTimestamps, + CancellationToken cancellationToken); + + /// <summary> + /// Gets the subtitle language encoding parameter. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="protocol">The protocol.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>System.String.</returns> + Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs new file mode 100644 index 000000000..a8d1e5a0f --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs @@ -0,0 +1,20 @@ + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class ImageEncodingOptions + { + public string InputPath { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public int? MaxWidth { get; set; } + + public int? MaxHeight { get; set; } + + public int? Quality { get; set; } + + public string Format { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs new file mode 100644 index 000000000..5f3f79d77 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -0,0 +1,149 @@ +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Logging; +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class JobLogger + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly ILogger _logger; + + public JobLogger(ILogger logger) + { + _logger = logger; + } + + public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + { + try + { + using (var reader = new StreamReader(source)) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + ParseLogLine(line, state); + + var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await target.FlushAsync().ConfigureAwait(false); + } + } + } + catch (ObjectDisposedException) + { + // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux + } + catch (Exception ex) + { + _logger.ErrorException("Error reading ffmpeg log", ex); + } + } + + private void ParseLogLine(string line, EncodingJobInfo state) + { + float? framerate = null; + double? percent = null; + TimeSpan? transcodingPosition = null; + long? bytesTranscoded = null; + int? bitRate = null; + + var parts = line.Split(' '); + + var totalMs = state.RunTimeTicks.HasValue + ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds + : 0; + + var startMs = state.BaseRequest.StartTimeTicks.HasValue + ? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds + : 0; + + for (var i = 0; i < parts.Length; i++) + { + var part = parts[i]; + + if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) && + (i + 1 < parts.Length)) + { + var rate = parts[i + 1]; + float val; + + if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val)) + { + framerate = val; + } + } + else if (state.RunTimeTicks.HasValue && + part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) + { + var time = part.Split(new[] { '=' }, 2).Last(); + TimeSpan val; + + if (TimeSpan.TryParse(time, _usCulture, out val)) + { + var currentMs = startMs + val.TotalMilliseconds; + + var percentVal = currentMs / totalMs; + percent = 100 * percentVal; + + transcodingPosition = val; + } + } + else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) + { + var size = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + long val; + + if (long.TryParse(size, NumberStyles.Any, _usCulture, out val)) + { + bytesTranscoded = val * scale.Value; + } + } + } + else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + float val; + + if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val)) + { + bitRate = (int)Math.Ceiling(val * scale.Value); + } + } + } + } + + if (framerate.HasValue || percent.HasValue) + { + state.ReportTranscodingProgress(transcodingPosition, 0, percent, 0, bitRate); + } + } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs new file mode 100644 index 000000000..70e4db84f --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -0,0 +1,53 @@ +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using System; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Controller.MediaEncoding +{ + /// <summary> + /// Class MediaEncoderHelpers + /// </summary> + public static class MediaEncoderHelpers + { + /// <summary> + /// Gets the input argument. + /// </summary> + /// <param name="fileSystem">The file system.</param> + /// <param name="videoPath">The video path.</param> + /// <param name="protocol">The protocol.</param> + /// <param name="isoMount">The iso mount.</param> + /// <param name="playableStreamFileNames">The playable stream file names.</param> + /// <returns>System.String[][].</returns> + public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, MediaProtocol protocol, IIsoMount isoMount, string[] playableStreamFileNames) + { + if (playableStreamFileNames.Length > 0) + { + if (isoMount == null) + { + return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); + } + return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames); + } + + return new[] {videoPath}; + } + + private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, string[] filenames) + { + if (filenames.Length == 0) + { + return new string[]{}; + } + + var allFiles = fileSystem + .GetFilePaths(rootPath, true) + .ToArray(); + + return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase))) + .Where(f => !string.IsNullOrEmpty(f)) + .ToArray(); + } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs new file mode 100644 index 000000000..1d7222801 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using System; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class MediaInfoRequest + { + public MediaSourceInfo MediaSource { get; set; } + public bool ExtractChapters { get; set; } + public DlnaProfileType MediaType { get; set; } + public IIsoMount MountedIso { get; set; } + public string[] PlayableStreamFileNames { get; set; } + + public MediaInfoRequest() + { + PlayableStreamFileNames = new string[] {}; + } + } +} diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs new file mode 100644 index 000000000..2f31b8e66 --- /dev/null +++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs @@ -0,0 +1,69 @@ +using System; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes + { + public static IAuthService AuthService { get; set; } + + /// <summary> + /// Gets or sets the roles. + /// </summary> + /// <value>The roles.</value> + public string Roles { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether [escape parental control]. + /// </summary> + /// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value> + public bool EscapeParentalControl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether [allow before startup wizard]. + /// </summary> + /// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value> + public bool AllowBeforeStartupWizard { get; set; } + + public bool AllowLocal { get; set; } + + /// <summary> + /// The request filter is executed before the service. + /// </summary> + /// <param name="request">The http request wrapper</param> + /// <param name="response">The http response wrapper</param> + /// <param name="requestDto">The request DTO</param> + public void RequestFilter(IRequest request, IResponse response, object requestDto) + { + AuthService.Authenticate(request, this); + } + + /// <summary> + /// Order in which Request Filters are executed. + /// <0 Executed before global request filters + /// >0 Executed after global request filters + /// </summary> + /// <value>The priority.</value> + public int Priority + { + get { return 0; } + } + + public string[] GetRoles() + { + return (Roles ?? string.Empty).Split(new []{ ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public bool AllowLocalOnly { get; set; } + } + + public interface IAuthenticationAttributes + { + bool EscapeParentalControl { get; } + bool AllowBeforeStartupWizard { get; } + bool AllowLocal { get; } + bool AllowLocalOnly { get; } + + string[] GetRoles(); + } +} diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs new file mode 100644 index 000000000..a68060db5 --- /dev/null +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -0,0 +1,52 @@ +using MediaBrowser.Controller.Entities; +using System; + + +namespace MediaBrowser.Controller.Net +{ + public class AuthorizationInfo + { + /// <summary> + /// Gets or sets the user identifier. + /// </summary> + /// <value>The user identifier.</value> + public Guid UserId { + get { + if (User == null) { + return Guid.Empty; + } + else { + return User.Id; + } + } + } + + /// <summary> + /// Gets or sets the device identifier. + /// </summary> + /// <value>The device identifier.</value> + public string DeviceId { get; set; } + /// <summary> + /// Gets or sets the device. + /// </summary> + /// <value>The device.</value> + public string Device { get; set; } + /// <summary> + /// Gets or sets the client. + /// </summary> + /// <value>The client.</value> + public string Client { get; set; } + /// <summary> + /// Gets or sets the version. + /// </summary> + /// <value>The version.</value> + public string Version { get; set; } + /// <summary> + /// Gets or sets the token. + /// </summary> + /// <value>The token.</value> + public string Token { get; set; } + + public User User { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs new file mode 100644 index 000000000..7df96b777 --- /dev/null +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -0,0 +1,322 @@ +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Threading; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received + /// </summary> + /// <typeparam name="TReturnDataType">The type of the T return data type.</typeparam> + /// <typeparam name="TStateType">The type of the T state type.</typeparam> + public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable + where TStateType : WebSocketListenerState, new() + where TReturnDataType : class + { + /// <summary> + /// The _active connections + /// </summary> + protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>> ActiveConnections = + new List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>>(); + + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + protected abstract string Name { get; } + + /// <summary> + /// Gets the data to send. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>Task{`1}.</returns> + protected abstract Task<TReturnDataType> GetDataToSend(TStateType state, CancellationToken cancellationToken); + + /// <summary> + /// The logger + /// </summary> + protected ILogger Logger; + + protected ITimerFactory TimerFactory { get; private set; } + + protected BasePeriodicWebSocketListener(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + Logger = logger; + } + + /// <summary> + /// Processes the message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>Task.</returns> + public Task ProcessMessage(WebSocketMessageInfo message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase)) + { + Start(message); + } + + if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase)) + { + Stop(message); + } + + return Task.FromResult(true); + } + + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + protected virtual bool SendOnTimer + { + get + { + return false; + } + } + + protected virtual void ParseMessageParams(string[] values) + { + + } + + /// <summary> + /// Starts sending messages over a web socket + /// </summary> + /// <param name="message">The message.</param> + private void Start(WebSocketMessageInfo message) + { + var vals = message.Data.Split(','); + + var dueTimeMs = long.Parse(vals[0], UsCulture); + var periodMs = long.Parse(vals[1], UsCulture); + + if (vals.Length > 2) + { + ParseMessageParams(vals.Skip(2).ToArray()); + } + + var cancellationTokenSource = new CancellationTokenSource(); + + Logger.Debug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); + + var timer = SendOnTimer ? + TimerFactory.Create(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite) : + null; + + var state = new TStateType + { + IntervalMs = periodMs, + InitialDelayMs = dueTimeMs + }; + + lock (ActiveConnections) + { + ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>(message.Connection, cancellationTokenSource, timer, state)); + } + + if (timer != null) + { + timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs)); + } + } + + /// <summary> + /// Timers the callback. + /// </summary> + /// <param name="state">The state.</param> + private void TimerCallback(object state) + { + var connection = (IWebSocketConnection)state; + + Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> tuple; + + lock (ActiveConnections) + { + tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection); + } + + if (tuple == null) + { + return; + } + + if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested) + { + DisposeConnection(tuple); + return; + } + + SendData(tuple); + } + + protected void SendData(bool force) + { + Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>[] tuples; + + lock (ActiveConnections) + { + tuples = ActiveConnections + .Where(c => + { + if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) + { + var state = c.Item4; + + if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs) + { + return true; + } + } + + return false; + }) + .ToArray(); + } + + foreach (var tuple in tuples) + { + SendData(tuple); + } + } + + private async void SendData(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> tuple) + { + var connection = tuple.Item1; + + try + { + var state = tuple.Item4; + + var cancellationToken = tuple.Item2.Token; + + var data = await GetDataToSend(state, cancellationToken).ConfigureAwait(false); + + if (data != null) + { + await connection.SendAsync(new WebSocketMessage<TReturnDataType> + { + MessageType = Name, + Data = data + + }, cancellationToken).ConfigureAwait(false); + + state.DateLastSendUtc = DateTime.UtcNow; + } + } + catch (OperationCanceledException) + { + if (tuple.Item2.IsCancellationRequested) + { + DisposeConnection(tuple); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error sending web socket message {0}", ex, Name); + DisposeConnection(tuple); + } + } + + /// <summary> + /// Stops sending messages over a web socket + /// </summary> + /// <param name="message">The message.</param> + private void Stop(WebSocketMessageInfo message) + { + lock (ActiveConnections) + { + var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection); + + if (connection != null) + { + DisposeConnection(connection); + } + } + } + + /// <summary> + /// Disposes the connection. + /// </summary> + /// <param name="connection">The connection.</param> + private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> connection) + { + Logger.Debug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); + + var timer = connection.Item3; + + if (timer != null) + { + try + { + timer.Dispose(); + } + catch (ObjectDisposedException) + { + + } + } + + try + { + connection.Item2.Cancel(); + connection.Item2.Dispose(); + } + catch (ObjectDisposedException) + { + + } + + ActiveConnections.Remove(connection); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + lock (ActiveConnections) + { + foreach (var connection in ActiveConnections.ToArray()) + { + DisposeConnection(connection); + } + } + } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + } + } + + public class WebSocketListenerState + { + public DateTime DateLastSendUtc { get; set; } + public long InitialDelayMs { get; set; } + public long IntervalMs { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs new file mode 100644 index 000000000..361320250 --- /dev/null +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -0,0 +1,9 @@ +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public interface IAuthService + { + void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); + } +} diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs new file mode 100644 index 000000000..5a9d0aa30 --- /dev/null +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -0,0 +1,21 @@ +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public interface IAuthorizationContext + { + /// <summary> + /// Gets the authorization information. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <returns>AuthorizationInfo.</returns> + AuthorizationInfo GetAuthorizationInfo(object requestContext); + + /// <summary> + /// Gets the authorization information. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <returns>AuthorizationInfo.</returns> + AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + } +} diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs new file mode 100644 index 000000000..03144e4b8 --- /dev/null +++ b/MediaBrowser.Controller/Net/IHasResultFactory.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Interface IHasResultFactory + /// Services that require a ResultFactory should implement this + /// </summary> + public interface IHasResultFactory : IRequiresRequest + { + /// <summary> + /// Gets or sets the result factory. + /// </summary> + /// <value>The result factory.</value> + IHttpResultFactory ResultFactory { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs new file mode 100644 index 000000000..f8e631de3 --- /dev/null +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Interface IHttpResultFactory + /// </summary> + public interface IHttpResultFactory + { + /// <summary> + /// Gets the result. + /// </summary> + /// <param name="content">The content.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="responseHeaders">The response headers.</param> + /// <returns>System.Object.</returns> + object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null); + + object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null); + object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null); + object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null); + + object GetRedirectResult(string url); + + object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) + where T : class; + + /// <summary> + /// Gets the static result. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="lastDateModified">The last date modified.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="factoryFn">The factory fn.</param> + /// <param name="responseHeaders">The response headers.</param> + /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> + /// <returns>System.Object.</returns> + Task<object> GetStaticResult(IRequest requestContext, + Guid cacheKey, + DateTime? lastDateModified, + TimeSpan? cacheDuration, + string contentType, Func<Task<Stream>> factoryFn, + IDictionary<string, string> responseHeaders = null, + bool isHeadRequest = false); + + /// <summary> + /// Gets the static result. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="options">The options.</param> + /// <returns>System.Object.</returns> + Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options); + + /// <summary> + /// Gets the static file result. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="path">The path.</param> + /// <param name="fileShare">The file share.</param> + /// <returns>System.Object.</returns> + Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read); + + /// <summary> + /// Gets the static file result. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="options">The options.</param> + /// <returns>System.Object.</returns> + Task<object> GetStaticFileResult(IRequest requestContext, + StaticFileResultOptions options); + } +} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs new file mode 100644 index 000000000..d2ebadcfa --- /dev/null +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Events; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Interface IHttpServer + /// </summary> + public interface IHttpServer : IDisposable + { + /// <summary> + /// Gets the URL prefix. + /// </summary> + /// <value>The URL prefix.</value> + string[] UrlPrefixes { get; } + + /// <summary> + /// Stops this instance. + /// </summary> + void Stop(); + + /// <summary> + /// Occurs when [web socket connected]. + /// </summary> + event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; + + /// <summary> + /// Inits this instance. + /// </summary> + void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listener); + + /// <summary> + /// If set, all requests will respond with this message + /// </summary> + string GlobalResponse { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs new file mode 100644 index 000000000..37ddbc2b3 --- /dev/null +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -0,0 +1,16 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public interface ISessionContext + { + SessionInfo GetSession(object requestContext); + User GetUser(object requestContext); + + SessionInfo GetSession(IRequest requestContext); + User GetUser(IRequest requestContext); + } +} diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs new file mode 100644 index 000000000..816e9afca --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -0,0 +1,85 @@ +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using System.Net.WebSockets; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace MediaBrowser.Controller.Net +{ + public interface IWebSocketConnection : IDisposable + { + /// <summary> + /// Occurs when [closed]. + /// </summary> + event EventHandler<EventArgs> Closed; + + /// <summary> + /// Gets the id. + /// </summary> + /// <value>The id.</value> + Guid Id { get; } + + /// <summary> + /// Gets the last activity date. + /// </summary> + /// <value>The last activity date.</value> + DateTime LastActivityDate { get; } + + /// <summary> + /// Gets or sets the URL. + /// </summary> + /// <value>The URL.</value> + string Url { get; set; } + /// <summary> + /// Gets or sets the query string. + /// </summary> + /// <value>The query string.</value> + QueryParamCollection QueryString { get; set; } + + /// <summary> + /// Gets or sets the receive action. + /// </summary> + /// <value>The receive action.</value> + Func<WebSocketMessageInfo, Task> OnReceive { get; set; } + + /// <summary> + /// Gets the state. + /// </summary> + /// <value>The state.</value> + WebSocketState State { get; } + + /// <summary> + /// Gets the remote end point. + /// </summary> + /// <value>The remote end point.</value> + string RemoteEndPoint { get; } + + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">message</exception> + Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); + + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <param name="buffer">The buffer.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendAsync(byte[] buffer, CancellationToken cancellationToken); + + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <param name="text">The text.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">buffer</exception> + Task SendAsync(string text, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs new file mode 100644 index 000000000..29698c1a4 --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + ///This is an interface for listening to messages coming through a web socket connection + /// </summary> + public interface IWebSocketListener + { + /// <summary> + /// Processes the message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>Task.</returns> + Task ProcessMessage(WebSocketMessageInfo message); + } +} diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs new file mode 100644 index 000000000..b251ab9a9 --- /dev/null +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -0,0 +1,21 @@ +using System; + +namespace MediaBrowser.Controller.Net +{ + public class SecurityException : Exception + { + public SecurityException(string message) + : base(message) + { + + } + + public SecurityExceptionType SecurityExceptionType { get; set; } + } + + public enum SecurityExceptionType + { + Unauthenticated = 0, + ParentalControl = 1 + } +} diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs new file mode 100644 index 000000000..1c9b2586d --- /dev/null +++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Net +{ + public class StaticResultOptions + { + public string ContentType { get; set; } + public TimeSpan? CacheDuration { get; set; } + public DateTime? DateLastModified { get; set; } + public Guid CacheKey { get; set; } + + public Func<Task<Stream>> ContentFactory { get; set; } + + public bool IsHeadRequest { get; set; } + + public IDictionary<string, string> ResponseHeaders { get; set; } + + public Action OnComplete { get; set; } + public Action OnError { get; set; } + + public string Path { get; set; } + public long? ContentLength { get; set; } + + public FileShareMode FileShare { get; set; } + + public StaticResultOptions() + { + ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + FileShare = FileShareMode.Read; + } + } + + public class StaticFileResultOptions : StaticResultOptions + { + } +} diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs new file mode 100644 index 000000000..b200f883a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Specialized; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Class WebSocketConnectEventArgs + /// </summary> + + public class WebSocketConnectingEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the URL. + /// </summary> + /// <value>The URL.</value> + public string Url { get; set; } + /// <summary> + /// Gets or sets the endpoint. + /// </summary> + /// <value>The endpoint.</value> + public string Endpoint { get; set; } + /// <summary> + /// Gets or sets the query string. + /// </summary> + /// <value>The query string.</value> + public QueryParamCollection QueryString { get; set; } + /// <summary> + /// Gets or sets a value indicating whether [allow connection]. + /// </summary> + /// <value><c>true</c> if [allow connection]; otherwise, <c>false</c>.</value> + public bool AllowConnection { get; set; } + + public WebSocketConnectingEventArgs() + { + QueryString = new QueryParamCollection(); + AllowConnection = true; + } + } + +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs new file mode 100644 index 000000000..332f16420 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -0,0 +1,16 @@ +using MediaBrowser.Model.Net; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Class WebSocketMessageInfo + /// </summary> + public class WebSocketMessageInfo : WebSocketMessage<string> + { + /// <summary> + /// Gets or sets the connection. + /// </summary> + /// <value>The connection.</value> + public IWebSocketConnection Connection { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs new file mode 100644 index 000000000..161f0ffba --- /dev/null +++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs @@ -0,0 +1,41 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Notifications; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; + +namespace MediaBrowser.Controller.Notifications +{ + public interface INotificationManager + { + /// <summary> + /// Sends the notification. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendNotification(NotificationRequest request, CancellationToken cancellationToken); + + Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken); + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="services">The services.</param> + /// <param name="notificationTypeFactories">The notification type factories.</param> + void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories); + + /// <summary> + /// Gets the notification types. + /// </summary> + /// <returns>IEnumerable{NotificationTypeInfo}.</returns> + List<NotificationTypeInfo> GetNotificationTypes(); + + /// <summary> + /// Gets the notification services. + /// </summary> + /// <returns>IEnumerable{NotificationServiceInfo}.</returns> + IEnumerable<NameIdPair> GetNotificationServices(); + } +} diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs new file mode 100644 index 000000000..b1e313b87 --- /dev/null +++ b/MediaBrowser.Controller/Notifications/INotificationService.cs @@ -0,0 +1,30 @@ +using MediaBrowser.Controller.Entities; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Notifications +{ + public interface INotificationService + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Sends the notification. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendNotification(UserNotification request, CancellationToken cancellationToken); + + /// <summary> + /// Determines whether [is enabled for user] [the specified user identifier]. + /// </summary> + /// <param name="user">The user.</param> + /// <returns><c>true</c> if [is enabled for user] [the specified user identifier]; otherwise, <c>false</c>.</returns> + bool IsEnabledForUser(User user); + } +} diff --git a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs new file mode 100644 index 000000000..bf92aae2d --- /dev/null +++ b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs @@ -0,0 +1,14 @@ +using MediaBrowser.Model.Notifications; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Notifications +{ + public interface INotificationTypeFactory + { + /// <summary> + /// Gets the notification types. + /// </summary> + /// <returns>IEnumerable{NotificationTypeInfo}.</returns> + IEnumerable<NotificationTypeInfo> GetNotificationTypes(); + } +} diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs new file mode 100644 index 000000000..d035a3995 --- /dev/null +++ b/MediaBrowser.Controller/Notifications/UserNotification.cs @@ -0,0 +1,21 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Notifications; +using System; + +namespace MediaBrowser.Controller.Notifications +{ + public class UserNotification + { + public string Name { get; set; } + + public string Description { get; set; } + + public string Url { get; set; } + + public NotificationLevel Level { get; set; } + + public DateTime Date { get; set; } + + public User User { get; set; } + } +} diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs new file mode 100644 index 000000000..25aba6bd9 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using MediaBrowser.Model.Entities; +using System; +using System.Threading; + +namespace MediaBrowser.Controller.Persistence +{ + /// <summary> + /// Interface IDisplayPreferencesRepository + /// </summary> + public interface IDisplayPreferencesRepository : IRepository + { + /// <summary> + /// Saves display preferences for an item + /// </summary> + /// <param name="displayPreferences">The display preferences.</param> + /// <param name="userId">The user id.</param> + /// <param name="client">The client.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, + CancellationToken cancellationToken); + + /// <summary> + /// Saves all display preferences for a user + /// </summary> + /// <param name="displayPreferences">The display preferences.</param> + /// <param name="userId">The user id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, + CancellationToken cancellationToken); + /// <summary> + /// Gets the display preferences. + /// </summary> + /// <param name="displayPreferencesId">The display preferences id.</param> + /// <param name="userId">The user id.</param> + /// <param name="client">The client.</param> + /// <returns>Task{DisplayPreferences}.</returns> + DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client); + + /// <summary> + /// Gets all display preferences for the given user. + /// </summary> + /// <param name="userId">The user id.</param> + /// <returns>Task{DisplayPreferences}.</returns> + IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId); + } +} diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs new file mode 100644 index 000000000..7905ea1aa --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -0,0 +1,159 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Persistence +{ + /// <summary> + /// Provides an interface to implement an Item repository + /// </summary> + public interface IItemRepository : IRepository + { + /// <summary> + /// Saves an item + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveItem(BaseItem item, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the item. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void DeleteItem(Guid id, CancellationToken cancellationToken); + + /// <summary> + /// Saves the items. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveItems(List<BaseItem> items, CancellationToken cancellationToken); + + void SaveImages(BaseItem item); + + /// <summary> + /// Retrieves the item. + /// </summary> + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + BaseItem RetrieveItem(Guid id); + + /// <summary> + /// Gets chapters for an item + /// </summary> + /// <param name="id"></param> + /// <returns></returns> + List<ChapterInfo> GetChapters(BaseItem id); + + /// <summary> + /// Gets a single chapter for an item + /// </summary> + /// <param name="id"></param> + /// <param name="index"></param> + /// <returns></returns> + ChapterInfo GetChapter(BaseItem id, int index); + + /// <summary> + /// Saves the chapters. + /// </summary> + void SaveChapters(Guid id, List<ChapterInfo> chapters); + + /// <summary> + /// Gets the media streams. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable{MediaStream}.</returns> + List<MediaStream> GetMediaStreams(MediaStreamQuery query); + + /// <summary> + /// Saves the media streams. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="streams">The streams.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken); + + /// <summary> + /// Gets the item ids. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable<Guid>.</returns> + QueryResult<Guid> GetItemIds(InternalItemsQuery query); + /// <summary> + /// Gets the items. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>QueryResult<BaseItem>.</returns> + QueryResult<BaseItem> GetItems(InternalItemsQuery query); + + /// <summary> + /// Gets the item ids list. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<Guid>.</returns> + List<Guid> GetItemIdsList(InternalItemsQuery query); + + /// <summary> + /// Gets the people. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<PersonInfo>.</returns> + List<PersonInfo> GetPeople(InternalPeopleQuery query); + + /// <summary> + /// Updates the people. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <param name="people">The people.</param> + void UpdatePeople(Guid itemId, List<PersonInfo> people); + + /// <summary> + /// Gets the people names. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<System.String>.</returns> + List<string> GetPeopleNames(InternalPeopleQuery query); + + /// <summary> + /// Gets the item ids with path. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>QueryResult<Tuple<Guid, System.String>>.</returns> + List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query); + + /// <summary> + /// Gets the item list. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<BaseItem>.</returns> + List<BaseItem> GetItemList(InternalItemsQuery query); + + /// <summary> + /// Updates the inherited values. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + void UpdateInheritedValues(CancellationToken cancellationToken); + + int GetCount(InternalItemsQuery query); + + QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query); + QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query); + + List<string> GetGameGenreNames(); + List<string> GetMusicGenreNames(); + List<string> GetStudioNames(); + List<string> GetGenreNames(); + List<string> GetAllArtistNames(); + } +} + diff --git a/MediaBrowser.Controller/Persistence/IRepository.cs b/MediaBrowser.Controller/Persistence/IRepository.cs new file mode 100644 index 000000000..2340ca646 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IRepository.cs @@ -0,0 +1,16 @@ +using System; + +namespace MediaBrowser.Controller.Persistence +{ + /// <summary> + /// Provides a base interface for all the repository interfaces + /// </summary> + public interface IRepository : IDisposable + { + /// <summary> + /// Gets the name of the repository + /// </summary> + /// <value>The name.</value> + string Name { get; } + } +} diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs new file mode 100644 index 000000000..5ab3f0943 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using System; +using System.Threading; + +namespace MediaBrowser.Controller.Persistence +{ + /// <summary> + /// Provides an interface to implement a UserData repository + /// </summary> + public interface IUserDataRepository : IRepository + { + /// <summary> + /// Saves the user data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="key">The key.</param> + /// <param name="userData">The user data.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken); + + /// <summary> + /// Gets the user data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="key">The key.</param> + /// <returns>Task{UserItemData}.</returns> + UserItemData GetUserData(long userId, string key); + + UserItemData GetUserData(long userId, List<string> keys); + + /// <summary> + /// Return all user data associated with the given user + /// </summary> + /// <param name="userId"></param> + /// <returns></returns> + List<UserItemData> GetAllUserData(long userId); + + /// <summary> + /// Save all user data associated with the given user + /// </summary> + /// <param name="userId"></param> + /// <param name="userData"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken); + + } +} diff --git a/MediaBrowser.Controller/Persistence/IUserRepository.cs b/MediaBrowser.Controller/Persistence/IUserRepository.cs new file mode 100644 index 000000000..2817c4255 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IUserRepository.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Controller.Entities; +using System.Collections.Generic; +using System.Threading; + +namespace MediaBrowser.Controller.Persistence +{ + /// <summary> + /// Provides an interface to implement a User repository + /// </summary> + public interface IUserRepository : IRepository + { + /// <summary> + /// Deletes the user. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void DeleteUser(User user); + + /// <summary> + /// Retrieves all users. + /// </summary> + /// <returns>IEnumerable{User}.</returns> + List<User> RetrieveAllUsers(); + + void CreateUser(User user); + void UpdateUser(User user); + } +} diff --git a/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs new file mode 100644 index 000000000..10985f57d --- /dev/null +++ b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs @@ -0,0 +1,26 @@ +using MediaBrowser.Model.Entities; +using System; + +namespace MediaBrowser.Controller.Persistence +{ + public class MediaStreamQuery + { + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public MediaStreamType? Type { get; set; } + + /// <summary> + /// Gets or sets the index. + /// </summary> + /// <value>The index.</value> + public int? Index { get; set; } + + /// <summary> + /// Gets or sets the item identifier. + /// </summary> + /// <value>The item identifier.</value> + public Guid ItemId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs new file mode 100644 index 000000000..5e790111d --- /dev/null +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -0,0 +1,59 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Playlists; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; + + +namespace MediaBrowser.Controller.Playlists +{ + public interface IPlaylistManager + { + /// <summary> + /// Gets the playlists. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <returns>IEnumerable<Playlist>.</returns> + IEnumerable<Playlist> GetPlaylists(Guid userId); + + /// <summary> + /// Creates the playlist. + /// </summary> + /// <param name="options">The options.</param> + /// <returns>Task<Playlist>.</returns> + Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options); + + /// <summary> + /// Adds to playlist. + /// </summary> + /// <param name="playlistId">The playlist identifier.</param> + /// <param name="itemIds">The item ids.</param> + /// <param name="userId">The user identifier.</param> + /// <returns>Task.</returns> + void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId); + + /// <summary> + /// Removes from playlist. + /// </summary> + /// <param name="playlistId">The playlist identifier.</param> + /// <param name="entryIds">The entry ids.</param> + /// <returns>Task.</returns> + void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds); + + /// <summary> + /// Gets the playlists folder. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <returns>Folder.</returns> + Folder GetPlaylistsFolder(Guid userId); + + /// <summary> + /// Moves the item. + /// </summary> + /// <param name="playlistId">The playlist identifier.</param> + /// <param name="entryId">The entry identifier.</param> + /// <param name="newIndex">The new index.</param> + /// <returns>Task.</returns> + void MoveItem(string playlistId, string entryId, int newIndex); + } +} diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs new file mode 100644 index 000000000..78614340a --- /dev/null +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -0,0 +1,315 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +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.Providers; +using System.Threading; + +namespace MediaBrowser.Controller.Playlists +{ + public class Playlist : Folder, IHasShares + { + public static string[] SupportedExtensions = new string[] { + + ".m3u", + ".m3u8", + ".pls", + ".wpl", + ".zpl" + }; + + public Guid OwnerUserId { get; set; } + + public Share[] Shares { get; set; } + + public Playlist() + { + Shares = new Share[] { }; + } + + [IgnoreDataMember] + public bool IsFile + { + get + { + return IsPlaylistFile(Path); + } + } + + public static bool IsPlaylistFile(string path) + { + return System.IO.Path.HasExtension(path); + } + + [IgnoreDataMember] + public override string ContainingFolderPath + { + get + { + var path = Path; + + if (IsPlaylistFile(path)) + { + return FileSystem.GetDirectoryName(path); + } + + return path; + } + } + + [IgnoreDataMember] + protected override bool FilterLinkedChildrenPerUser + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsInheritedParentImages + { + get + { + return false; + } + } + + [IgnoreDataMember] + public override bool SupportsPlayedStatus + { + get + { + return string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase); + } + } + + [IgnoreDataMember] + public override bool AlwaysScanInternalMetadataPath + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override bool SupportsCumulativeRunTimeTicks + { + get + { + return true; + } + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) + { + return true; + } + + public override bool IsSaveLocalMetadataEnabled() + { + return true; + } + + protected override List<BaseItem> LoadChildren() + { + // Save a trip to the database + return new List<BaseItem>(); + } + + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + return Task.FromResult(true); + } + + public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + { + return GetPlayableItems(user, query); + } + + protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + { + return new List<BaseItem>(); + } + + public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + { + return GetPlayableItems(user, query); + } + + public IEnumerable<Tuple<LinkedChild, BaseItem>> GetManageableItems() + { + return GetLinkedChildrenInfos(); + } + + private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) + { + if (query == null) + { + query = new InternalItemsQuery(user); + } + + query.IsFolder = false; + + return base.GetChildren(user, true, query); + } + + public static List<BaseItem> GetPlaylistItems(string playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options) + { + if (user != null) + { + inputItems = inputItems.Where(i => i.IsVisible(user)); + } + + var list = new List<BaseItem>(); + + foreach (var item in inputItems) + { + var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options); + list.AddRange(playlistItems); + } + + return list; + } + + private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options) + { + var musicGenre = item as MusicGenre; + if (musicGenre != null) + { + return LibraryManager.GetItemList(new InternalItemsQuery(user) + { + Recursive = true, + IncludeItemTypes = new[] { typeof(Audio).Name }, + GenreIds = new[] { musicGenre.Id }, + OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + DtoOptions = options + }); + } + + var musicArtist = item as MusicArtist; + if (musicArtist != null) + { + return LibraryManager.GetItemList(new InternalItemsQuery(user) + { + Recursive = true, + IncludeItemTypes = new[] { typeof(Audio).Name }, + ArtistIds = new[] { musicArtist.Id }, + OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + DtoOptions = options + }); + } + + var folder = item as Folder; + if (folder != null) + { + var query = new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false, + OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + MediaTypes = new[] { mediaType }, + EnableTotalRecordCount = false, + DtoOptions = options + }; + + return folder.GetItemList(query); + } + + return new[] { item }; + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + public string PlaylistMediaType { get; set; } + + [IgnoreDataMember] + public override string MediaType + { + get + { + return PlaylistMediaType; + } + } + + public void SetMediaType(string value) + { + PlaylistMediaType = value; + } + + [IgnoreDataMember] + private bool IsSharedItem + { + get + { + var path = Path; + + if (string.IsNullOrEmpty(path)) + { + return false; + } + + return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path); + } + } + + public override bool IsVisible(User user) + { + if (!IsSharedItem) + { + return base.IsVisible(user); + } + + if (user.Id == OwnerUserId) + { + return true; + } + + var shares = Shares; + if (shares.Length == 0) + { + return base.IsVisible(user); + } + + var userId = user.Id.ToString("N"); + foreach (var share in shares) + { + if (string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override bool IsVisibleStandalone(User user) + { + if (!IsSharedItem) + { + return base.IsVisibleStandalone(user); + } + + return IsVisible(user); + } + } +} diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs new file mode 100644 index 000000000..d294107d7 --- /dev/null +++ b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Reflection; + +namespace MediaBrowser.Controller.Plugins +{ + public interface ILocalizablePlugin + { + Stream GetDictionary(string culture); + } + + public static class LocalizablePluginHelper + { + public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture) + { + // Find all dictionaries using GetManifestResourceNames, start start with the prefix + // Return the one for the culture if exists, otherwise return the default + return null; + } + } +} diff --git a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs new file mode 100644 index 000000000..5feaf798c --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs @@ -0,0 +1,50 @@ +using MediaBrowser.Common.Plugins; +using System.IO; + +namespace MediaBrowser.Controller.Plugins +{ + /// <summary> + /// Interface IConfigurationPage + /// </summary> + public interface IPluginConfigurationPage + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the type of the configuration page. + /// </summary> + /// <value>The type of the configuration page.</value> + ConfigurationPageType ConfigurationPageType { get; } + + /// <summary> + /// Gets the plugin. + /// </summary> + /// <value>The plugin.</value> + IPlugin Plugin { get; } + + /// <summary> + /// Gets the HTML stream. + /// </summary> + /// <returns>Stream.</returns> + Stream GetHtmlStream(); + } + + /// <summary> + /// Enum ConfigurationPageType + /// </summary> + public enum ConfigurationPageType + { + /// <summary> + /// The plugin configuration + /// </summary> + PluginConfiguration, + /// <summary> + /// The none + /// </summary> + None + } +} diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs new file mode 100644 index 000000000..9ad829e45 --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs @@ -0,0 +1,20 @@ +using System; + +namespace MediaBrowser.Controller.Plugins +{ + /// <summary> + /// Interface IServerEntryPoint + /// </summary> + public interface IServerEntryPoint : IDisposable + { + /// <summary> + /// Runs this instance. + /// </summary> + void Run(); + } + + public interface IRunBeforeStartup + { + + } +} diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..844b93f37 --- /dev/null +++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MediaBrowser.Controller")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MediaBrowser.Controller")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +//
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs new file mode 100644 index 000000000..74feb4ea2 --- /dev/null +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class AlbumInfo : ItemLookupInfo + { + /// <summary> + /// Gets or sets the album artist. + /// </summary> + /// <value>The album artist.</value> + public string[] AlbumArtists { get; set; } + + /// <summary> + /// Gets or sets the artist provider ids. + /// </summary> + /// <value>The artist provider ids.</value> + public Dictionary<string, string> ArtistProviderIds { get; set; } + public List<SongInfo> SongInfos { get; set; } + + public AlbumInfo() + { + ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SongInfos = new List<SongInfo>(); + AlbumArtists = EmptyStringArray; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs new file mode 100644 index 000000000..8a4abd5c6 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class ArtistInfo : ItemLookupInfo + { + public List<SongInfo> SongInfos { get; set; } + + public ArtistInfo() + { + SongInfos = new List<SongInfo>(); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs new file mode 100644 index 000000000..52519bcb0 --- /dev/null +++ b/MediaBrowser.Controller/Providers/BookInfo.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public class BookInfo : ItemLookupInfo + { + public string SeriesName { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs new file mode 100644 index 000000000..f604231de --- /dev/null +++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public class BoxSetInfo : ItemLookupInfo + { + + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs new file mode 100644 index 000000000..65192671e --- /dev/null +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -0,0 +1,106 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Providers +{ + public class DirectoryService : IDirectoryService + { + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + + private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase); + + private readonly Dictionary<string, FileSystemMetadata> _fileCache = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); + + private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); + + public DirectoryService(ILogger logger, IFileSystem fileSystem) + { + _logger = logger; + _fileSystem = fileSystem; + } + + public DirectoryService(IFileSystem fileSystem) + : this(new NullLogger(), fileSystem) + { + } + + public FileSystemMetadata[] GetFileSystemEntries(string path) + { + FileSystemMetadata[] entries; + + if (!_cache.TryGetValue(path, out entries)) + { + //_logger.Debug("Getting files for " + path); + + entries = _fileSystem.GetFileSystemEntries(path).ToArray(); + + //_cache.TryAdd(path, entries); + _cache[path] = entries; + } + + return entries; + } + + public List<FileSystemMetadata> GetFiles(string path) + { + var list = new List<FileSystemMetadata>(); + var items = GetFileSystemEntries(path); + foreach (var item in items) + { + if (!item.IsDirectory) + { + list.Add(item); + } + } + return list; + } + + public FileSystemMetadata GetFile(string path) + { + FileSystemMetadata file; + if (!_fileCache.TryGetValue(path, out file)) + { + file = _fileSystem.GetFileInfo(path); + + if (file != null && file.Exists) + { + //_fileCache.TryAdd(path, file); + _fileCache[path] = file; + } + else + { + return null; + } + } + + return file; + //return _fileSystem.GetFileInfo(path); + } + + public List<string> GetFilePaths(string path) + { + return GetFilePaths(path, false); + } + + public List<string> GetFilePaths(string path, bool clearCache) + { + List<string> result; + if (clearCache || !_filePathCache.TryGetValue(path, out result)) + { + result = _fileSystem.GetFilePaths(path).ToList(); + + _filePathCache[path] = result; + } + + return result; + } + + } +} diff --git a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs new file mode 100644 index 000000000..14b4c6afb --- /dev/null +++ b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs @@ -0,0 +1,10 @@ +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public class DynamicImageInfo + { + public string ImageId { get; set; } + public ImageType Type { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs new file mode 100644 index 000000000..d19a28a24 --- /dev/null +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.Controller.Providers +{ + public class DynamicImageResponse + { + public string Path { get; set; } + public MediaProtocol Protocol { get; set; } + public Stream Stream { get; set; } + public ImageFormat Format { get; set; } + public bool HasImage { get; set; } + + public void SetFormatFromMimeType(string mimeType) + { + if (mimeType.EndsWith("gif", StringComparison.OrdinalIgnoreCase)) + { + Format = ImageFormat.Gif; + } + else if (mimeType.EndsWith("bmp", StringComparison.OrdinalIgnoreCase)) + { + Format = ImageFormat.Bmp; + } + else if (mimeType.EndsWith("png", StringComparison.OrdinalIgnoreCase)) + { + Format = ImageFormat.Png; + } + else + { + Format = ImageFormat.Jpg; + } + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs new file mode 100644 index 000000000..4eafe0e0e --- /dev/null +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class EpisodeInfo : ItemLookupInfo + { + public Dictionary<string, string> SeriesProviderIds { get; set; } + + public int? IndexNumberEnd { get; set; } + + public bool IsMissingEpisode { get; set; } + public string SeriesDisplayOrder { get; set; } + + public EpisodeInfo() + { + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ExtraInfo.cs b/MediaBrowser.Controller/Providers/ExtraInfo.cs new file mode 100644 index 000000000..1fbe6e93a --- /dev/null +++ b/MediaBrowser.Controller/Providers/ExtraInfo.cs @@ -0,0 +1,15 @@ +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public class ExtraInfo + { + public string Path { get; set; } + + public LocationType LocationType { get; set; } + + public bool IsDownloadable { get; set; } + + public ExtraType ExtraType { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ExtraSource.cs b/MediaBrowser.Controller/Providers/ExtraSource.cs new file mode 100644 index 000000000..901af60f8 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ExtraSource.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Providers +{ + public enum ExtraSource + { + Local = 1, + Metadata = 2, + Remote = 3 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/GameInfo.cs b/MediaBrowser.Controller/Providers/GameInfo.cs new file mode 100644 index 000000000..771cf6cec --- /dev/null +++ b/MediaBrowser.Controller/Providers/GameInfo.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Controller.Providers +{ + public class GameInfo : ItemLookupInfo + { + /// <summary> + /// Gets or sets the game system. + /// </summary> + /// <value>The game system.</value> + public string GameSystem { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/GameSystemInfo.cs b/MediaBrowser.Controller/Providers/GameSystemInfo.cs new file mode 100644 index 000000000..efe2635cd --- /dev/null +++ b/MediaBrowser.Controller/Providers/GameSystemInfo.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Controller.Providers +{ + public class GameSystemInfo : ItemLookupInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs new file mode 100644 index 000000000..af1838d74 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface ICustomMetadataProvider : IMetadataProvider + { + } + + public interface ICustomMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ICustomMetadataProvider + where TItemType : BaseItem + { + /// <summary> + /// Fetches the asynchronous. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{ItemUpdateType}.</returns> + Task<ItemUpdateType> FetchAsync(TItemType item, MetadataRefreshOptions options, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs new file mode 100644 index 000000000..0b4574f6e --- /dev/null +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Providers +{ + public interface IDirectoryService + { + FileSystemMetadata[] GetFileSystemEntries(string path); + List<FileSystemMetadata> GetFiles(string path); + FileSystemMetadata GetFile(string path); + + List<string> GetFilePaths(string path); + List<string> GetFilePaths(string path, bool clearCache); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs new file mode 100644 index 000000000..3e9127fc4 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public interface IDynamicImageProvider : IImageProvider + { + /// <summary> + /// Gets the supported images. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable{ImageType}.</returns> + IEnumerable<ImageType> GetSupportedImages(BaseItem item); + + /// <summary> + /// Gets the image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{DynamicImageResponse}.</returns> + Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs new file mode 100644 index 000000000..946f28199 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -0,0 +1,15 @@ +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public interface IExternalId + { + string Name { get; } + + string Key { get; } + + string UrlFormatString { get; } + + bool Supports(IHasProviderIds item); + } +} diff --git a/MediaBrowser.Controller/Providers/IExtrasProvider.cs b/MediaBrowser.Controller/Providers/IExtrasProvider.cs new file mode 100644 index 000000000..58775ccac --- /dev/null +++ b/MediaBrowser.Controller/Providers/IExtrasProvider.cs @@ -0,0 +1,20 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public interface IExtrasProvider + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Supportses the specified item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool Supports(BaseItem item); + } +} diff --git a/MediaBrowser.Controller/Providers/IForcedProvider.cs b/MediaBrowser.Controller/Providers/IForcedProvider.cs new file mode 100644 index 000000000..9e35b00ad --- /dev/null +++ b/MediaBrowser.Controller/Providers/IForcedProvider.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// This is a marker interface that will cause a provider to run even if IsLocked=true + /// </summary> + public interface IForcedProvider + { + } +} diff --git a/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs new file mode 100644 index 000000000..7cc05bf67 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs @@ -0,0 +1,15 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public interface IHasItemChangeMonitor + { + /// <summary> + /// Determines whether the specified item has changed. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns><c>true</c> if the specified item has changed; otherwise, <c>false</c>.</returns> + bool HasChanged(BaseItem item, IDirectoryService directoryService); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IHasLookupInfo.cs b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs new file mode 100644 index 000000000..afce49852 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Controller.Providers +{ + public interface IHasLookupInfo<out TLookupInfoType> + where TLookupInfoType : ItemLookupInfo, new() + { + TLookupInfoType GetLookupInfo(); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs new file mode 100644 index 000000000..cb5298dd3 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IHasOrder.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public interface IHasOrder + { + int Order { get; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs new file mode 100644 index 000000000..c8ea25335 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IImageEnhancer.cs @@ -0,0 +1,61 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Entities; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface IImageEnhancer + { + /// <summary> + /// Return true only if the given image for the given item will be enhanced by this enhancer. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns> + bool Supports(BaseItem item, ImageType imageType); + + /// <summary> + /// Gets the priority or order in which this enhancer should be run. + /// </summary> + /// <value>The priority.</value> + MetadataProviderPriority Priority { get; } + + /// <summary> + /// Return a key incorporating all configuration information related to this item + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <returns>Cache key relating to the current state of this item and configuration</returns> + string GetConfigurationCacheKey(BaseItem item, ImageType imageType); + + /// <summary> + /// Gets the size of the enhanced image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="originalImageSize">Size of the original image.</param> + /// <returns>ImageSize.</returns> + ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize); + + EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex); + + /// <summary> + /// Enhances the image async. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="inputFile">The input file.</param> + /// <param name="outputFile">The output file.</param> + /// <param name="imageType">Type of the image.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>Task{Image}.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex); + } + + public class EnhancedImageInfo + { + public bool RequiresTransparency { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs new file mode 100644 index 000000000..ac857b2aa --- /dev/null +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Interface IImageProvider + /// </summary> + public interface IImageProvider + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Supportses the specified item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + bool Supports(BaseItem item); + } +} diff --git a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs new file mode 100644 index 000000000..96e154dad --- /dev/null +++ b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public interface ILocalImageFileProvider : ILocalImageProvider + { + List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs new file mode 100644 index 000000000..1027a4cb2 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// This is just a marker interface + /// </summary> + public interface ILocalImageProvider : IImageProvider + { + } +} diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs new file mode 100644 index 000000000..fc4cca19c --- /dev/null +++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface ILocalMetadataProvider : IMetadataProvider + { + } + + public interface ILocalMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ILocalMetadataProvider + where TItemType : BaseItem + { + /// <summary> + /// Gets the metadata. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{MetadataResult{`0}}.</returns> + Task<MetadataResult<TItemType>> GetMetadata(ItemInfo info, + IDirectoryService directoryService, + CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs new file mode 100644 index 000000000..7da590193 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs @@ -0,0 +1,21 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Marker interface + /// </summary> + public interface IMetadataProvider + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + } + + public interface IMetadataProvider<TItemType> : IMetadataProvider + where TItemType : BaseItem + { + } +} diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs new file mode 100644 index 000000000..1c9c4b71a --- /dev/null +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -0,0 +1,34 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface IMetadataService + { + /// <summary> + /// Determines whether this instance can refresh the specified item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns> + bool CanRefresh(BaseItem item); + bool CanRefreshPrimary(Type type); + + /// <summary> + /// Refreshes the metadata. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="refreshOptions">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken); + + /// <summary> + /// Gets the order. + /// </summary> + /// <value>The order.</value> + int Order { get; } + } +} diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs new file mode 100644 index 000000000..608674905 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public interface IPreRefreshProvider : ICustomMetadataProvider + { + + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs new file mode 100644 index 000000000..f1930ee2f --- /dev/null +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -0,0 +1,178 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Events; + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Interface IProviderManager + /// </summary> + public interface IProviderManager + { + /// <summary> + /// Queues the refresh. + /// </summary> + void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority); + + /// <summary> + /// Refreshes the full item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken); + + /// <summary> + /// Refreshes the metadata. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="options">The options.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken); + + /// <summary> + /// Saves the image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="url">The URL.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken); + + /// <summary> + /// Saves the image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="source">The source.</param> + /// <param name="mimeType">Type of the MIME.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken); + + /// <summary> + /// Saves the image. + /// </summary> + /// <returns>Task.</returns> + Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken); + + /// <summary> + /// Adds the metadata providers. + /// </summary> + void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders, + IEnumerable<IMetadataSaver> savers, + IEnumerable<IExternalId> externalIds); + + /// <summary> + /// Gets the available remote images. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> + Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the image providers. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable{ImageProviderInfo}.</returns> + IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item); + + /// <summary> + /// Gets all metadata plugins. + /// </summary> + /// <returns>IEnumerable{MetadataPlugin}.</returns> + MetadataPluginSummary[] GetAllMetadataPlugins(); + + /// <summary> + /// Gets the external urls. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable{ExternalUrl}.</returns> + IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item); + + /// <summary> + /// Gets the external identifier infos. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable{ExternalIdInfo}.</returns> + IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item); + + /// <summary> + /// Saves the metadata. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="updateType">Type of the update.</param> + /// <returns>Task.</returns> + void SaveMetadata(BaseItem item, ItemUpdateType updateType); + + /// <summary> + /// Saves the metadata. + /// </summary> + void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers); + + /// <summary> + /// Gets the metadata options. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>MetadataOptions.</returns> + MetadataOptions GetMetadataOptions(BaseItem item); + + /// <summary> + /// Gets the remote search results. + /// </summary> + /// <typeparam name="TItemType">The type of the t item type.</typeparam> + /// <typeparam name="TLookupType">The type of the t lookup type.</typeparam> + /// <param name="searchInfo">The search information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{SearchResult{``1}}}.</returns> + Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>( + RemoteSearchQuery<TLookupType> searchInfo, + CancellationToken cancellationToken) + where TItemType : BaseItem, new() + where TLookupType : ItemLookupInfo; + + /// <summary> + /// Gets the search image. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="url">The URL.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{HttpResponseInfo}.</returns> + Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken); + + Dictionary<Guid, Guid> GetRefreshQueue(); + + void OnRefreshStart(BaseItem item); + void OnRefreshProgress(BaseItem item, double progress); + void OnRefreshComplete(BaseItem item); + + double? GetRefreshProgress(Guid id); + + event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; + event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; + event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; + } + + public enum RefreshPriority + { + High = 0, + Normal = 1, + Low = 2 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs new file mode 100644 index 000000000..5db5ddbb2 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -0,0 +1,39 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Interface IImageProvider + /// </summary> + public interface IRemoteImageProvider : IImageProvider + { + /// <summary> + /// Gets the supported images. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable{ImageType}.</returns> + IEnumerable<ImageType> GetSupportedImages(BaseItem item); + + /// <summary> + /// Gets the images. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> + Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken); + + /// <summary> + /// Gets the image response. + /// </summary> + /// <param name="url">The URL.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{HttpResponseInfo}.</returns> + Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs new file mode 100644 index 000000000..695d488ed --- /dev/null +++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Providers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface IRemoteMetadataProvider : IMetadataProvider + { + } + + public interface IRemoteMetadataProvider<TItemType, in TLookupInfoType> : IMetadataProvider<TItemType>, IRemoteMetadataProvider, IRemoteSearchProvider<TLookupInfoType> + where TItemType : BaseItem, IHasLookupInfo<TLookupInfoType> + where TLookupInfoType : ItemLookupInfo, new() + { + Task<MetadataResult<TItemType>> GetMetadata(TLookupInfoType info, CancellationToken cancellationToken); + } + + public interface IRemoteSearchProvider<in TLookupInfoType> : IRemoteSearchProvider + where TLookupInfoType : ItemLookupInfo + { + Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TLookupInfoType searchInfo, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs new file mode 100644 index 000000000..0077def42 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; + +namespace MediaBrowser.Controller.Providers +{ + public interface IRemoteSearchProvider : IMetadataProvider + { + /// <summary> + /// Gets the image response. + /// </summary> + /// <param name="url">The URL.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{HttpResponseInfo}.</returns> + Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs new file mode 100644 index 000000000..942d25071 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using MediaBrowser.Model.Entities; +using System; +using System.Linq; + +namespace MediaBrowser.Controller.Providers +{ + public class ImageRefreshOptions + { + public MetadataRefreshMode ImageRefreshMode { get; set; } + public IDirectoryService DirectoryService { get; private set; } + + public bool ReplaceAllImages { get; set; } + + public ImageType[] ReplaceImages { get; set; } + public bool IsAutomated { get; set; } + + public ImageRefreshOptions(IDirectoryService directoryService) + { + ImageRefreshMode = MetadataRefreshMode.Default; + DirectoryService = directoryService; + + ReplaceImages = new ImageType[] { }; + IsAutomated = true; + } + + public bool IsReplacingImage(ImageType type) + { + return ImageRefreshMode == MetadataRefreshMode.FullRefresh && + (ReplaceAllImages || ReplaceImages.Contains(type)); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs new file mode 100644 index 000000000..76adfe8a2 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -0,0 +1,32 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Providers +{ + public class ItemInfo + { + public ItemInfo(BaseItem item) + { + Path = item.Path; + ContainingFolderPath = item.ContainingFolderPath; + IsInMixedFolder = item.IsInMixedFolder; + + var video = item as Video; + if (video != null) + { + VideoType = video.VideoType; + IsPlaceHolder = video.IsPlaceHolder; + } + + ItemType = item.GetType(); + } + + public Type ItemType { get; set; } + public string Path { get; set; } + public string ContainingFolderPath { get; set; } + public VideoType VideoType { get; set; } + public bool IsInMixedFolder { get; set; } + public bool IsPlaceHolder { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs new file mode 100644 index 000000000..98122e776 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -0,0 +1,47 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class ItemLookupInfo : IHasProviderIds + { + protected static string[] EmptyStringArray = new string[] { }; + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets the metadata language. + /// </summary> + /// <value>The metadata language.</value> + public string MetadataLanguage { get; set; } + /// <summary> + /// Gets or sets the metadata country code. + /// </summary> + /// <value>The metadata country code.</value> + public string MetadataCountryCode { get; set; } + /// <summary> + /// Gets or sets the provider ids. + /// </summary> + /// <value>The provider ids.</value> + public Dictionary<string, string> ProviderIds { get; set; } + /// <summary> + /// Gets or sets the year. + /// </summary> + /// <value>The year.</value> + public int? Year { get; set; } + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + public DateTime? PremiereDate { get; set; } + public bool IsAutomated { get; set; } + + public ItemLookupInfo() + { + IsAutomated = true; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs new file mode 100644 index 000000000..5e6efe9f6 --- /dev/null +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -0,0 +1,13 @@ + +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Providers +{ + public class LocalImageInfo + { + public FileSystemMetadata FileInfo { get; set; } + public ImageType Type { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs new file mode 100644 index 000000000..d01261866 --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs @@ -0,0 +1,40 @@ + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Determines when a provider should execute, relative to others + /// </summary> + public enum MetadataProviderPriority + { + // Run this provider at the beginning + /// <summary> + /// The first + /// </summary> + First = 1, + + // Run this provider after all first priority providers + /// <summary> + /// The second + /// </summary> + Second = 2, + + // Run this provider after all second priority providers + /// <summary> + /// The third + /// </summary> + Third = 3, + + /// <summary> + /// The fourth + /// </summary> + Fourth = 4, + + Fifth = 5, + + // Run this provider last + /// <summary> + /// The last + /// </summary> + Last = 999 + } +} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs new file mode 100644 index 000000000..56492006a --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Controller.Providers +{ + public enum MetadataRefreshMode + { + /// <summary> + /// The none + /// </summary> + None = 0, + + /// <summary> + /// The validation only + /// </summary> + ValidationOnly = 1, + + /// <summary> + /// Providers will be executed based on default rules + /// </summary> + Default = 2, + + /// <summary> + /// All providers will be executed to search for new metadata + /// </summary> + FullRefresh = 3 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs new file mode 100644 index 000000000..3e34075a6 --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Controller.Providers +{ + public class MetadataRefreshOptions : ImageRefreshOptions + { + /// <summary> + /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers. + /// </summary> + public bool ReplaceAllMetadata { get; set; } + + public MetadataRefreshMode MetadataRefreshMode { get; set; } + public RemoteSearchResult SearchResult { get; set; } + + public string[] RefreshPaths { get; set; } + + public bool ForceSave { get; set; } + public bool EnableRemoteContentProbe { get; set; } + + public MetadataRefreshOptions(IFileSystem fileSystem) + : this(new DirectoryService(new NullLogger(), fileSystem)) + { + } + + public MetadataRefreshOptions(IDirectoryService directoryService) + : base(directoryService) + { + MetadataRefreshMode = MetadataRefreshMode.Default; + } + + public MetadataRefreshOptions(MetadataRefreshOptions copy) + : base(copy.DirectoryService) + { + MetadataRefreshMode = copy.MetadataRefreshMode; + ForceSave = copy.ForceSave; + ReplaceAllMetadata = copy.ReplaceAllMetadata; + EnableRemoteContentProbe = copy.EnableRemoteContentProbe; + + ImageRefreshMode = copy.ImageRefreshMode; + ReplaceAllImages = copy.ReplaceAllImages; + ReplaceImages = copy.ReplaceImages; + SearchResult = copy.SearchResult; + + if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0) + { + if (RefreshPaths == null) + { + RefreshPaths = new string[] { }; + } + + RefreshPaths = copy.RefreshPaths.ToArray(); + } + } + + public bool RefreshItem(BaseItem item) + { + if (RefreshPaths != null && RefreshPaths.Length > 0) + { + return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + return true; + } + } +} diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs new file mode 100644 index 000000000..f35d41ca4 --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -0,0 +1,77 @@ +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class MetadataResult<T> + { + public List<LocalImageInfo> Images { get; set; } + public List<UserItemData> UserDataList { get; set; } + + public MetadataResult() + { + Images = new List<LocalImageInfo>(); + ResultLanguage = "en"; + } + + public List<PersonInfo> People { get; set; } + + public bool HasMetadata { get; set; } + public T Item { get; set; } + public string ResultLanguage { get; set; } + public string Provider { get; set; } + public bool QueriedById { get; set; } + public void AddPerson(PersonInfo p) + { + if (People == null) + { + People = new List<PersonInfo>(); + } + + PeopleHelper.AddPerson(People, p); + } + + /// <summary> + /// Not only does this clear, but initializes the list so that services can differentiate between a null list and zero people + /// </summary> + public void ResetPeople() + { + if (People == null) + { + People = new List<PersonInfo>(); + } + People.Clear(); + } + + public UserItemData GetOrAddUserData(string userId) + { + if (UserDataList == null) + { + UserDataList = new List<UserItemData>(); + } + + UserItemData userData = null; + + foreach (var i in UserDataList) + { + if (string.Equals(userId, i.UserId.ToString("N"), StringComparison.OrdinalIgnoreCase)) + { + userData = i; + } + } + + if (userData == null) + { + userData = new UserItemData() + { + UserId = new Guid(userId) + }; + + UserDataList.Add(userData); + } + + return userData; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs new file mode 100644 index 000000000..198336fc0 --- /dev/null +++ b/MediaBrowser.Controller/Providers/MovieInfo.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public class MovieInfo : ItemLookupInfo + { + + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs new file mode 100644 index 000000000..6e12405f7 --- /dev/null +++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public class MusicVideoInfo : ItemLookupInfo + { + public string[] Artists { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs new file mode 100644 index 000000000..db4dacb0b --- /dev/null +++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs @@ -0,0 +1,7 @@ +namespace MediaBrowser.Controller.Providers +{ + public class PersonLookupInfo : ItemLookupInfo + { + + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs new file mode 100644 index 000000000..77cf9e255 --- /dev/null +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Controller.Providers +{ + public class RemoteSearchQuery<T> + where T : ItemLookupInfo + { + public T SearchInfo { get; set; } + + public string ItemId { get; set; } + + /// <summary> + /// If set will only search within the given provider + /// </summary> + public string SearchProviderName { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether [include disabled providers]. + /// </summary> + /// <value><c>true</c> if [include disabled providers]; otherwise, <c>false</c>.</value> + public bool IncludeDisabledProviders { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs new file mode 100644 index 000000000..31af268b8 --- /dev/null +++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class SeasonInfo : ItemLookupInfo + { + public Dictionary<string, string> SeriesProviderIds { get; set; } + + public SeasonInfo() + { + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/SeriesInfo.cs b/MediaBrowser.Controller/Providers/SeriesInfo.cs new file mode 100644 index 000000000..0b1361757 --- /dev/null +++ b/MediaBrowser.Controller/Providers/SeriesInfo.cs @@ -0,0 +1,6 @@ +namespace MediaBrowser.Controller.Providers +{ + public class SeriesInfo : ItemLookupInfo + { + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs new file mode 100644 index 000000000..e3a6f5d37 --- /dev/null +++ b/MediaBrowser.Controller/Providers/SongInfo.cs @@ -0,0 +1,16 @@ + +namespace MediaBrowser.Controller.Providers +{ + public class SongInfo : ItemLookupInfo + { + public string[] AlbumArtists { get; set; } + public string Album { get; set; } + public string[] Artists { get; set; } + + public SongInfo() + { + Artists = EmptyStringArray; + AlbumArtists = EmptyStringArray; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/TrailerInfo.cs b/MediaBrowser.Controller/Providers/TrailerInfo.cs new file mode 100644 index 000000000..ea8377adf --- /dev/null +++ b/MediaBrowser.Controller/Providers/TrailerInfo.cs @@ -0,0 +1,6 @@ +namespace MediaBrowser.Controller.Providers +{ + public class TrailerInfo : ItemLookupInfo + { + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/VideoContentType.cs b/MediaBrowser.Controller/Providers/VideoContentType.cs new file mode 100644 index 000000000..903c77612 --- /dev/null +++ b/MediaBrowser.Controller/Providers/VideoContentType.cs @@ -0,0 +1,19 @@ + +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Enum VideoContentType + /// </summary> + public enum VideoContentType + { + /// <summary> + /// The episode + /// </summary> + Episode = 0, + + /// <summary> + /// The movie + /// </summary> + Movie = 1 + } +} diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs new file mode 100644 index 000000000..fc5157d5f --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs @@ -0,0 +1,61 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.Resolvers +{ + /// <summary> + /// Class ItemResolver + /// </summary> + /// <typeparam name="T"></typeparam> + public abstract class ItemResolver<T> : IItemResolver + where T : BaseItem, new() + { + /// <summary> + /// Resolves the specified args. + /// </summary> + /// <param name="args">The args.</param> + /// <returns>`0.</returns> + protected virtual T Resolve(ItemResolveArgs args) + { + return null; + } + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public virtual ResolverPriority Priority + { + get + { + return ResolverPriority.First; + } + } + + /// <summary> + /// Sets initial values on the newly resolved item + /// </summary> + /// <param name="item">The item.</param> + /// <param name="args">The args.</param> + protected virtual void SetInitialItemValues(T item, ItemResolveArgs args) + { + } + + /// <summary> + /// Resolves the path. + /// </summary> + /// <param name="args">The args.</param> + /// <returns>BaseItem.</returns> + BaseItem IItemResolver.ResolvePath(ItemResolveArgs args) + { + var item = Resolve(args); + + if (item != null) + { + SetInitialItemValues(item, args); + } + + return item; + } + } +} diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs new file mode 100644 index 000000000..3af5d5f7f --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -0,0 +1,48 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using System.Collections.Generic; + +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Resolvers +{ + /// <summary> + /// Interface IItemResolver + /// </summary> + public interface IItemResolver + { + /// <summary> + /// Resolves the path. + /// </summary> + /// <param name="args">The args.</param> + /// <returns>BaseItem.</returns> + BaseItem ResolvePath(ItemResolveArgs args); + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + ResolverPriority Priority { get; } + } + + public interface IMultiItemResolver + { + MultiItemResolverResult ResolveMultiple(Folder parent, + List<FileSystemMetadata> files, + string collectionType, + IDirectoryService directoryService); + } + + public class MultiItemResolverResult + { + public List<BaseItem> Items { get; set; } + public List<FileSystemMetadata> ExtraFiles { get; set; } + + public MultiItemResolverResult() + { + Items = new List<BaseItem>(); + ExtraFiles = new List<FileSystemMetadata>(); + } + } +} diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs new file mode 100644 index 000000000..25537193a --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs @@ -0,0 +1,15 @@ + +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Resolvers +{ + /// <summary> + /// Provides a base "rule" that anyone can use to have paths ignored by the resolver + /// </summary> + public interface IResolverIgnoreRule + { + bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent); + } +} diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs new file mode 100644 index 000000000..df5edeb05 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs @@ -0,0 +1,27 @@ + +namespace MediaBrowser.Controller.Resolvers +{ + /// <summary> + /// Enum ResolverPriority + /// </summary> + public enum ResolverPriority + { + /// <summary> + /// The first + /// </summary> + First = 1, + /// <summary> + /// The second + /// </summary> + Second = 2, + /// <summary> + /// The third + /// </summary> + Third = 3, + Fourth = 4, + /// <summary> + /// The last + /// </summary> + Last = 5 + } +} diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs new file mode 100644 index 000000000..c75bf89e4 --- /dev/null +++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs @@ -0,0 +1,70 @@ +using System; + +namespace MediaBrowser.Controller.Security +{ + public class AuthenticationInfo + { + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public long Id { get; set; } + + /// <summary> + /// Gets or sets the access token. + /// </summary> + /// <value>The access token.</value> + public string AccessToken { get; set; } + + /// <summary> + /// Gets or sets the device identifier. + /// </summary> + /// <value>The device identifier.</value> + public string DeviceId { get; set; } + + /// <summary> + /// Gets or sets the name of the application. + /// </summary> + /// <value>The name of the application.</value> + public string AppName { get; set; } + + /// <summary> + /// Gets or sets the application version. + /// </summary> + /// <value>The application version.</value> + public string AppVersion { get; set; } + + /// <summary> + /// Gets or sets the name of the device. + /// </summary> + /// <value>The name of the device.</value> + public string DeviceName { get; set; } + + /// <summary> + /// Gets or sets the user identifier. + /// </summary> + /// <value>The user identifier.</value> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is active. + /// </summary> + /// <value><c>true</c> if this instance is active; otherwise, <c>false</c>.</value> + public bool IsActive { get; set; } + + /// <summary> + /// Gets or sets the date created. + /// </summary> + /// <value>The date created.</value> + public DateTime DateCreated { get; set; } + + /// <summary> + /// Gets or sets the date revoked. + /// </summary> + /// <value>The date revoked.</value> + public DateTime? DateRevoked { get; set; } + + public DateTime DateLastActivity { get; set; } + public string UserName { get; set;} + } +} diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs new file mode 100644 index 000000000..125534c46 --- /dev/null +++ b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs @@ -0,0 +1,49 @@ +using System; + +namespace MediaBrowser.Controller.Security +{ + public class AuthenticationInfoQuery + { + /// <summary> + /// Gets or sets the device identifier. + /// </summary> + /// <value>The device identifier.</value> + public string DeviceId { get; set; } + + /// <summary> + /// Gets or sets the user identifier. + /// </summary> + /// <value>The user identifier.</value> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the access token. + /// </summary> + /// <value>The access token.</value> + public string AccessToken { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is active. + /// </summary> + /// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value> + public bool? IsActive { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has user. + /// </summary> + /// <value><c>null</c> if [has user] contains no value, <c>true</c> if [has user]; otherwise, <c>false</c>.</value> + public bool? HasUser { get; set; } + + /// <summary> + /// Gets or sets the start index. + /// </summary> + /// <value>The start index.</value> + public int? StartIndex { get; set; } + + /// <summary> + /// Gets or sets the limit. + /// </summary> + /// <value>The limit.</value> + public int? Limit { get; set; } + } +} diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs new file mode 100644 index 000000000..2843c6b73 --- /dev/null +++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs @@ -0,0 +1,37 @@ +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Querying; +using System.Threading; + +namespace MediaBrowser.Controller.Security +{ + public interface IAuthenticationRepository + { + /// <summary> + /// Creates the specified information. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void Create(AuthenticationInfo info); + + /// <summary> + /// Updates the specified information. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + void Update(AuthenticationInfo info); + + /// <summary> + /// Gets the specified query. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>QueryResult{AuthenticationInfo}.</returns> + QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query); + + void Delete(AuthenticationInfo info); + + DeviceOptions GetDeviceOptions(string deviceId); + void UpdateDeviceOptions(string deviceId, DeviceOptions options); + } +} diff --git a/MediaBrowser.Controller/Security/IEncryptionManager.cs b/MediaBrowser.Controller/Security/IEncryptionManager.cs new file mode 100644 index 000000000..bb4f77d83 --- /dev/null +++ b/MediaBrowser.Controller/Security/IEncryptionManager.cs @@ -0,0 +1,20 @@ + +namespace MediaBrowser.Controller.Security +{ + public interface IEncryptionManager + { + /// <summary> + /// Encrypts the string. + /// </summary> + /// <param name="value">The value.</param> + /// <returns>System.String.</returns> + string EncryptString(string value); + + /// <summary> + /// Decrypts the string. + /// </summary> + /// <param name="value">The value.</param> + /// <returns>System.String.</returns> + string DecryptString(string value); + } +} diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs new file mode 100644 index 000000000..eb64db8c3 --- /dev/null +++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs @@ -0,0 +1,19 @@ +using System; + + +namespace MediaBrowser.Controller.Session +{ + public class AuthenticationRequest + { + public string Username { get; set; } + public Guid UserId { get; set; } + public string Password { get; set; } + public string PasswordSha1 { get; set; } + public string PasswordMd5 { get; set; } + public string App { get; set; } + public string AppVersion { get; set; } + public string DeviceId { get; set; } + public string DeviceName { get; set; } + public string RemoteEndPoint { get; set; } + } +} diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs new file mode 100644 index 000000000..e1d3a7ee6 --- /dev/null +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Session +{ + public interface ISessionController + { + /// <summary> + /// Gets a value indicating whether this instance is session active. + /// </summary> + /// <value><c>true</c> if this instance is session active; otherwise, <c>false</c>.</value> + bool IsSessionActive { get; } + + /// <summary> + /// Gets a value indicating whether [supports media remote control]. + /// </summary> + /// <value><c>true</c> if [supports media remote control]; otherwise, <c>false</c>.</value> + bool SupportsMediaControl { get; } + + /// <summary> + /// Sends the message. + /// </summary> + Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs new file mode 100644 index 000000000..b7719e556 --- /dev/null +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -0,0 +1,328 @@ +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Session +{ + /// <summary> + /// Interface ISessionManager + /// </summary> + public interface ISessionManager + { + /// <summary> + /// Occurs when [playback start]. + /// </summary> + event EventHandler<PlaybackProgressEventArgs> PlaybackStart; + + /// <summary> + /// Occurs when [playback progress]. + /// </summary> + event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; + + /// <summary> + /// Occurs when [playback stopped]. + /// </summary> + event EventHandler<PlaybackStopEventArgs> PlaybackStopped; + + /// <summary> + /// Occurs when [session started]. + /// </summary> + event EventHandler<SessionEventArgs> SessionStarted; + + /// <summary> + /// Occurs when [session ended]. + /// </summary> + event EventHandler<SessionEventArgs> SessionEnded; + + event EventHandler<SessionEventArgs> SessionActivity; + + /// <summary> + /// Occurs when [capabilities changed]. + /// </summary> + event EventHandler<SessionEventArgs> CapabilitiesChanged; + + /// <summary> + /// Occurs when [authentication failed]. + /// </summary> + event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; + + /// <summary> + /// Occurs when [authentication succeeded]. + /// </summary> + event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded; + + /// <summary> + /// Gets the sessions. + /// </summary> + /// <value>The sessions.</value> + IEnumerable<SessionInfo> Sessions { get; } + + /// <summary> + /// Logs the user activity. + /// </summary> + /// <param name="appName">Type of the client.</param> + /// <param name="appVersion">The app version.</param> + /// <param name="deviceId">The device id.</param> + /// <param name="deviceName">Name of the device.</param> + /// <param name="remoteEndPoint">The remote end point.</param> + /// <param name="user">The user.</param> + SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user); + + void UpdateDeviceName(string sessionId, string reportedDeviceName); + + /// <summary> + /// Used to report that playback has started for an item + /// </summary> + /// <param name="info">The info.</param> + /// <returns>Task.</returns> + Task OnPlaybackStart(PlaybackStartInfo info); + + /// <summary> + /// Used to report playback progress for an item + /// </summary> + /// <param name="info">The info.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + Task OnPlaybackProgress(PlaybackProgressInfo info); + + Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated); + + /// <summary> + /// Used to report that playback has ended for an item + /// </summary> + /// <param name="info">The info.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + Task OnPlaybackStopped(PlaybackStopInfo info); + + /// <summary> + /// Reports the session ended. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <returns>Task.</returns> + void ReportSessionEnded(string sessionId); + + /// <summary> + /// Sends the general command. + /// </summary> + /// <param name="controllingSessionId">The controlling session identifier.</param> + /// <param name="sessionId">The session identifier.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken); + + /// <summary> + /// Sends the message command. + /// </summary> + /// <param name="controllingSessionId">The controlling session identifier.</param> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken); + + /// <summary> + /// Sends the play command. + /// </summary> + /// <param name="controllingSessionId">The controlling session identifier.</param> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); + + /// <summary> + /// Sends the browse command. + /// </summary> + /// <param name="controllingSessionId">The controlling session identifier.</param> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken); + + /// <summary> + /// Sends the playstate command. + /// </summary> + /// <param name="controllingSessionId">The controlling session identifier.</param> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken); + + /// <summary> + /// Sends the message to admin sessions. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="name">The name.</param> + /// <param name="data">The data.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken); + + /// <summary> + /// Sends the message to user sessions. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <returns>Task.</returns> + Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken); + + Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken); + + /// <summary> + /// Sends the message to user device sessions. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="deviceId">The device identifier.</param> + /// <param name="name">The name.</param> + /// <param name="data">The data.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken); + + /// <summary> + /// Sends the restart required message. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendRestartRequiredNotification(CancellationToken cancellationToken); + + /// <summary> + /// Sends the server shutdown notification. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendServerShutdownNotification(CancellationToken cancellationToken); + + /// <summary> + /// Sends the server restart notification. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendServerRestartNotification(CancellationToken cancellationToken); + + /// <summary> + /// Adds the additional user. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="userId">The user identifier.</param> + void AddAdditionalUser(string sessionId, Guid userId); + + /// <summary> + /// Removes the additional user. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="userId">The user identifier.</param> + void RemoveAdditionalUser(string sessionId, Guid userId); + + /// <summary> + /// Reports the now viewing item. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="itemId">The item identifier.</param> + void ReportNowViewingItem(string sessionId, string itemId); + + /// <summary> + /// Reports the now viewing item. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="item">The item.</param> + void ReportNowViewingItem(string sessionId, BaseItemDto item); + + /// <summary> + /// Authenticates the new session. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{SessionInfo}.</returns> + Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request); + + /// <summary> + /// Creates the new session. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task<AuthenticationResult>.</returns> + Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request); + + /// <summary> + /// Reports the capabilities. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="capabilities">The capabilities.</param> + void ReportCapabilities(string sessionId, ClientCapabilities capabilities); + + /// <summary> + /// Reports the transcoding information. + /// </summary> + /// <param name="deviceId">The device identifier.</param> + /// <param name="info">The information.</param> + void ReportTranscodingInfo(string deviceId, TranscodingInfo info); + + /// <summary> + /// Clears the transcoding information. + /// </summary> + /// <param name="deviceId">The device identifier.</param> + void ClearTranscodingInfo(string deviceId); + + /// <summary> + /// Gets the session. + /// </summary> + /// <param name="deviceId">The device identifier.</param> + /// <param name="client">The client.</param> + /// <param name="version">The version.</param> + /// <returns>SessionInfo.</returns> + SessionInfo GetSession(string deviceId, string client, string version); + + /// <summary> + /// Gets the session by authentication token. + /// </summary> + /// <param name="token">The token.</param> + /// <param name="deviceId">The device identifier.</param> + /// <param name="remoteEndpoint">The remote endpoint.</param> + /// <returns>SessionInfo.</returns> + SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint); + + /// <summary> + /// Gets the session by authentication token. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="deviceId">The device identifier.</param> + /// <param name="remoteEndpoint">The remote endpoint.</param> + /// <param name="appVersion">The application version.</param> + /// <returns>Task<SessionInfo>.</returns> + SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion); + + /// <summary> + /// Logouts the specified access token. + /// </summary> + /// <param name="accessToken">The access token.</param> + /// <returns>Task.</returns> + void Logout(string accessToken); + void Logout(AuthenticationInfo accessToken); + + /// <summary> + /// Revokes the user tokens. + /// </summary> + /// <returns>Task.</returns> + void RevokeUserTokens(Guid userId, string currentAccessToken); + + /// <summary> + /// Revokes the token. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>Task.</returns> + void RevokeToken(string id); + + void CloseIfNeeded(SessionInfo session); + } +} diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs new file mode 100644 index 000000000..96daa6ec9 --- /dev/null +++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs @@ -0,0 +1,9 @@ +using System; + +namespace MediaBrowser.Controller.Session +{ + public class SessionEventArgs : EventArgs + { + public SessionInfo SessionInfo { get; set; } + } +} diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs new file mode 100644 index 000000000..869d3fcb0 --- /dev/null +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -0,0 +1,396 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Threading; +using System.Linq; +using System; + +namespace MediaBrowser.Controller.Session +{ + /// <summary> + /// Class SessionInfo + /// </summary> + public class SessionInfo : IDisposable + { + private ISessionManager _sessionManager; + private readonly ILogger _logger; + + public SessionInfo(ISessionManager sessionManager, ILogger logger) + { + _sessionManager = sessionManager; + _logger = logger; + + AdditionalUsers = new SessionUserInfo[] { }; + PlayState = new PlayerStateInfo(); + SessionControllers = new ISessionController[] { }; + } + + public PlayerStateInfo PlayState { get; set; } + + public SessionUserInfo[] AdditionalUsers { get; set; } + + public ClientCapabilities Capabilities { get; set; } + + /// <summary> + /// Gets or sets the remote end point. + /// </summary> + /// <value>The remote end point.</value> + public string RemoteEndPoint { get; set; } + + /// <summary> + /// Gets or sets the playable media types. + /// </summary> + /// <value>The playable media types.</value> + public string[] PlayableMediaTypes + { + get + { + if (Capabilities == null) + { + return new string[] {}; + } + return Capabilities.PlayableMediaTypes; + } + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the username. + /// </summary> + /// <value>The username.</value> + public string UserName { get; set; } + + /// <summary> + /// Gets or sets the type of the client. + /// </summary> + /// <value>The type of the client.</value> + public string Client { get; set; } + + /// <summary> + /// Gets or sets the last activity date. + /// </summary> + /// <value>The last activity date.</value> + public DateTime LastActivityDate { get; set; } + + /// <summary> + /// Gets or sets the last playback check in. + /// </summary> + /// <value>The last playback check in.</value> + public DateTime LastPlaybackCheckIn { get; set; } + + /// <summary> + /// Gets or sets the name of the device. + /// </summary> + /// <value>The name of the device.</value> + public string DeviceName { get; set; } + + public string DeviceType { get; set; } + + /// <summary> + /// Gets or sets the now playing item. + /// </summary> + /// <value>The now playing item.</value> + public BaseItemDto NowPlayingItem { get; set; } + + public BaseItem FullNowPlayingItem { get; set; } + + /// <summary> + /// Gets or sets the device id. + /// </summary> + /// <value>The device id.</value> + public string DeviceId { get; set; } + + /// <summary> + /// Gets or sets the application version. + /// </summary> + /// <value>The application version.</value> + public string ApplicationVersion { get; set; } + + /// <summary> + /// Gets or sets the session controller. + /// </summary> + /// <value>The session controller.</value> + [IgnoreDataMember] + public ISessionController[] SessionControllers { get; set; } + + /// <summary> + /// Gets or sets the application icon URL. + /// </summary> + /// <value>The application icon URL.</value> + public string AppIconUrl { get; set; } + + /// <summary> + /// Gets or sets the supported commands. + /// </summary> + /// <value>The supported commands.</value> + public string[] SupportedCommands + { + get + { + if (Capabilities == null) + { + return new string[] {}; + } + return Capabilities.SupportedCommands; + } + } + + public TranscodingInfo TranscodingInfo { get; set; } + + /// <summary> + /// Gets a value indicating whether this instance is active. + /// </summary> + /// <value><c>true</c> if this instance is active; otherwise, <c>false</c>.</value> + public bool IsActive + { + get + { + var controllers = SessionControllers; + foreach (var controller in controllers) + { + if (controller.IsSessionActive) + { + return true; + } + } + if (controllers.Length > 0) + { + return false; + } + + return true; + } + } + + public bool SupportsMediaControl + { + get + { + if (Capabilities == null || !Capabilities.SupportsMediaControl) + { + return false; + } + + var controllers = SessionControllers; + foreach (var controller in controllers) + { + if (controller.SupportsMediaControl) + { + return true; + } + } + + return false; + } + } + + public bool SupportsRemoteControl + { + get + { + if (Capabilities == null || !Capabilities.SupportsMediaControl) + { + return false; + } + + var controllers = SessionControllers; + foreach (var controller in controllers) + { + if (controller.SupportsMediaControl) + { + return true; + } + } + + return false; + } + } + + public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) + { + var controllers = SessionControllers.ToList(); + foreach (var controller in controllers) + { + if (controller is T) + { + return new Tuple<ISessionController, bool>(controller, false); + } + } + + var newController = factory(this); + _logger.Debug("Creating new {0}", newController.GetType().Name); + controllers.Add(newController); + + SessionControllers = controllers.ToArray(); + return new Tuple<ISessionController, bool>(newController, true); + } + + public void AddController(ISessionController controller) + { + var controllers = SessionControllers.ToList(); + controllers.Add(controller); + SessionControllers = controllers.ToArray(); + } + + public bool ContainsUser(string userId) + { + return ContainsUser(new Guid(userId)); + } + + public bool ContainsUser(Guid userId) + { + if (UserId.Equals(userId)) + { + return true; + } + + foreach (var additionalUser in AdditionalUsers) + { + if (userId.Equals(userId)) + { + return true; + } + } + return false; + } + + private readonly object _progressLock = new object(); + private ITimer _progressTimer; + private PlaybackProgressInfo _lastProgressInfo; + + public void StartAutomaticProgress(ITimerFactory timerFactory, PlaybackProgressInfo progressInfo) + { + if (_disposed) + { + return; + } + + lock (_progressLock) + { + _lastProgressInfo = progressInfo; + + if (_progressTimer == null) + { + _progressTimer = timerFactory.Create(OnProgressTimerCallback, null, 1000, 1000); + } + else + { + _progressTimer.Change(1000, 1000); + } + } + } + + // 1 second + private const long ProgressIncrement = 10000000; + + private async void OnProgressTimerCallback(object state) + { + if (_disposed) + { + return; + } + + var progressInfo = _lastProgressInfo; + if (progressInfo == null) + { + return; + } + if (progressInfo.IsPaused) + { + return; + } + + var positionTicks = progressInfo.PositionTicks ?? 0; + if (positionTicks < 0) + { + positionTicks = 0; + } + + var newPositionTicks = positionTicks + ProgressIncrement; + var item = progressInfo.Item; + long? runtimeTicks = item == null ? null : item.RunTimeTicks; + + // Don't report beyond the runtime + if (runtimeTicks.HasValue && newPositionTicks >= runtimeTicks.Value) + { + return; + } + + progressInfo.PositionTicks = newPositionTicks; + + try + { + await _sessionManager.OnPlaybackProgress(progressInfo, true).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting playback progress", ex); + } + } + + public void StopAutomaticProgress() + { + lock (_progressLock) + { + if (_progressTimer != null) + { + _progressTimer.Dispose(); + _progressTimer = null; + } + _lastProgressInfo = null; + } + } + + private bool _disposed = false; + + public void Dispose() + { + _disposed = true; + + StopAutomaticProgress(); + + var controllers = SessionControllers.ToList(); + SessionControllers = new ISessionController[] { }; + + foreach (var controller in controllers) + { + var disposable = controller as IDisposable; + + if (disposable != null) + { + _logger.Debug("Disposing session controller {0}", disposable.GetType().Name); + + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing session controller", ex); + } + } + } + + _sessionManager = null; + } + + public QueueItem[] NowPlayingQueue { get; set; } + public bool HasCustomDeviceName { get; set; } + public string PlaylistItemId { get; set; } + public string ServerId { get; set; } + public string UserPrimaryImageTag { get; set; } + } +} diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs new file mode 100644 index 000000000..6d0b95bcb --- /dev/null +++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Controller.Entities; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Sorting +{ + /// <summary> + /// Interface IBaseItemComparer + /// </summary> + public interface IBaseItemComparer : IComparer<BaseItem> + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + } +} diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs new file mode 100644 index 000000000..915d4854b --- /dev/null +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; + +namespace MediaBrowser.Controller.Sorting +{ + /// <summary> + /// Represents a BaseItem comparer that requires a User to perform it's comparison + /// </summary> + public interface IUserBaseItemComparer : IBaseItemComparer + { + /// <summary> + /// Gets or sets the user. + /// </summary> + /// <value>The user.</value> + User User { get; set; } + + /// <summary> + /// Gets or sets the user manager. + /// </summary> + /// <value>The user manager.</value> + IUserManager UserManager { get; set; } + + /// <summary> + /// Gets or sets the user data repository. + /// </summary> + /// <value>The user data repository.</value> + IUserDataManager UserDataRepository { get; set; } + } +} diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs new file mode 100644 index 000000000..ec8ee5a11 --- /dev/null +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MediaBrowser.Controller.Sorting +{ + public static class SortExtensions + { + public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName) + { + return list.OrderBy(getName, new AlphanumComparator()); + } + + public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName) + { + return list.OrderByDescending(getName, new AlphanumComparator()); + } + + public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) + { + return list.ThenBy(getName, new AlphanumComparator()); + } + + public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) + { + return list.ThenByDescending(getName, new AlphanumComparator()); + } + + private class AlphanumComparator : IComparer<string> + { + private enum ChunkType { Alphanumeric, Numeric }; + + private static bool InChunk(char ch, char otherCh) + { + var type = ChunkType.Alphanumeric; + + if (char.IsDigit(otherCh)) + { + type = ChunkType.Numeric; + } + + if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) + || (type == ChunkType.Numeric && !char.IsDigit(ch))) + { + return false; + } + + return true; + } + + public static int CompareValues(string s1, string s2) + { + if (s1 == null || s2 == null) + { + return 0; + } + + int thisMarker = 0, thisNumericChunk = 0; + int thatMarker = 0, thatNumericChunk = 0; + + while ((thisMarker < s1.Length) || (thatMarker < s2.Length)) + { + if (thisMarker >= s1.Length) + { + return -1; + } + else if (thatMarker >= s2.Length) + { + return 1; + } + char thisCh = s1[thisMarker]; + char thatCh = s2[thatMarker]; + + StringBuilder thisChunk = new StringBuilder(); + StringBuilder thatChunk = new StringBuilder(); + + while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0]))) + { + thisChunk.Append(thisCh); + thisMarker++; + + if (thisMarker < s1.Length) + { + thisCh = s1[thisMarker]; + } + } + + while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0]))) + { + thatChunk.Append(thatCh); + thatMarker++; + + if (thatMarker < s2.Length) + { + thatCh = s2[thatMarker]; + } + } + + int result = 0; + // If both chunks contain numeric characters, sort them numerically + if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) + { + if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) + { + return 0; + } + if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) + { + return 0; + } + + if (thisNumericChunk < thatNumericChunk) + { + result = -1; + } + + if (thisNumericChunk > thatNumericChunk) + { + result = 1; + } + } + else + { + result = thisChunk.ToString().CompareTo(thatChunk.ToString()); + } + + if (result != 0) + { + return result; + } + } + + return 0; + } + + public int Compare(string x, string y) + { + return CompareValues(x, y); + } + } + } +} diff --git a/MediaBrowser.Controller/Sorting/SortHelper.cs b/MediaBrowser.Controller/Sorting/SortHelper.cs new file mode 100644 index 000000000..3456b9b04 --- /dev/null +++ b/MediaBrowser.Controller/Sorting/SortHelper.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Controller.Sorting +{ + public static class SortHelper + { + private enum ChunkType { Alphanumeric, Numeric }; + + public static bool InChunk(char ch, char otherCh) + { + var type = ChunkType.Alphanumeric; + + if (char.IsDigit(otherCh)) + { + type = ChunkType.Numeric; + } + + if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) + || (type == ChunkType.Numeric && !char.IsDigit(ch))) + { + return false; + } + + return true; + } + } +} diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs new file mode 100644 index 000000000..e41826be5 --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -0,0 +1,74 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.Subtitles +{ + public interface ISubtitleManager + { + /// <summary> + /// Occurs when [subtitle download failure]. + /// </summary> + event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure; + + /// <summary> + /// Occurs when [subtitles downloaded]. + /// </summary> + event EventHandler<SubtitleDownloadEventArgs> SubtitlesDownloaded; + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="subtitleProviders">The subtitle providers.</param> + void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders); + + /// <summary> + /// Searches the subtitles. + /// </summary> + Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, + string language, + bool? isPerfectMatch, + CancellationToken cancellationToken); + + /// <summary> + /// Searches the subtitles. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns> + Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, + CancellationToken cancellationToken); + + /// <summary> + /// Downloads the subtitles. + /// </summary> + Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken); + + /// <summary> + /// Downloads the subtitles. + /// </summary> + Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken); + + /// <summary> + /// Gets the remote subtitles. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{SubtitleResponse}.</returns> + Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the subtitles. + /// </summary> + Task DeleteSubtitles(BaseItem item, int index); + + /// <summary> + /// Gets the providers. + /// </summary> + SubtitleProviderInfo[] GetSupportedProviders(BaseItem item); + } +} diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs new file mode 100644 index 000000000..2502d685d --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs @@ -0,0 +1,40 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Providers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Subtitles +{ + public interface ISubtitleProvider + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the supported media types. + /// </summary> + /// <value>The supported media types.</value> + IEnumerable<VideoContentType> SupportedMediaTypes { get; } + + /// <summary> + /// Searches the subtitles. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns> + Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Gets the subtitles. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{SubtitleResponse}.</returns> + Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs new file mode 100644 index 000000000..1d204f2cb --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Subtitles +{ + public class SubtitleDownloadEventArgs + { + public BaseItem Item { get; set; } + + public string Format { get; set; } + + public string Language { get; set; } + + public bool IsForced { get; set; } + + public string Provider { get; set; } + } + + public class SubtitleDownloadFailureEventArgs + { + public BaseItem Item { get; set; } + + public string Provider { get; set; } + + public Exception Exception { get; set; } + } +} diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs new file mode 100644 index 000000000..e2f6dfc97 --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace MediaBrowser.Controller.Subtitles +{ + public class SubtitleResponse + { + public string Language { get; set; } + public string Format { get; set; } + public bool IsForced { get; set; } + public Stream Stream { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs new file mode 100644 index 000000000..84bf28c05 --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs @@ -0,0 +1,39 @@ +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Subtitles +{ + public class SubtitleSearchRequest : IHasProviderIds + { + public string Language { get; set; } + public string TwoLetterISOLanguageName { get; set; } + + public VideoContentType ContentType { get; set; } + + public string MediaPath { get; set; } + public string SeriesName { get; set; } + public string Name { get; set; } + public int? IndexNumber { get; set; } + public int? IndexNumberEnd { get; set; } + public int? ParentIndexNumber { get; set; } + public int? ProductionYear { get; set; } + public long? RuntimeTicks { get; set; } + public bool IsPerfectMatch { get; set; } + public Dictionary<string, string> ProviderIds { get; set; } + + public bool SearchAllProviders { get; set; } + public string[] DisabledSubtitleFetchers { get; set; } + public string[] SubtitleFetcherOrder { get; set; } + + public SubtitleSearchRequest() + { + SearchAllProviders = true; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + DisabledSubtitleFetchers = new string[] {}; + SubtitleFetcherOrder = new string[] {}; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs new file mode 100644 index 000000000..cf868a381 --- /dev/null +++ b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Model.Sync; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Sync +{ + public interface IHasDynamicAccess + { + /// <summary> + /// Gets the synced file information. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="target">The target.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task<SyncedFileInfo>.</returns> + Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs new file mode 100644 index 000000000..aeb7a3bff --- /dev/null +++ b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Sync +{ + /// <summary> + /// A marker interface + /// </summary> + public interface IRemoteSyncProvider + { + } +} diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs new file mode 100644 index 000000000..335fea296 --- /dev/null +++ b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Sync; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Sync +{ + public interface IServerSyncProvider : ISyncProvider + { + /// <summary> + /// Transfers the file. + /// </summary> + Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, Stream inputStream, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken); + + Task<QueryResult<FileSystemMetadata>> GetFiles(string[] directoryPathParts, SyncTarget target, CancellationToken cancellationToken); + } + + public interface ISupportsDirectCopy + { + /// <summary> + /// Sends the file. + /// </summary> + Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, string inputPath, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs new file mode 100644 index 000000000..0b2fcc95e --- /dev/null +++ b/MediaBrowser.Controller/Sync/ISyncProvider.cs @@ -0,0 +1,27 @@ +using MediaBrowser.Model.Sync; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Sync +{ + public interface ISyncProvider + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the synchronize targets. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <returns>IEnumerable<SyncTarget>.</returns> + List<SyncTarget> GetSyncTargets(string userId); + + /// <summary> + /// Gets all synchronize targets. + /// </summary> + /// <returns>IEnumerable<SyncTarget>.</returns> + List<SyncTarget> GetAllSyncTargets(); + } +} diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs new file mode 100644 index 000000000..1c87551f1 --- /dev/null +++ b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs @@ -0,0 +1,35 @@ +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Sync +{ + public class SyncedFileInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + public string[] PathParts { get; set; } + /// <summary> + /// Gets or sets the protocol. + /// </summary> + /// <value>The protocol.</value> + public MediaProtocol Protocol { get; set; } + /// <summary> + /// Gets or sets the required HTTP headers. + /// </summary> + /// <value>The required HTTP headers.</value> + public Dictionary<string, string> RequiredHttpHeaders { get; set; } + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string Id { get; set; } + + public SyncedFileInfo() + { + RequiredHttpHeaders = new Dictionary<string, string>(); + } + } +} diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs new file mode 100644 index 000000000..56e06bcfa --- /dev/null +++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs @@ -0,0 +1,20 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Querying; +using System.Collections.Generic; +using MediaBrowser.Controller.Dto; + +namespace MediaBrowser.Controller.TV +{ + public interface ITVSeriesManager + { + /// <summary> + /// Gets the next up. + /// </summary> + QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options); + + /// <summary> + /// Gets the next up. + /// </summary> + QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options); + } +} |
