diff options
260 files changed, 8809 insertions, 3856 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 5cd0aae80..785cc395c 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; @@ -28,12 +30,19 @@ namespace MediaBrowser.Api private ILogger Logger { get; set; } /// <summary> + /// The application paths + /// </summary> + private readonly IServerApplicationPaths AppPaths; + + /// <summary> /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class. /// </summary> /// <param name="logger">The logger.</param> - public ApiEntryPoint(ILogger logger) + /// <param name="appPaths">The application paths.</param> + public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths) { Logger = logger; + AppPaths = appPaths; Instance = this; } @@ -43,6 +52,31 @@ namespace MediaBrowser.Api /// </summary> public void Run() { + try + { + DeleteEncodedMediaCache(); + } + catch (DirectoryNotFoundException) + { + // Don't clutter the log + } + catch (IOException ex) + { + Logger.ErrorException("Error deleting encoded media cache", ex); + } + } + + /// <summary> + /// Deletes the encoded media cache. + /// </summary> + private void DeleteEncodedMediaCache() + { + foreach (var file in Directory.EnumerateFiles(AppPaths.EncodedMediaCachePath) + .Where(i => EntityResolutionHelper.VideoFileExtensions.Contains(Path.GetExtension(i))) + .ToList()) + { + File.Delete(file); + } } /// <summary> @@ -62,7 +96,7 @@ namespace MediaBrowser.Api { var jobCount = _activeTranscodingJobs.Count; - Parallel.ForEach(_activeTranscodingJobs, KillTranscodingJob); + Parallel.ForEach(_activeTranscodingJobs.ToList(), KillTranscodingJob); // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files if (jobCount > 0) @@ -317,7 +351,7 @@ namespace MediaBrowser.Api { Logger.Info("Deleting partial stream file(s) {0}", job.Path); - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1500).ConfigureAwait(false); try { diff --git a/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs index a8b34b8bd..8f9babd06 100644 --- a/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs +++ b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs @@ -63,7 +63,9 @@ namespace MediaBrowser.Api if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version)) { - SessionManager.LogSessionActivity(client, version, deviceId, device, user); + var remoteEndPoint = request.RemoteIp; + + SessionManager.LogSessionActivity(client, version, deviceId, device, remoteEndPoint, user); } } } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index ddce1ddcd..62fcbd280 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities; +using System.IO; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -51,6 +52,11 @@ namespace MediaBrowser.Api return ResultFactory.GetOptimizedResult(Request, result); } + protected object ToStreamResult(Stream stream, string contentType) + { + return ResultFactory.GetResult(stream, contentType); + } + /// <summary> /// To the optimized result using cache. /// </summary> diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 2de78d75b..663e1be28 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -1,17 +1,17 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using ServiceStack; +using ServiceStack.Text.Controller; +using ServiceStack.Web; using System; using System.Collections.Generic; using System.Drawing; @@ -19,8 +19,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using ServiceStack.Text.Controller; -using ServiceStack.Web; namespace MediaBrowser.Api.Images { @@ -39,18 +37,6 @@ namespace MediaBrowser.Api.Images public string Id { get; set; } } - [Route("/LiveTv/Channels/{Id}/Images", "GET")] - [Api(Description = "Gets information about an item's images")] - public class GetChannelImageInfos : IReturn<List<ImageInfo>> - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - [Route("/Artists/{Name}/Images", "GET")] [Route("/Genres/{Name}/Images", "GET")] [Route("/GameGenres/{Name}/Images", "GET")] @@ -80,20 +66,7 @@ namespace MediaBrowser.Api.Images [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } - - [Route("/LiveTv/Channels/{Id}/Images/{Type}", "GET")] - [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "GET")] - [Api(Description = "Gets an item image")] - public class GetChannelImage : ImageRequest - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - + /// <summary> /// Class UpdateItemImageIndex /// </summary> @@ -270,19 +243,6 @@ namespace MediaBrowser.Api.Images public Guid Id { get; set; } } - [Route("/LiveTv/Channels/{Id}/Images/{Type}", "DELETE")] - [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "DELETE")] - [Api(Description = "Deletes an item image")] - public class DeleteChannelImage : DeleteImageRequest, IReturnVoid - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - /// <summary> /// Class PostUserImage /// </summary> @@ -358,38 +318,13 @@ namespace MediaBrowser.Api.Images public Stream RequestStream { get; set; } } - [Route("/LiveTv/Channels/{Id}/Images/{Type}", "POST")] - [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "POST")] - [Api(Description = "Posts an item image")] - public class PostChannelImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// <summary> - /// The raw Http Request Input Stream - /// </summary> - /// <value>The request stream.</value> - public Stream RequestStream { get; set; } - } - /// <summary> /// Class ImageService /// </summary> public class ImageService : BaseApiService { - /// <summary> - /// The _user manager - /// </summary> private readonly IUserManager _userManager; - /// <summary> - /// The _library manager - /// </summary> private readonly ILibraryManager _libraryManager; private readonly IApplicationPaths _appPaths; @@ -400,12 +335,11 @@ namespace MediaBrowser.Api.Images private readonly IDtoService _dtoService; private readonly IImageProcessor _imageProcessor; - private readonly ILiveTvManager _liveTv; /// <summary> /// Initializes a new instance of the <see cref="ImageService" /> class. /// </summary> - public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor, ILiveTvManager liveTv) + public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor) { _userManager = userManager; _libraryManager = libraryManager; @@ -414,7 +348,6 @@ namespace MediaBrowser.Api.Images _itemRepo = itemRepo; _dtoService = dtoService; _imageProcessor = imageProcessor; - _liveTv = liveTv; } /// <summary> @@ -431,15 +364,6 @@ namespace MediaBrowser.Api.Images return ToOptimizedResult(result); } - public object Get(GetChannelImageInfos request) - { - var item = _liveTv.GetChannel(request.Id); - - var result = GetItemImageInfos(item); - - return ToOptimizedResult(result); - } - public object Get(GetItemByNameImageInfos request) { var result = GetItemByNameImageInfos(request); @@ -540,7 +464,7 @@ namespace MediaBrowser.Api.Images return list; } - private ImageInfo GetImageInfo(string path, BaseItem item, int? imageIndex, ImageType type) + private ImageInfo GetImageInfo(string path, IHasImages item, int? imageIndex, ImageType type) { try { @@ -567,13 +491,6 @@ namespace MediaBrowser.Api.Images } } - public object Get(GetChannelImage request) - { - var item = _liveTv.GetChannel(request.Id); - - return GetImage(request, item); - } - /// <summary> /// Gets the specified request. /// </summary> @@ -659,20 +576,6 @@ namespace MediaBrowser.Api.Images Task.WaitAll(task); } - public void Post(PostChannelImage request) - { - var pathInfo = PathInfo.Parse(Request.PathInfo); - var id = pathInfo.GetArgumentValue<string>(2); - - request.Type = (ImageType)Enum.Parse(typeof(ImageType), pathInfo.GetArgumentValue<string>(4), true); - - var item = _liveTv.GetChannel(id); - - var task = PostImage(item, request.RequestStream, request.Type, Request.ContentType); - - Task.WaitAll(task); - } - /// <summary> /// Deletes the specified request. /// </summary> @@ -699,15 +602,6 @@ namespace MediaBrowser.Api.Images Task.WaitAll(task); } - public void Delete(DeleteChannelImage request) - { - var item = _liveTv.GetChannel(request.Id); - - var task = item.DeleteImage(request.Type, request.Index); - - Task.WaitAll(task); - } - /// <summary> /// Deletes the specified request. /// </summary> @@ -762,69 +656,9 @@ namespace MediaBrowser.Api.Images /// <param name="newIndex">The new index.</param> /// <returns>Task.</returns> /// <exception cref="System.ArgumentException">The change index operation is only applicable to backdrops and screenshots</exception> - private Task UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex) + private Task UpdateItemIndex(IHasImages item, ImageType type, int currentIndex, int newIndex) { - string file1; - string file2; - - if (type == ImageType.Screenshot) - { - var hasScreenshots = (IHasScreenshots)item; - file1 = hasScreenshots.ScreenshotImagePaths[currentIndex]; - file2 = hasScreenshots.ScreenshotImagePaths[newIndex]; - } - else if (type == ImageType.Backdrop) - { - file1 = item.BackdropImagePaths[currentIndex]; - file2 = item.BackdropImagePaths[newIndex]; - } - else - { - throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); - } - - SwapFiles(file1, file2); - - // Directory watchers should repeat this, but do a quick refresh first - return item.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false); - } - - /// <summary> - /// Swaps the files. - /// </summary> - /// <param name="file1">The file1.</param> - /// <param name="file2">The file2.</param> - private void SwapFiles(string file1, string file2) - { - var temp1 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp"); - var temp2 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp"); - - // Copying over will fail against hidden files - RemoveHiddenAttribute(file1); - RemoveHiddenAttribute(file2); - - File.Copy(file1, temp1); - File.Copy(file2, temp2); - - File.Copy(temp1, file2, true); - File.Copy(temp2, file1, true); - - File.Delete(temp1); - File.Delete(temp2); - } - - private void RemoveHiddenAttribute(string path) - { - var currentFile = new FileInfo(path); - - // This will fail if the file is hidden - if (currentFile.Exists) - { - if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) - { - currentFile.Attributes &= ~FileAttributes.Hidden; - } - } + return item.SwapImages(type, currentIndex, newIndex); } /// <summary> @@ -835,7 +669,7 @@ namespace MediaBrowser.Api.Images /// <returns>System.Object.</returns> /// <exception cref="ResourceNotFoundException"> /// </exception> - private object GetImage(ImageRequest request, BaseItem item) + public object GetImage(ImageRequest request, IHasImages item) { var imagePath = GetImagePath(request, item); @@ -924,7 +758,7 @@ namespace MediaBrowser.Api.Images /// <param name="request">The request.</param> /// <param name="item">The item.</param> /// <returns>System.String.</returns> - private string GetImagePath(ImageRequest request, BaseItem item) + private string GetImagePath(ImageRequest request, IHasImages item) { var index = request.Index ?? 0; @@ -939,7 +773,7 @@ namespace MediaBrowser.Api.Images /// <param name="imageType">Type of the image.</param> /// <param name="mimeType">Type of the MIME.</param> /// <returns>Task.</returns> - private async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType) + public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType) { using (var reader = new StreamReader(inputStream)) { diff --git a/MediaBrowser.Api/Images/ImageWriter.cs b/MediaBrowser.Api/Images/ImageWriter.cs index 5d1ee140d..2ace05125 100644 --- a/MediaBrowser.Api/Images/ImageWriter.cs +++ b/MediaBrowser.Api/Images/ImageWriter.cs @@ -2,12 +2,11 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using ServiceStack; +using ServiceStack.Web; using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using ServiceStack.Web; namespace MediaBrowser.Api.Images { @@ -27,7 +26,7 @@ namespace MediaBrowser.Api.Images /// Gets or sets the item. /// </summary> /// <value>The item.</value> - public BaseItem Item { get; set; } + public IHasImages Item { get; set; } /// <summary> /// The original image date modified /// </summary> diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index 9fc4bb3d0..6c5b279d0 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -146,7 +146,7 @@ namespace MediaBrowser.Api private async Task UpdateItem(UpdateChannel request) { - var item = _liveTv.GetChannel(request.Id); + var item = _liveTv.GetInternalChannel(request.Id); UpdateItem(request, item); @@ -271,6 +271,17 @@ namespace MediaBrowser.Api item.Overview = request.Overview; item.Genres = request.Genres; + var episode = item as Episode; + if (episode != null) + { + episode.DvdSeasonNumber = request.DvdSeasonNumber; + episode.DvdEpisodeNumber = request.DvdEpisodeNumber; + episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber; + episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber; + episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber; + episode.AbsoluteEpisodeNumber = request.AbsoluteEpisodeNumber; + } + var hasTags = item as IHasTags; if (hasTags != null) { @@ -300,10 +311,12 @@ namespace MediaBrowser.Api SetProductionLocations(item, request); - var hasLanguage = item as IHasLanguage; - if (hasLanguage != null) + var hasLang = item as IHasPreferredMetadataLanguage; + + if (hasLang != null) { - hasLanguage.Language = request.Language; + hasLang.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode; + hasLang.PreferredMetadataLanguage = request.PreferredMetadataLanguage; } var hasAspectRatio = item as IHasAspectRatio; @@ -374,6 +387,11 @@ namespace MediaBrowser.Api series.Status = request.Status; series.AirDays = request.AirDays; series.AirTime = request.AirTime; + + if (request.DisplaySpecialsWithSeasons.HasValue) + { + series.DisplaySpecialsWithSeasons = request.DisplaySpecialsWithSeasons.Value; + } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index bef0ba1e1..f3d5824da 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -1,6 +1,7 @@ using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; using ServiceStack; using System; using System.Collections.Generic; @@ -67,7 +68,27 @@ namespace MediaBrowser.Api.Library /// <returns>System.Object.</returns> public object Get(GetPhyscialPaths request) { - var result = _libraryManager.RootFolder.Children.SelectMany(c => c.ResolveArgs.PhysicalLocations).ToList(); + var result = _libraryManager.RootFolder.Children + .SelectMany(c => + { + var locationType = c.LocationType; + + if (locationType != LocationType.Remote && locationType != LocationType.Virtual) + { + try + { + return c.ResolveArgs.PhysicalLocations; + } + catch (Exception ex) + { + Logger.ErrorException("Error getting ResolveArgs for {0}", ex, c.Path); + } + + } + + return new List<string>(); + }) + .ToList(); return ToOptimizedResult(result); } @@ -85,7 +106,7 @@ namespace MediaBrowser.Api.Library { allTypes = allTypes.Where(t => { - if (t == typeof(UserRootFolder) || t == typeof(AggregateFolder) || t == typeof(Folder) || t == typeof(IndexFolder) || t == typeof(CollectionFolder) || t == typeof(Year)) + if (t == typeof(UserRootFolder) || t == typeof(AggregateFolder) || t == typeof(Folder) || t == typeof(CollectionFolder) || t == typeof(Year)) { return false; } diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index c964b5517..775907379 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -28,8 +28,8 @@ namespace MediaBrowser.Api.Library public string UserId { get; set; } } - [Route("/Library/VirtualFolders/{Name}", "POST")] - [Route("/Users/{UserId}/VirtualFolders/{Name}", "POST")] + [Route("/Library/VirtualFolders", "POST")] + [Route("/Users/{UserId}/VirtualFolders", "POST")] public class AddVirtualFolder : IReturnVoid { /// <summary> @@ -57,8 +57,8 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } - [Route("/Library/VirtualFolders/{Name}", "DELETE")] - [Route("/Users/{UserId}/VirtualFolders/{Name}", "DELETE")] + [Route("/Library/VirtualFolders", "DELETE")] + [Route("/Users/{UserId}/VirtualFolders", "DELETE")] public class RemoveVirtualFolder : IReturnVoid { /// <summary> @@ -80,8 +80,8 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } - [Route("/Library/VirtualFolders/{Name}/Name", "POST")] - [Route("/Users/{UserId}/VirtualFolders/{Name}/Name", "POST")] + [Route("/Library/VirtualFolders/Name", "POST")] + [Route("/Users/{UserId}/VirtualFolders/Name", "POST")] public class RenameVirtualFolder : IReturnVoid { /// <summary> @@ -109,8 +109,8 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } - [Route("/Library/VirtualFolders/{Name}/Paths", "POST")] - [Route("/Users/{UserId}/VirtualFolders/{Name}/Paths", "POST")] + [Route("/Library/VirtualFolders/Paths", "POST")] + [Route("/Users/{UserId}/VirtualFolders/Paths", "POST")] public class AddMediaPath : IReturnVoid { /// <summary> @@ -138,8 +138,8 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } - [Route("/Library/VirtualFolders/{Name}/Paths", "DELETE")] - [Route("/Users/{UserId}/VirtualFolders/{Name}/Paths", "DELETE")] + [Route("/Library/VirtualFolders/Paths", "DELETE")] + [Route("/Users/{UserId}/VirtualFolders/Paths", "DELETE")] public class RemoveMediaPath : IReturnVoid { /// <summary> @@ -243,6 +243,11 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Post(AddVirtualFolder request) { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentNullException("request"); + } + var name = _fileSystem.GetValidFilename(request.Name); string rootFolderPath; @@ -307,6 +312,16 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Post(RenameVirtualFolder request) { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentNullException("request"); + } + + if (string.IsNullOrWhiteSpace(request.NewName)) + { + throw new ArgumentNullException("request"); + } + string rootFolderPath; if (string.IsNullOrEmpty(request.UserId)) @@ -380,6 +395,11 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Delete(RemoveVirtualFolder request) { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentNullException("request"); + } + string rootFolderPath; if (string.IsNullOrEmpty(request.UserId)) @@ -435,6 +455,11 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Post(AddMediaPath request) { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentNullException("request"); + } + _directoryWatchers.Stop(); try @@ -476,6 +501,11 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Delete(RemoveMediaPath request) { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentNullException("request"); + } + _directoryWatchers.Stop(); try diff --git a/MediaBrowser.Api/LibraryService.cs b/MediaBrowser.Api/LibraryService.cs index 7dc8301fe..d9442b63d 100644 --- a/MediaBrowser.Api/LibraryService.cs +++ b/MediaBrowser.Api/LibraryService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Dto; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -32,6 +33,21 @@ namespace MediaBrowser.Api public string Id { get; set; } } + [Route("/Videos/{Id}/Subtitle/{Index}", "GET")] + [Api(Description = "Gets an external subtitle file")] + public class GetSubtitle + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + + [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] + public int Index { get; set; } + } + /// <summary> /// Class GetCriticReviews /// </summary> @@ -240,6 +256,25 @@ namespace MediaBrowser.Api return ToStaticFileResult(item.Path); } + public object Get(GetSubtitle request) + { + var subtitleStream = _itemRepo.GetMediaStreams(new MediaStreamQuery + { + + Index = request.Index, + ItemId = new Guid(request.Id), + Type = MediaStreamType.Subtitle + + }).FirstOrDefault(); + + if (subtitleStream == null) + { + throw new ResourceNotFoundException(); + } + + return ToStaticFileResult(subtitleStream.Path); + } + /// <summary> /// Gets the specified request. /// </summary> diff --git a/MediaBrowser.Api/LiveTv/LiveTvImageService.cs b/MediaBrowser.Api/LiveTv/LiveTvImageService.cs new file mode 100644 index 000000000..65c4e5e23 --- /dev/null +++ b/MediaBrowser.Api/LiveTv/LiveTvImageService.cs @@ -0,0 +1,195 @@ +using MediaBrowser.Api.Images; +using MediaBrowser.Common.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.Dto; +using MediaBrowser.Model.Entities; +using ServiceStack; +using ServiceStack.Text.Controller; +using ServiceStack.Web; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.LiveTv +{ + [Route("/LiveTv/Channels/{Id}/Images/{Type}", "POST")] + [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "POST")] + [Api(Description = "Posts an item image")] + public class PostChannelImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + /// <summary> + /// The raw Http Request Input Stream + /// </summary> + /// <value>The request stream.</value> + public Stream RequestStream { get; set; } + } + + [Route("/LiveTv/Channels/{Id}/Images/{Type}", "DELETE")] + [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "DELETE")] + [Api(Description = "Deletes an item image")] + public class DeleteChannelImage : DeleteImageRequest, IReturnVoid + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + } + [Route("/LiveTv/Channels/{Id}/Images/{Type}", "GET")] + [Route("/LiveTv/Channels/{Id}/Images/{Type}/{Index}", "GET")] + [Api(Description = "Gets an item image")] + public class GetChannelImage : ImageRequest + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/LiveTv/Recordings/{Id}/Images/{Type}", "GET")] + [Route("/LiveTv/Recordings/{Id}/Images/{Type}/{Index}", "GET")] + [Api(Description = "Gets an item image")] + public class GetRecordingImage : ImageRequest + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/LiveTv/Programs/{Id}/Images/{Type}", "GET")] + [Route("/LiveTv/Programs/{Id}/Images/{Type}/{Index}", "GET")] + [Api(Description = "Gets an item image")] + public class GetProgramImage : ImageRequest + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Program Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/LiveTv/Channels/{Id}/Images", "GET")] + [Api(Description = "Gets information about an item's images")] + public class GetChannelImageInfos : IReturn<List<ImageInfo>> + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + public class LiveTvImageService : BaseApiService + { + private readonly ILiveTvManager _liveTv; + + private readonly IUserManager _userManager; + + private readonly ILibraryManager _libraryManager; + + private readonly IApplicationPaths _appPaths; + + private readonly IProviderManager _providerManager; + + private readonly IItemRepository _itemRepo; + private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; + + public LiveTvImageService(ILiveTvManager liveTv, IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor) + { + _liveTv = liveTv; + _userManager = userManager; + _libraryManager = libraryManager; + _appPaths = appPaths; + _providerManager = providerManager; + _itemRepo = itemRepo; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + } + + public object Get(GetChannelImageInfos request) + { + var item = _liveTv.GetInternalChannel(request.Id); + + var result = GetImageService().GetItemImageInfos(item); + + return ToOptimizedResult(result); + } + + public object Get(GetChannelImage request) + { + var item = _liveTv.GetInternalChannel(request.Id); + + return GetImageService().GetImage(request, item); + } + + public object Get(GetRecordingImage request) + { + var item = _liveTv.GetInternalRecording(request.Id, CancellationToken.None).Result; + + return GetImageService().GetImage(request, item); + } + + public object Get(GetProgramImage request) + { + var item = _liveTv.GetInternalProgram(request.Id); + + return GetImageService().GetImage(request, item); + } + + public void Post(PostChannelImage request) + { + var pathInfo = PathInfo.Parse(Request.PathInfo); + var id = pathInfo.GetArgumentValue<string>(2); + + request.Type = (ImageType)Enum.Parse(typeof(ImageType), pathInfo.GetArgumentValue<string>(4), true); + + var item = _liveTv.GetInternalChannel(id); + + var task = GetImageService().PostImage(item, request.RequestStream, request.Type, Request.ContentType); + + Task.WaitAll(task); + } + + public void Delete(DeleteChannelImage request) + { + var item = _liveTv.GetInternalChannel(request.Id); + + var task = item.DeleteImage(request.Type, request.Index); + + Task.WaitAll(task); + } + + private ImageService GetImageService() + { + return new ImageService(_userManager, _libraryManager, _appPaths, _providerManager, _itemRepo, _dtoService, + _imageProcessor) + { + ResultFactory = ResultFactory, + Request = Request + }; + } + } +} diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 9e83a56de..61883ddaa 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using ServiceStack; @@ -23,7 +24,7 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "Type", Description = "Optional filter by channel type.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public ChannelType? Type { get; set; } - [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string UserId { get; set; } } @@ -38,7 +39,7 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } - [ApiMember(Name = "UserId", Description = "Optional user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string UserId { get; set; } } @@ -48,6 +49,26 @@ namespace MediaBrowser.Api.LiveTv { [ApiMember(Name = "ChannelId", Description = "Optional filter by channel id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string ChannelId { get; set; } + + [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } + + [ApiMember(Name = "GroupId", Description = "Optional filter by recording group.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string GroupId { get; set; } + + [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? StartIndex { get; set; } + + [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? Limit { get; set; } + } + + [Route("/LiveTv/Recordings/Groups", "GET")] + [Api(Description = "Gets live tv recording groups")] + public class GetRecordingGroups : IReturn<QueryResult<RecordingInfoDto>> + { + [ApiMember(Name = "UserId", Description = "Optional filter by user and attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } } [Route("/LiveTv/Recordings/{Id}", "GET")] @@ -56,6 +77,9 @@ namespace MediaBrowser.Api.LiveTv { [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } + + [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } } [Route("/LiveTv/Timers/{Id}", "GET")] @@ -66,6 +90,14 @@ namespace MediaBrowser.Api.LiveTv public string Id { get; set; } } + [Route("/LiveTv/Timers/Defaults", "GET")] + [Api(Description = "Gets default values for a new timer")] + public class GetDefaultTimer : IReturn<SeriesTimerInfoDto> + { + [ApiMember(Name = "ProgramId", Description = "Optional, to attach default values based on a program.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string ProgramId { get; set; } + } + [Route("/LiveTv/Timers", "GET")] [Api(Description = "Gets live tv timers")] public class GetTimers : IReturn<QueryResult<TimerInfoDto>> @@ -85,6 +117,18 @@ namespace MediaBrowser.Api.LiveTv public string UserId { get; set; } } + [Route("/LiveTv/Programs/{Id}", "GET")] + [Api(Description = "Gets a live tv program")] + public class GetProgram : IReturn<ProgramInfoDto> + { + [ApiMember(Name = "Id", Description = "Program Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + + [ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } + } + + [Route("/LiveTv/Recordings/{Id}", "DELETE")] [Api(Description = "Deletes a live tv recording")] public class DeleteRecording : IReturnVoid @@ -100,14 +144,76 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } - + + [Route("/LiveTv/Timers/{Id}", "POST")] + [Api(Description = "Updates a live tv timer")] + public class UpdateTimer : TimerInfoDto, IReturnVoid + { + } + + [Route("/LiveTv/Timers", "POST")] + [Api(Description = "Creates a live tv timer")] + public class CreateTimer : TimerInfoDto, IReturnVoid + { + } + + [Route("/LiveTv/SeriesTimers/{Id}", "GET")] + [Api(Description = "Gets a live tv series timer")] + public class GetSeriesTimer : IReturn<TimerInfoDto> + { + [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/LiveTv/SeriesTimers", "GET")] + [Api(Description = "Gets live tv series timers")] + public class GetSeriesTimers : IReturn<QueryResult<SeriesTimerInfoDto>> + { + } + + [Route("/LiveTv/SeriesTimers/{Id}", "DELETE")] + [Api(Description = "Cancels a live tv series timer")] + public class CancelSeriesTimer : IReturnVoid + { + [ApiMember(Name = "Id", Description = "Timer Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/LiveTv/SeriesTimers/{Id}", "POST")] + [Api(Description = "Updates a live tv series timer")] + public class UpdateSeriesTimer : SeriesTimerInfoDto, IReturnVoid + { + } + + [Route("/LiveTv/SeriesTimers", "POST")] + [Api(Description = "Creates a live tv series timer")] + public class CreateSeriesTimer : SeriesTimerInfoDto, IReturnVoid + { + } + + [Route("/LiveTv/Recordings/{Id}/Stream", "GET")] + public class GetInternalRecordingStream + { + [ApiMember(Name = "Id", Description = "Recording Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/LiveTv/Channels/{Id}/Stream", "GET")] + public class GetInternalChannelStream + { + [ApiMember(Name = "Id", Description = "Channel Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + public class LiveTvService : BaseApiService { private readonly ILiveTvManager _liveTvManager; + private readonly IUserManager _userManager; - public LiveTvService(ILiveTvManager liveTvManager) + public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager) { _liveTvManager = liveTvManager; + _userManager = userManager; } public object Get(GetServices request) @@ -134,14 +240,16 @@ namespace MediaBrowser.Api.LiveTv ChannelType = request.Type, UserId = request.UserId - }); + }, CancellationToken.None).Result; return ToOptimizedResult(result); } public object Get(GetChannel request) { - var result = _liveTvManager.GetChannelInfoDto(request.Id, request.UserId); + var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId)); + + var result = _liveTvManager.GetChannel(request.Id, CancellationToken.None, user).Result; return ToOptimizedResult(result); } @@ -162,7 +270,11 @@ namespace MediaBrowser.Api.LiveTv { var result = _liveTvManager.GetRecordings(new RecordingQuery { - ChannelId = request.ChannelId + ChannelId = request.ChannelId, + UserId = request.UserId, + GroupId = request.GroupId, + StartIndex = request.StartIndex, + Limit = request.Limit }, CancellationToken.None).Result; @@ -171,7 +283,9 @@ namespace MediaBrowser.Api.LiveTv public object Get(GetRecording request) { - var result = _liveTvManager.GetRecording(request.Id, CancellationToken.None).Result; + var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId)); + + var result = _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).Result; return ToOptimizedResult(result); } @@ -207,5 +321,107 @@ namespace MediaBrowser.Api.LiveTv Task.WaitAll(task); } + + public void Post(UpdateTimer request) + { + var task = _liveTvManager.UpdateTimer(request, CancellationToken.None); + + Task.WaitAll(task); + } + + public object Get(GetSeriesTimers request) + { + var result = _liveTvManager.GetSeriesTimers(new SeriesTimerQuery + { + + }, CancellationToken.None).Result; + + return ToOptimizedResult(result); + } + + public object Get(GetSeriesTimer request) + { + var result = _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).Result; + + return ToOptimizedResult(result); + } + + public void Delete(CancelSeriesTimer request) + { + var task = _liveTvManager.CancelSeriesTimer(request.Id); + + Task.WaitAll(task); + } + + public void Post(UpdateSeriesTimer request) + { + var task = _liveTvManager.UpdateSeriesTimer(request, CancellationToken.None); + + Task.WaitAll(task); + } + + public object Get(GetDefaultTimer request) + { + if (string.IsNullOrEmpty(request.ProgramId)) + { + var result = _liveTvManager.GetNewTimerDefaults(CancellationToken.None).Result; + + return ToOptimizedResult(result); + } + else + { + var result = _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).Result; + + return ToOptimizedResult(result); + } + } + + public object Get(GetProgram request) + { + var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId)); + + var result = _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).Result; + + return ToOptimizedResult(result); + } + + public void Post(CreateSeriesTimer request) + { + var task = _liveTvManager.CreateSeriesTimer(request, CancellationToken.None); + + Task.WaitAll(task); + } + + public void Post(CreateTimer request) + { + var task = _liveTvManager.CreateTimer(request, CancellationToken.None); + + Task.WaitAll(task); + } + + public object Get(GetInternalRecordingStream request) + { + var stream = _liveTvManager.GetRecordingStream(request.Id, CancellationToken.None).Result; + + return ToStreamResult(stream.Stream, stream.MimeType); + } + + public object Get(GetInternalChannelStream request) + { + var stream = _liveTvManager.GetChannelStream(request.Id, CancellationToken.None).Result; + + return ToStreamResult(stream.Stream, stream.MimeType); + } + + public object Get(GetRecordingGroups request) + { + var result = _liveTvManager.GetRecordingGroups(new RecordingGroupQuery + { + UserId = request.UserId + + }, CancellationToken.None).Result; + + return ToOptimizedResult(result); + } } }
\ No newline at end of file diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index b1e0339fc..0732ee00c 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Api</RootNamespace> <AssemblyName>MediaBrowser.Api</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -25,6 +25,7 @@ <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <PlatformTarget>AnyCPU</PlatformTarget> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -33,19 +34,21 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup> <RunPostBuildEvent>Always</RunPostBuildEvent> </PropertyGroup> <ItemGroup> - <Reference Include="ServiceStack.Interfaces, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> - </Reference> - <Reference Include="ServiceStack.Text, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Text.dll</HintPath> - </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="Microsoft.CSharp" /> @@ -56,6 +59,12 @@ <HintPath>..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll</HintPath> </Reference> <Reference Include="System.Xml" /> + <Reference Include="ServiceStack.Interfaces"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Text"> + <HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath> + </Reference> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -81,6 +90,7 @@ <Compile Include="Library\LibraryHelpers.cs" /> <Compile Include="Library\LibraryService.cs" /> <Compile Include="Library\LibraryStructureService.cs" /> + <Compile Include="LiveTv\LiveTvImageService.cs" /> <Compile Include="LiveTv\LiveTvService.cs" /> <Compile Include="LocalizationService.cs" /> <Compile Include="MoviesService.cs" /> @@ -153,7 +163,7 @@ <PostBuildEvent> </PostBuildEvent> </PropertyGroup> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> + <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 90996296d..1e2ae58b2 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -1,13 +1,14 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -32,7 +33,7 @@ namespace MediaBrowser.Api.Playback /// Gets or sets the application paths. /// </summary> /// <value>The application paths.</value> - protected IServerApplicationPaths ApplicationPaths { get; private set; } + protected IServerConfigurationManager ServerConfigurationManager { get; private set; } /// <summary> /// Gets or sets the user manager. @@ -62,21 +63,26 @@ namespace MediaBrowser.Api.Playback protected IFileSystem FileSystem { get; private set; } protected IItemRepository ItemRepository { get; private set; } + protected ILiveTvManager LiveTvManager { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="BaseStreamingService" /> class. /// </summary> - /// <param name="appPaths">The app paths.</param> + /// <param name="serverConfig">The server configuration.</param> /// <param name="userManager">The user manager.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="isoManager">The iso manager.</param> /// <param name="mediaEncoder">The media encoder.</param> - protected BaseStreamingService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository) + /// <param name="dtoService">The dto service.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="itemRepository">The item repository.</param> + protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager) { + LiveTvManager = liveTvManager; ItemRepository = itemRepository; FileSystem = fileSystem; DtoService = dtoService; - ApplicationPaths = appPaths; + ServerConfigurationManager = serverConfig; UserManager = userManager; LibraryManager = libraryManager; IsoManager = isoManager; @@ -105,7 +111,7 @@ namespace MediaBrowser.Api.Playback /// <returns>System.String.</returns> protected virtual string GetOutputFileExtension(StreamState state) { - return Path.GetExtension(state.Url); + return Path.GetExtension(state.RequestedUrl); } /// <summary> @@ -115,7 +121,7 @@ namespace MediaBrowser.Api.Playback /// <returns>System.String.</returns> protected virtual string GetOutputFilePath(StreamState state) { - var folder = ApplicationPaths.EncodedMediaCachePath; + var folder = ServerConfigurationManager.ApplicationPaths.EncodedMediaCachePath; var outputFileExtension = GetOutputFileExtension(state); @@ -182,7 +188,7 @@ namespace MediaBrowser.Api.Playback { var args = string.Empty; - if (state.Item.LocationType == LocationType.Remote) + if (state.IsRemote || !state.HasMediaStreams) { return string.Empty; } @@ -191,6 +197,10 @@ namespace MediaBrowser.Api.Playback { args += string.Format("-map 0:{0}", state.VideoStream.Index); } + else if (!state.HasMediaStreams) + { + args += string.Format("-map 0:{0}", 0); + } else { args += "-map -0:v"; @@ -200,6 +210,10 @@ namespace MediaBrowser.Api.Playback { args += string.Format(" -map 0:{0}", state.AudioStream.Index); } + else if (!state.HasMediaStreams) + { + args += string.Format(" -map 0:{0}", 1); + } else { @@ -247,6 +261,64 @@ namespace MediaBrowser.Api.Playback } /// <summary> + /// Gets the number of threads. + /// </summary> + /// <returns>System.Int32.</returns> + /// <exception cref="System.Exception">Unrecognized EncodingQuality value.</exception> + protected int GetNumberOfThreads() + { + var quality = ServerConfigurationManager.Configuration.EncodingQuality; + + switch (quality) + { + case EncodingQuality.Auto: + return 0; + case EncodingQuality.HighSpeed: + return 2; + case EncodingQuality.HighQuality: + return 2; + case EncodingQuality.MaxQuality: + return 0; + default: + throw new Exception("Unrecognized EncodingQuality value."); + } + } + + /// <summary> + /// Gets the video bitrate to specify on the command line + /// </summary> + /// <param name="state">The state.</param> + /// <param name="videoCodec">The video codec.</param> + /// <returns>System.String.</returns> + protected string GetVideoQualityParam(StreamState state, string videoCodec) + { + var args = string.Empty; + + // webm + if (videoCodec.Equals("libvpx", StringComparison.OrdinalIgnoreCase)) + { + args = "-speed 16 -quality good -profile:v 0 -slices 8"; + } + + // asf/wmv + else if (videoCodec.Equals("wmv2", StringComparison.OrdinalIgnoreCase)) + { + args = "-g 100 -qmax 15"; + } + + else if (videoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase)) + { + args = "-preset superfast"; + } + else if (videoCodec.Equals("mpeg4", StringComparison.OrdinalIgnoreCase)) + { + args = "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; + } + + return args.Trim(); + } + + /// <summary> /// If we're going to put a fixed size on the command line, this will calculate it /// </summary> /// <param name="state">The state.</param> @@ -268,14 +340,17 @@ namespace MediaBrowser.Api.Playback string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)) { - assSubtitleParam = GetTextSubtitleParam((Video)state.Item, state.SubtitleStream, request.StartTimeTicks, performTextSubtitleConversion); + assSubtitleParam = GetTextSubtitleParam(state, request.StartTimeTicks, performTextSubtitleConversion); } } // If fixed dimensions were supplied if (request.Width.HasValue && request.Height.HasValue) { - return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", request.Width.Value, request.Height.Value, assSubtitleParam); + var widthParam = request.Width.Value.ToString(UsCulture); + var heightParam = request.Height.Value.ToString(UsCulture); + + return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", widthParam, heightParam, assSubtitleParam); } var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase); @@ -283,33 +358,41 @@ namespace MediaBrowser.Api.Playback // If a fixed width was requested if (request.Width.HasValue) { + var widthParam = request.Width.Value.ToString(UsCulture); + return isH264Output ? - string.Format(" -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", request.Width.Value, assSubtitleParam) : - string.Format(" -vf \"scale={0}:-1{1}\"", request.Width.Value, assSubtitleParam); + string.Format(" -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", widthParam, assSubtitleParam) : + string.Format(" -vf \"scale={0}:-1{1}\"", widthParam, assSubtitleParam); } // If a fixed height was requested if (request.Height.HasValue) { + var heightParam = request.Height.Value.ToString(UsCulture); + return isH264Output ? - string.Format(" -vf \"scale=trunc(oh*a*2)/2:{0}{1}\"", request.Height.Value, assSubtitleParam) : - string.Format(" -vf \"scale=-1:{0}{1}\"", request.Height.Value, assSubtitleParam); + string.Format(" -vf \"scale=trunc(oh*a*2)/2:{0}{1}\"", heightParam, assSubtitleParam) : + string.Format(" -vf \"scale=-1:{0}{1}\"", heightParam, assSubtitleParam); } // If a max width was requested if (request.MaxWidth.HasValue && (!request.MaxHeight.HasValue || state.VideoStream == null)) { + var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + return isH264Output ? - string.Format(" -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", request.MaxWidth.Value, assSubtitleParam) : - string.Format(" -vf \"scale=min(iw\\,{0}):-1{1}\"", request.MaxWidth.Value, assSubtitleParam); + string.Format(" -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", maxWidthParam, assSubtitleParam) : + string.Format(" -vf \"scale=min(iw\\,{0}):-1{1}\"", maxWidthParam, assSubtitleParam); } // If a max height was requested if (request.MaxHeight.HasValue && (!request.MaxWidth.HasValue || state.VideoStream == null)) { + var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); + return isH264Output ? - string.Format(" -vf \"scale=trunc(oh*a*2)/2:min(ih\\,{0}){1}\"", request.MaxHeight.Value, assSubtitleParam) : - string.Format(" -vf \"scale=-1:min(ih\\,{0}){1}\"", request.MaxHeight.Value, assSubtitleParam); + string.Format(" -vf \"scale=trunc(oh*a*2)/2:min(ih\\,{0}){1}\"", maxHeightParam, assSubtitleParam) : + string.Format(" -vf \"scale=-1:min(ih\\,{0}){1}\"", maxHeightParam, assSubtitleParam); } if (state.VideoStream == null) @@ -329,7 +412,10 @@ namespace MediaBrowser.Api.Playback // If we're encoding with libx264, it can't handle odd numbered widths or heights, so we'll have to fix that if (isH264Output) { - return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", outputSize.Width, outputSize.Height, assSubtitleParam); + var widthParam = outputSize.Width.ToString(UsCulture); + var heightParam = outputSize.Height.ToString(UsCulture); + + return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", widthParam, heightParam, assSubtitleParam); } // Otherwise use -vf scale since ffmpeg will ensure internally that the aspect ratio is preserved @@ -339,14 +425,14 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the text subtitle param. /// </summary> - /// <param name="video">The video.</param> - /// <param name="subtitleStream">The subtitle stream.</param> + /// <param name="state">The state.</param> /// <param name="startTimeTicks">The start time ticks.</param> /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param> /// <returns>System.String.</returns> - protected string GetTextSubtitleParam(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) + protected string GetTextSubtitleParam(StreamState state, long? startTimeTicks, bool performConversion) { - var path = subtitleStream.IsExternal ? GetConvertedAssPath(video, subtitleStream, startTimeTicks, performConversion) : GetExtractedAssPath(video, subtitleStream, startTimeTicks, performConversion); + var path = state.SubtitleStream.IsExternal ? GetConvertedAssPath(state.MediaPath, state.SubtitleStream, startTimeTicks, performConversion) : + GetExtractedAssPath(state, startTimeTicks, performConversion); if (string.IsNullOrEmpty(path)) { @@ -359,22 +445,21 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the extracted ass path. /// </summary> - /// <param name="video">The video.</param> - /// <param name="subtitleStream">The subtitle stream.</param> + /// <param name="state">The state.</param> /// <param name="startTimeTicks">The start time ticks.</param> /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param> /// <returns>System.String.</returns> - private string GetExtractedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) + private string GetExtractedAssPath(StreamState state, long? startTimeTicks, bool performConversion) { var offset = TimeSpan.FromTicks(startTimeTicks ?? 0); - var path = Kernel.Instance.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass"); + var path = FFMpegManager.Instance.GetSubtitleCachePath(state.MediaPath, state.SubtitleStream, offset, ".ass"); if (performConversion) { InputType type; - var inputPath = MediaEncoderHelpers.GetInputArgument(video, null, out type); + var inputPath = MediaEncoderHelpers.GetInputArgument(state.MediaPath, state.IsRemote, state.VideoType, state.IsoType, null, state.PlayableStreamFileNames, out type); try { @@ -382,7 +467,7 @@ namespace MediaBrowser.Api.Playback Directory.CreateDirectory(parentPath); - var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, subtitleStream.Index, offset, path, CancellationToken.None); + var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, state.SubtitleStream.Index, offset, path, CancellationToken.None); Task.WaitAll(task); } @@ -398,22 +483,16 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the converted ass path. /// </summary> - /// <param name="video">The video.</param> + /// <param name="mediaPath">The media path.</param> /// <param name="subtitleStream">The subtitle stream.</param> /// <param name="startTimeTicks">The start time ticks.</param> /// <param name="performConversion">if set to <c>true</c> [perform conversion].</param> /// <returns>System.String.</returns> - private string GetConvertedAssPath(Video video, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) + private string GetConvertedAssPath(string mediaPath, MediaStream subtitleStream, long? startTimeTicks, bool performConversion) { - // If it's already ass, no conversion neccessary - //if (string.Equals(Path.GetExtension(subtitleStream.Path), ".ass", StringComparison.OrdinalIgnoreCase)) - //{ - // return subtitleStream.Path; - //} - var offset = TimeSpan.FromTicks(startTimeTicks ?? 0); - var path = Kernel.Instance.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, offset, ".ass"); + var path = FFMpegManager.Instance.GetSubtitleCachePath(mediaPath, subtitleStream, offset, ".ass"); if (performConversion) { @@ -461,25 +540,15 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the probe size argument. /// </summary> - /// <param name="item">The item.</param> + /// <param name="mediaPath">The media path.</param> + /// <param name="isVideo">if set to <c>true</c> [is video].</param> + /// <param name="videoType">Type of the video.</param> + /// <param name="isoType">Type of the iso.</param> /// <returns>System.String.</returns> - protected string GetProbeSizeArgument(BaseItem item) + protected string GetProbeSizeArgument(string mediaPath, bool isVideo, VideoType? videoType, IsoType? isoType) { - var type = InputType.AudioFile; - - if (item is Audio) - { - type = MediaEncoderHelpers.GetInputType(item.Path, null, null); - } - else - { - var video = item as Video; - - if (video != null) - { - type = MediaEncoderHelpers.GetInputType(item.Path, video.VideoType, video.IsoType); - } - } + var type = !isVideo ? MediaEncoderHelpers.GetInputType(mediaPath, null, null) : + MediaEncoderHelpers.GetInputType(mediaPath, videoType, isoType); return MediaEncoder.GetProbeSizeArgument(type); } @@ -589,22 +658,19 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the input argument. /// </summary> - /// <param name="item">The item.</param> - /// <param name="isoMount">The iso mount.</param> + /// <param name="state">The state.</param> /// <returns>System.String.</returns> - protected string GetInputArgument(BaseItem item, IIsoMount isoMount) + protected string GetInputArgument(StreamState state) { var type = InputType.AudioFile; - var inputPath = new[] { item.Path }; - - var video = item as Video; + var inputPath = new[] { state.MediaPath }; - if (video != null) + if (state.IsInputVideo) { - if (!(video.VideoType == VideoType.Iso && isoMount == null)) + if (!(state.VideoType == VideoType.Iso && state.IsoMount == null)) { - inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type); + inputPath = MediaEncoderHelpers.GetInputArgument(state.MediaPath, state.IsRemote, state.VideoType, state.IsoType, state.IsoMount, state.PlayableStreamFileNames, out type); } } @@ -623,11 +689,9 @@ namespace MediaBrowser.Api.Playback Directory.CreateDirectory(parentPath); - var video = state.Item as Video; - - if (video != null && video.VideoType == VideoType.Iso && video.IsoType.HasValue && IsoManager.CanMount(video.Path)) + if (state.IsInputVideo && state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath)) { - state.IsoMount = await IsoManager.Mount(video.Path, CancellationToken.None).ConfigureAwait(false); + state.IsoMount = await IsoManager.Mount(state.MediaPath, CancellationToken.None).ConfigureAwait(false); } var process = new Process @@ -652,11 +716,12 @@ namespace MediaBrowser.Api.Playback EnableRaisingEvents = true }; - ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, video != null, state.Request.StartTimeTicks, state.Item.Path, state.Request.DeviceId); + ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, state.IsInputVideo, state.Request.StartTimeTicks, state.MediaPath, state.Request.DeviceId); Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); - var logFilePath = Path.Combine(ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid() + ".txt"); + var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid() + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); @@ -691,13 +756,13 @@ namespace MediaBrowser.Api.Playback } // Allow a small amount of time to buffer a little - if (state.Item is Video) + if (state.IsInputVideo) { await Task.Delay(500).ConfigureAwait(false); } // This is arbitrary, but add a little buffer time when internet streaming - if (state.Item.LocationType == LocationType.Remote) + if (state.IsRemote) { await Task.Delay(4000).ConfigureAwait(false); } @@ -724,11 +789,11 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the user agent param. /// </summary> - /// <param name="item">The item.</param> + /// <param name="path">The path.</param> /// <returns>System.String.</returns> - protected string GetUserAgentParam(BaseItem item) + protected string GetUserAgentParam(string path) { - var useragent = GetUserAgent(item); + var useragent = GetUserAgent(path); if (!string.IsNullOrEmpty(useragent)) { @@ -741,11 +806,16 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the user agent. /// </summary> - /// <param name="item">The item.</param> + /// <param name="path">The path.</param> /// <returns>System.String.</returns> - protected string GetUserAgent(BaseItem item) + protected string GetUserAgent(string path) { - if (item.Path.IndexOf("apple.com", StringComparison.OrdinalIgnoreCase) != -1) + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + + } + if (path.IndexOf("apple.com", StringComparison.OrdinalIgnoreCase) != -1) { return "QuickTime/7.7.4"; } @@ -784,13 +854,10 @@ namespace MediaBrowser.Api.Playback /// Gets the state. /// </summary> /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>StreamState.</returns> - protected StreamState GetState(StreamRequest request) + protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken) { - var item = DtoService.GetItemByDtoId(request.Id); - - var media = (IHasMediaStreams)item; - var url = Request.PathInfo; if (!request.AudioCodec.HasValue) @@ -800,11 +867,78 @@ namespace MediaBrowser.Api.Playback var state = new StreamState { - Item = item, Request = request, - Url = url + RequestedUrl = url }; + BaseItem item; + + if (string.Equals(request.Type, "Recording", StringComparison.OrdinalIgnoreCase)) + { + var recording = await LiveTvManager.GetInternalRecording(request.Id, cancellationToken).ConfigureAwait(false); + + state.VideoType = VideoType.VideoFile; + state.IsInputVideo = string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + state.PlayableStreamFileNames = new List<string>(); + + if (!string.IsNullOrEmpty(recording.RecordingInfo.Path) && File.Exists(recording.RecordingInfo.Path)) + { + state.MediaPath = recording.RecordingInfo.Path; + state.IsRemote = false; + } + else if (!string.IsNullOrEmpty(recording.RecordingInfo.Url)) + { + state.MediaPath = recording.RecordingInfo.Url; + state.IsRemote = true; + } + else + { + state.MediaPath = string.Format("http://localhost:{0}/mediabrowser/LiveTv/Recordings/{1}/Stream", + ServerConfigurationManager.Configuration.HttpServerPortNumber, + request.Id); + + state.IsRemote = true; + } + + item = recording; + } + else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase)) + { + var channel = LiveTvManager.GetInternalChannel(request.Id); + + state.VideoType = VideoType.VideoFile; + state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + state.PlayableStreamFileNames = new List<string>(); + + state.MediaPath = string.Format("http://localhost:{0}/mediabrowser/LiveTv/Channels/{1}/Stream", + ServerConfigurationManager.Configuration.HttpServerPortNumber, + request.Id); + + state.IsRemote = true; + + item = channel; + } + else + { + item = DtoService.GetItemByDtoId(request.Id); + + state.MediaPath = item.Path; + state.IsRemote = item.LocationType == LocationType.Remote; + + var video = item as Video; + + if (video != null) + { + state.IsInputVideo = true; + state.VideoType = video.VideoType; + state.IsoType = video.IsoType; + + state.PlayableStreamFileNames = video.PlayableStreamFileNames == null + ? new List<string>() + : video.PlayableStreamFileNames.ToList(); + } + } + var videoRequest = request as VideoStreamRequest; var mediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery @@ -829,6 +963,8 @@ namespace MediaBrowser.Api.Playback state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); } + state.HasMediaStreams = mediaStreams.Count > 0; + return state; } diff --git a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs index efcc3f07a..d5bf22362 100644 --- a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs @@ -1,8 +1,9 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; @@ -26,8 +27,8 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> public class AudioHlsService : BaseHlsService { - public AudioHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository) + public AudioHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager) { } diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 1e5e8b82d..68342e91d 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -2,10 +2,10 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Common.Net; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls @@ -22,14 +23,14 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> public abstract class BaseHlsService : BaseStreamingService { - protected BaseHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository) + protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager) { } protected override string GetOutputFilePath(StreamState state) { - var folder = ApplicationPaths.EncodedMediaCachePath; + var folder = ServerConfigurationManager.ApplicationPaths.EncodedMediaCachePath; var outputFileExtension = GetOutputFileExtension(state); @@ -73,7 +74,7 @@ namespace MediaBrowser.Api.Playback.Hls /// <returns>System.Object.</returns> protected object ProcessRequest(StreamRequest request) { - var state = GetState(request); + var state = GetState(request, CancellationToken.None).Result; return ProcessRequestAsync(state).Result; } @@ -247,7 +248,7 @@ namespace MediaBrowser.Api.Playback.Hls /// <returns>System.String.</returns> protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions) { - var probeSize = GetProbeSizeArgument(state.Item); + var probeSize = GetProbeSizeArgument(state.MediaPath, state.IsInputVideo, state.VideoType, state.IsoType); var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; @@ -257,13 +258,16 @@ namespace MediaBrowser.Api.Playback.Hls var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds); - var args = string.Format("{0}{1} {2} {3} -i {4}{5} -threads 0 {6} {7} -sc_threshold 0 {8} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{9}\"", + var threads = GetNumberOfThreads(); + + var args = string.Format("{0}{1} {2} {3} -i {4}{5} -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{10}\"", itsOffset, probeSize, - GetUserAgentParam(state.Item), + GetUserAgentParam(state.MediaPath), GetFastSeekCommandLineParameter(state.Request), - GetInputArgument(state.Item, state.IsoMount), + GetInputArgument(state), GetSlowSeekCommandLineParameter(state.Request), + threads, GetMapArgs(state), GetVideoArguments(state, performSubtitleConversions), GetAudioArguments(state), @@ -272,13 +276,14 @@ namespace MediaBrowser.Api.Playback.Hls if (hlsVideoRequest != null) { - if (hlsVideoRequest.AppendBaselineStream && state.Item is Video) + if (hlsVideoRequest.AppendBaselineStream && state.IsInputVideo) { var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8"); var bitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? 64000; - var lowBitrateParams = string.Format(" -threads 0 -vn -codec:a:0 libmp3lame -ac 2 -ab {1} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{0}\"", + var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {2} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{1}\"", + threads, lowBitratePath, bitrate / 2); diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index 02a632694..93e1a06a0 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -145,16 +145,13 @@ namespace MediaBrowser.Api.Playback.Hls } } - private void ExtendPlaylistTimer(string playlist) + private async void ExtendPlaylistTimer(string playlist) { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); - Task.Run(async () => - { - await Task.Delay(20000).ConfigureAwait(false); + await Task.Delay(20000).ConfigureAwait(false); - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); - }); + ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); } } } diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index fe863c862..583082500 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -1,8 +1,9 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.IO; using ServiceStack; @@ -32,8 +33,8 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> public class VideoHlsService : BaseHlsService { - public VideoHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository) + public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager) { } @@ -93,7 +94,7 @@ namespace MediaBrowser.Api.Playback.Hls audioSampleRate = state.Request.AudioSampleRate.Value + ":"; } - args += string.Format(" -af \"adelay=1,aresample={0}async=1000{1}\"", audioSampleRate, volParam); + args += string.Format(" -af \"adelay=1,aresample={0}async=1{1}\"", audioSampleRate, volParam); return args; } @@ -123,13 +124,13 @@ namespace MediaBrowser.Api.Playback.Hls (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 || state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1); - var args = "-codec:v:0 " + codec + " -preset superfast" + keyFrameArg; + var args = "-codec:v:0 " + codec + " " + GetVideoQualityParam(state, "libx264") + keyFrameArg; var bitrate = GetVideoBitrateParam(state); if (bitrate.HasValue) { - args += string.Format(" -b:v {0} -maxrate ({0}*.85) -bufsize {0}", bitrate.Value.ToString(UsCulture)); + args += string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture)); } // Add resolution params, if specified diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 86ab498f6..baf7f48fe 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -1,9 +1,10 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; +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.Model.IO; using ServiceStack; @@ -41,8 +42,8 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> public class AudioService : BaseProgressiveStreamingService { - public AudioService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor, IFileSystem fileSystem) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, itemRepo, dtoService, imageProcessor, fileSystem) + public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IImageProcessor imageProcessor) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, imageProcessor) { } @@ -101,13 +102,16 @@ namespace MediaBrowser.Api.Playback.Progressive const string vn = " -vn"; - return string.Format("{0} -i {1}{2} -threads 0{5} {3} -id3v2_version 3 -write_id3v1 1 \"{4}\"", + var threads = GetNumberOfThreads(); + + return string.Format("{0} -i {1}{2} -threads {3}{4} {5} -id3v2_version 3 -write_id3v1 1 \"{6}\"", GetFastSeekCommandLineParameter(request), - GetInputArgument(state.Item, state.IsoMount), + GetInputArgument(state), GetSlowSeekCommandLineParameter(request), + threads, + vn, string.Join(" ", audioTranscodeParams.ToArray()), - outputPath, - vn).Trim(); + outputPath).Trim(); } } } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 1fea32219..e367801d2 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -1,20 +1,18 @@ -using MediaBrowser.Api.Images; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Common.Net; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Progressive @@ -26,8 +24,8 @@ namespace MediaBrowser.Api.Playback.Progressive { protected readonly IImageProcessor ImageProcessor; - protected BaseProgressiveStreamingService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepository, IDtoService dtoService, IImageProcessor imageProcessor, IFileSystem fileSystem) : - base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository) + protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IImageProcessor imageProcessor) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager) { ImageProcessor = imageProcessor; } @@ -51,9 +49,7 @@ namespace MediaBrowser.Api.Playback.Progressive // Try to infer based on the desired video codec if (videoRequest != null && videoRequest.VideoCodec.HasValue) { - var video = state.Item as Video; - - if (video != null) + if (state.IsInputVideo) { switch (videoRequest.VideoCodec.Value) { @@ -72,9 +68,7 @@ namespace MediaBrowser.Api.Playback.Progressive // Try to infer based on the desired audio codec if (state.Request.AudioCodec.HasValue) { - var audio = state.Item as Audio; - - if (audio != null) + if (!state.IsInputVideo) { switch (state.Request.AudioCodec.Value) { @@ -186,18 +180,13 @@ namespace MediaBrowser.Api.Playback.Progressive /// <returns>Task.</returns> protected object ProcessRequest(StreamRequest request, bool isHeadRequest) { - var state = GetState(request); - - if (request.AlbumArt) - { - return GetAlbumArtResponse(state); - } + var state = GetState(request, CancellationToken.None).Result; var responseHeaders = new Dictionary<string, string>(); - if (request.Static && state.Item.LocationType == LocationType.Remote) + if (request.Static && state.IsRemote) { - return GetStaticRemoteStreamResult(state.Item, responseHeaders, isHeadRequest).Result; + return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result; } var outputPath = GetOutputFilePath(state); @@ -210,7 +199,7 @@ namespace MediaBrowser.Api.Playback.Progressive if (request.Static) { - return ResultFactory.GetStaticFileResult(Request, state.Item.Path, FileShare.Read, responseHeaders, isHeadRequest); + return ResultFactory.GetStaticFileResult(Request, state.MediaPath, FileShare.Read, responseHeaders, isHeadRequest); } if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)) @@ -224,19 +213,19 @@ namespace MediaBrowser.Api.Playback.Progressive /// <summary> /// Gets the static remote stream result. /// </summary> - /// <param name="item">The item.</param> + /// <param name="mediaPath">The media path.</param> /// <param name="responseHeaders">The response headers.</param> /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> /// <returns>Task{System.Object}.</returns> - private async Task<object> GetStaticRemoteStreamResult(BaseItem item, Dictionary<string, string> responseHeaders, bool isHeadRequest) + private async Task<object> GetStaticRemoteStreamResult(string mediaPath, Dictionary<string, string> responseHeaders, bool isHeadRequest) { responseHeaders["Accept-Ranges"] = "none"; var httpClient = new HttpClient(); - using (var message = new HttpRequestMessage(HttpMethod.Get, item.Path)) + using (var message = new HttpRequestMessage(HttpMethod.Get, mediaPath)) { - var useragent = GetUserAgent(item); + var useragent = GetUserAgent(mediaPath); if (!string.IsNullOrEmpty(useragent)) { @@ -273,47 +262,6 @@ namespace MediaBrowser.Api.Playback.Progressive } /// <summary> - /// Gets the album art response. - /// </summary> - /// <param name="state">The state.</param> - /// <returns>System.Object.</returns> - private object GetAlbumArtResponse(StreamState state) - { - var request = new GetItemImage - { - MaxWidth = 800, - MaxHeight = 800, - Type = ImageType.Primary, - Id = state.Item.Id.ToString() - }; - - // Try and find some image to return - if (!state.Item.HasImage(ImageType.Primary)) - { - if (state.Item.HasImage(ImageType.Backdrop)) - { - request.Type = ImageType.Backdrop; - } - else if (state.Item.HasImage(ImageType.Thumb)) - { - request.Type = ImageType.Thumb; - } - else if (state.Item.HasImage(ImageType.Logo)) - { - request.Type = ImageType.Logo; - } - } - - return new ImageService(UserManager, LibraryManager, ApplicationPaths, null, ItemRepository, DtoService, ImageProcessor, null) - { - Logger = Logger, - Request = Request, - ResultFactory = ResultFactory - - }.Get(request); - } - - /// <summary> /// Gets the stream result. /// </summary> /// <param name="state">The state.</param> diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 40c7492ff..f4e4019f6 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -1,10 +1,10 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.IO; using ServiceStack; @@ -55,8 +55,8 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> public class VideoService : BaseProgressiveStreamingService { - public VideoService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor, IFileSystem fileSystem) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, itemRepo, dtoService, imageProcessor, fileSystem) + public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IImageProcessor imageProcessor) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, imageProcessor) { } @@ -89,9 +89,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// <returns>System.String.</returns> protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions) { - var video = (Video)state.Item; - - var probeSize = GetProbeSizeArgument(state.Item); + var probeSize = GetProbeSizeArgument(state.MediaPath, state.IsInputVideo, state.VideoType, state.IsoType); // Get the output codec name var videoCodec = GetVideoCodec(state.VideoRequest); @@ -104,13 +102,13 @@ namespace MediaBrowser.Api.Playback.Progressive format = " -f mp4 -movflags frag_keyframe+empty_moov"; } - var threads = string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase) ? 2 : 0; + var threads = string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase) ? 2 : GetNumberOfThreads(); return string.Format("{0} {1} {2} -i {3}{4}{5} {6} {7} -threads {8} {9}{10} \"{11}\"", probeSize, - GetUserAgentParam(state.Item), + GetUserAgentParam(state.MediaPath), GetFastSeekCommandLineParameter(state.Request), - GetInputArgument(video, state.IsoMount), + GetInputArgument(state), GetSlowSeekCommandLineParameter(state.Request), keyFrame, GetMapArgs(state), @@ -165,9 +163,16 @@ namespace MediaBrowser.Api.Playback.Progressive var qualityParam = GetVideoQualityParam(state, codec); + var bitrate = GetVideoBitrateParam(state); + + if (bitrate.HasValue) + { + qualityParam += string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); + } + if (!string.IsNullOrEmpty(qualityParam)) { - args += " " + qualityParam; + args += " " + qualityParam.Trim(); } args += " -vsync vfr"; @@ -213,9 +218,9 @@ namespace MediaBrowser.Api.Playback.Progressive { return "-acodec copy"; } - + var args = "-acodec " + codec; - + // Add the number of audio channels var channels = GetNumAudioChannelsParam(request, state.AudioStream); @@ -231,64 +236,23 @@ namespace MediaBrowser.Api.Playback.Progressive args += " -ab " + bitrate.Value.ToString(UsCulture); } - var volParam = string.Empty; - var AudioSampleRate = string.Empty; - - // Boost volume to 200% when downsampling from 6ch to 2ch - if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5) - { - volParam = ",volume=2.000000"; - } - - if (state.Request.AudioSampleRate.HasValue) - { - AudioSampleRate= state.Request.AudioSampleRate.Value + ":"; - } - - args += string.Format(" -af \"aresample={0}async=1000{1}\"",AudioSampleRate, volParam); - - return args; - } - - /// <summary> - /// Gets the video bitrate to specify on the command line - /// </summary> - /// <param name="state">The state.</param> - /// <param name="videoCodec">The video codec.</param> - /// <returns>System.String.</returns> - private string GetVideoQualityParam(StreamState state, string videoCodec) - { - var args = string.Empty; + var volParam = string.Empty; + var AudioSampleRate = string.Empty; - // webm - if (videoCodec.Equals("libvpx", StringComparison.OrdinalIgnoreCase)) + // Boost volume to 200% when downsampling from 6ch to 2ch + if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5) { - args = "-speed 16 -quality good -profile:v 0 -slices 8"; + volParam = ",volume=2.000000"; } - // asf/wmv - else if (videoCodec.Equals("wmv2", StringComparison.OrdinalIgnoreCase)) + if (state.Request.AudioSampleRate.HasValue) { - args = "-g 100 -qmax 15"; + AudioSampleRate = state.Request.AudioSampleRate.Value + ":"; } - else if (videoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase)) - { - args = "-preset superfast"; - } - else if (videoCodec.Equals("mpeg4", StringComparison.OrdinalIgnoreCase)) - { - args = "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; - } - - var bitrate = GetVideoBitrateParam(state); - - if (bitrate.HasValue) - { - args += string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); - } + args += string.Format(" -af \"aresample={0}async=1{1}\"", AudioSampleRate, volParam); - return args.Trim(); + return args; } } } diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 1486c0de7..454cc411c 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -65,6 +65,12 @@ namespace MediaBrowser.Api.Playback /// No need to put this in api docs since it's dlna only /// </summary> public bool AlbumArt { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } } public class VideoStreamRequest : StreamRequest diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 3c2ea5a13..be1ad85eb 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -1,14 +1,13 @@ -using System.IO; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using System.Collections.Generic; +using System.IO; namespace MediaBrowser.Api.Playback { public class StreamState { - public string Url { get; set; } + public string RequestedUrl { get; set; } public StreamRequest Request { get; set; } @@ -29,12 +28,24 @@ namespace MediaBrowser.Api.Playback public MediaStream SubtitleStream { get; set; } - public BaseItem Item { get; set; } - /// <summary> /// Gets or sets the iso mount. /// </summary> /// <value>The iso mount.</value> public IIsoMount IsoMount { get; set; } + + public string MediaPath { get; set; } + + public bool IsRemote { get; set; } + + public bool IsInputVideo { get; set; } + + public VideoType VideoType { get; set; } + + public IsoType? IsoType { get; set; } + + public List<string> PlayableStreamFileNames { get; set; } + + public bool HasMediaStreams { get; set; } } } diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index 25e22ab59..78ff1bc07 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -148,12 +148,13 @@ namespace MediaBrowser.Api MediaType = item.MediaType, MatchedTerm = hintInfo.MatchedTerm, DisplayMediaType = item.DisplayMediaType, - RunTimeTicks = item.RunTimeTicks + RunTimeTicks = item.RunTimeTicks, + ProductionYear = item.ProductionYear }; if (item.HasImage(ImageType.Primary)) { - result.PrimaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary, item.GetImage(ImageType.Primary)); + result.PrimaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary, item.GetImagePath(ImageType.Primary)); } var episode = item as Episode; diff --git a/MediaBrowser.Api/SystemService.cs b/MediaBrowser.Api/SystemService.cs index dae603dc8..23d64e89b 100644 --- a/MediaBrowser.Api/SystemService.cs +++ b/MediaBrowser.Api/SystemService.cs @@ -124,9 +124,11 @@ namespace MediaBrowser.Api /// <returns>System.Object.</returns> public object Get(GetConfiguration request) { - var dateModified = _fileSystem.GetLastWriteTimeUtc(_configurationManager.ApplicationPaths.SystemConfigurationFilePath); + var configPath = _configurationManager.ApplicationPaths.SystemConfigurationFilePath; - var cacheKey = (_configurationManager.ApplicationPaths.SystemConfigurationFilePath + dateModified.Ticks).GetMD5(); + var dateModified = _fileSystem.GetLastWriteTimeUtc(configPath); + + var cacheKey = (configPath + dateModified.Ticks).GetMD5(); return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => _configurationManager.Configuration); } diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index b45d4dfb0..9521f82cc 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -1,4 +1,5 @@ using MediaBrowser.Api.UserLibrary; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -192,18 +193,6 @@ namespace MediaBrowser.Api /// <returns>System.Object.</returns> public object Get(GetNextUpEpisodes request) { - var result = GetNextUpEpisodeItemsResult(request); - - return ToOptimizedResult(result); - } - - /// <summary> - /// Gets the next up episodes. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>Task{ItemsResult}.</returns> - private ItemsResult GetNextUpEpisodeItemsResult(GetNextUpEpisodes request) - { var user = _userManager.GetUserById(request.UserId); var itemsList = GetNextUpEpisodes(request) @@ -215,11 +204,13 @@ namespace MediaBrowser.Api var returnItems = pagedItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray(); - return new ItemsResult + var result = new ItemsResult { TotalRecordCount = itemsList.Count, Items = returnItems }; + + return ToOptimizedResult(result); } public IEnumerable<Episode> GetNextUpEpisodes(GetNextUpEpisodes request) @@ -273,14 +264,12 @@ namespace MediaBrowser.Api /// <returns>Task{Episode}.</returns> private Tuple<Episode, DateTime> GetNextUp(Series series, User user, GetNextUpEpisodes request) { - var allEpisodes = series.GetRecursiveChildren(user) - .OfType<Episode>() - .OrderByDescending(i => i.PremiereDate ?? DateTime.MinValue) - .ThenByDescending(i => i.IndexNumber ?? 0) + // Get them in display order, then reverse + var allEpisodes = series.GetSeasons(user, true, true) + .SelectMany(i => i.GetEpisodes(user, true, true)) + .Reverse() .ToList(); - allEpisodes = FilterItems(request, allEpisodes).ToList(); - Episode lastWatched = null; var lastWatchedDate = DateTime.MinValue; Episode nextUp = null; @@ -302,7 +291,10 @@ namespace MediaBrowser.Api } else { - nextUp = episode; + if (episode.LocationType != LocationType.Virtual) + { + nextUp = episode; + } } } @@ -314,15 +306,6 @@ namespace MediaBrowser.Api return new Tuple<Episode, DateTime>(null, lastWatchedDate); } - - private IEnumerable<Episode> FilterItems(GetNextUpEpisodes request, IEnumerable<Episode> items) - { - // Make this configurable when needed - items = items.Where(i => i.LocationType != LocationType.Virtual); - - return items; - } - private IEnumerable<Series> FilterSeries(GetNextUpEpisodes request, IEnumerable<Series> items) { if (!string.IsNullOrWhiteSpace(request.SeriesId)) @@ -364,12 +347,12 @@ namespace MediaBrowser.Api var series = _libraryManager.GetItemById(request.Id) as Series; - var fields = request.GetItemFields().ToList(); - - var seasons = series.GetChildren(user, true) - .OfType<Season>(); + if (series == null) + { + throw new ResourceNotFoundException("No series exists with Id " + request.Id); + } - var sortOrder = ItemSortBy.SortName; + var seasons = series.GetSeasons(user); if (request.IsSpecialSeason.HasValue) { @@ -378,29 +361,8 @@ namespace MediaBrowser.Api seasons = seasons.Where(i => i.IsSpecialSeason == val); } - var config = user.Configuration; - - if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes) - { - seasons = seasons.Where(i => !i.IsMissingOrVirtualUnaired); - } - else - { - if (!config.DisplayMissingEpisodes) - { - seasons = seasons.Where(i => !i.IsMissingSeason); - } - if (!config.DisplayUnairedEpisodes) - { - seasons = seasons.Where(i => !i.IsVirtualUnaired); - } - } - seasons = FilterVirtualSeasons(request, seasons); - seasons = _libraryManager.Sort(seasons, user, new[] { sortOrder }, SortOrder.Ascending) - .Cast<Season>(); - // This must be the last filter if (!string.IsNullOrEmpty(request.AdjacentTo)) { @@ -408,6 +370,8 @@ namespace MediaBrowser.Api .Cast<Season>(); } + var fields = request.GetItemFields().ToList(); + var returnItems = seasons.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) .ToArray(); @@ -450,66 +414,45 @@ namespace MediaBrowser.Api { var user = _userManager.GetUserById(request.UserId); - var series = _libraryManager.GetItemById(request.Id) as Series; - - var fields = request.GetItemFields().ToList(); - - var episodes = series.GetRecursiveChildren(user) - .OfType<Episode>(); + IEnumerable<Episode> episodes; - var sortOrder = ItemSortBy.SortName; - - if (!string.IsNullOrEmpty(request.SeasonId)) + if (string.IsNullOrEmpty(request.SeasonId)) { - var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season; + var series = _libraryManager.GetItemById(request.Id) as Series; - if (season.IndexNumber.HasValue) + if (series == null) { - episodes = FilterEpisodesBySeason(episodes, season.IndexNumber.Value, true); - - sortOrder = ItemSortBy.AiredEpisodeOrder; + throw new ResourceNotFoundException("No series exists with Id " + request.Id); } - else - { - episodes = season.RecursiveChildren.OfType<Episode>(); - sortOrder = ItemSortBy.SortName; - } + episodes = series.GetEpisodes(user, request.Season.Value); } - - else if (request.Season.HasValue) + else { - episodes = FilterEpisodesBySeason(episodes, request.Season.Value, true); - - sortOrder = ItemSortBy.AiredEpisodeOrder; - } - - var config = user.Configuration; + var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season; - if (!config.DisplayMissingEpisodes) - { - episodes = episodes.Where(i => !i.IsMissingEpisode); - } - if (!config.DisplayUnairedEpisodes) - { - episodes = episodes.Where(i => !i.IsVirtualUnaired); + if (season == null) + { + throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId); + } + + episodes = season.GetEpisodes(user); } + // Filter after the fact in case the ui doesn't want them if (request.IsMissing.HasValue) { var val = request.IsMissing.Value; episodes = episodes.Where(i => i.IsMissingEpisode == val); } + // Filter after the fact in case the ui doesn't want them if (request.IsVirtualUnaired.HasValue) { var val = request.IsVirtualUnaired.Value; episodes = episodes.Where(i => i.IsVirtualUnaired == val); } - episodes = _libraryManager.Sort(episodes, user, new[] { sortOrder }, SortOrder.Ascending) - .Cast<Episode>(); - // This must be the last filter if (!string.IsNullOrEmpty(request.AdjacentTo)) { @@ -517,6 +460,8 @@ namespace MediaBrowser.Api .Cast<Episode>(); } + var fields = request.GetItemFields().ToList(); + var returnItems = episodes.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) .ToArray(); @@ -526,27 +471,5 @@ namespace MediaBrowser.Api Items = returnItems }; } - - internal static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials) - { - if (!includeSpecials || seasonNumber < 1) - { - return episodes.Where(i => (i.PhysicalSeasonNumber ?? -1) == seasonNumber); - } - - return episodes.Where(i => - { - var episode = i; - - if (episode != null) - { - var currentSeasonNumber = episode.AiredSeasonNumber; - - return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber; - } - - return false; - }); - } } } diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 6fcff545f..8ea225186 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -1,6 +1,4 @@ -using System.Globalization; -using System.IO; -using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -13,6 +11,8 @@ using MediaBrowser.Model.Querying; using ServiceStack; using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; namespace MediaBrowser.Api.UserLibrary @@ -53,12 +53,6 @@ namespace MediaBrowser.Api.UserLibrary public string SearchTerm { get; set; } /// <summary> - /// The dynamic, localized index function name - /// </summary> - /// <value>The index by.</value> - public string IndexBy { get; set; } - - /// <summary> /// Limit results to items containing specific genres /// </summary> /// <value>The genres.</value> @@ -358,7 +352,7 @@ namespace MediaBrowser.Api.UserLibrary } else { - items = ((Folder)item).GetChildren(user, true, request.IndexBy); + items = ((Folder)item).GetChildren(user, true); } if (request.IncludeIndexContainers) @@ -1041,7 +1035,7 @@ namespace MediaBrowser.Api.UserLibrary if (request.AiredDuringSeason.HasValue) { - items = TvShowsService.FilterEpisodesBySeason(items.OfType<Episode>(), request.AiredDuringSeason.Value, true); + items = Series.FilterEpisodesBySeason(items.OfType<Episode>(), request.AiredDuringSeason.Value, true); } if (!string.IsNullOrEmpty(request.MinPremiereDate)) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 3b7808ba7..254fa6ff1 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -5,11 +5,11 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using ServiceStack; +using ServiceStack.Text.Controller; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using ServiceStack.Text.Controller; namespace MediaBrowser.Api { @@ -269,13 +269,15 @@ namespace MediaBrowser.Api /// Posts the specified request. /// </summary> /// <param name="request">The request.</param> - public async Task Post(AuthenticateUser request) + public object Post(AuthenticateUser request) { // No response needed. Will throw an exception on failure. - await AuthenticateUser(request).ConfigureAwait(false); + var result = AuthenticateUser(request).Result; + + return result; } - public async Task<object> Post(AuthenticateUserByName request) + public object Post(AuthenticateUserByName request) { var user = _userManager.Users.FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase)); @@ -284,7 +286,7 @@ namespace MediaBrowser.Api throw new ArgumentException(string.Format("User {0} not found.", request.Username)); } - var result = await AuthenticateUser(new AuthenticateUser { Id = user.Id, Password = request.Password }).ConfigureAwait(false); + var result = AuthenticateUser(new AuthenticateUser { Id = user.Id, Password = request.Password }).Result; return ToOptimizedResult(result); } diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs index bfbbc06ee..929745a94 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs @@ -9,6 +9,7 @@ using MediaBrowser.Common.Implementations.Updates; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.Security; using MediaBrowser.Common.Updates; @@ -180,42 +181,50 @@ namespace MediaBrowser.Common.Implementations /// Inits this instance. /// </summary> /// <returns>Task.</returns> - public virtual async Task Init() + public virtual async Task Init(IProgress<double> progress) { - // https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.IntegrationTests/Web.config#L4 - Licensing.RegisterLicense("1001-e1JlZjoxMDAxLE5hbWU6VGVzdCBCdXNpbmVzcyxUeXBlOkJ1c2luZXNzLEhhc2g6UHVNTVRPclhvT2ZIbjQ5MG5LZE1mUTd5RUMzQnBucTFEbTE3TDczVEF4QUNMT1FhNXJMOWkzVjFGL2ZkVTE3Q2pDNENqTkQyUktRWmhvUVBhYTBiekJGUUZ3ZE5aZHFDYm9hL3lydGlwUHI5K1JsaTBYbzNsUC85cjVJNHE5QVhldDN6QkE4aTlvdldrdTgyTk1relY2eis2dFFqTThYN2lmc0JveHgycFdjPSxFeHBpcnk6MjAxMy0wMS0wMX0="); - + try + { + // https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.IntegrationTests/Web.config#L4 + Licensing.RegisterLicense("1001-e1JlZjoxMDAxLE5hbWU6VGVzdCBCdXNpbmVzcyxUeXBlOkJ1c2luZXNzLEhhc2g6UHVNTVRPclhvT2ZIbjQ5MG5LZE1mUTd5RUMzQnBucTFEbTE3TDczVEF4QUNMT1FhNXJMOWkzVjFGL2ZkVTE3Q2pDNENqTkQyUktRWmhvUVBhYTBiekJGUUZ3ZE5aZHFDYm9hL3lydGlwUHI5K1JsaTBYbzNsUC85cjVJNHE5QVhldDN6QkE4aTlvdldrdTgyTk1relY2eis2dFFqTThYN2lmc0JveHgycFdjPSxFeHBpcnk6MjAxMy0wMS0wMX0="); + } + catch + { + // Failing under mono + } + progress.Report(1); + JsonSerializer = CreateJsonSerializer(); IsFirstRun = !ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted; + progress.Report(2); Logger = LogManager.GetLogger("App"); LogManager.LogSeverity = ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging ? LogSeverity.Debug : LogSeverity.Info; - - OnLoggerLoaded(); + progress.Report(3); DiscoverTypes(); + progress.Report(14); Logger.Info("Version {0} initializing", ApplicationVersion); SetHttpLimit(); + progress.Report(15); - await RegisterResources().ConfigureAwait(false); + var innerProgress = new ActionableProgress<double>(); + innerProgress.RegisterAction(p => progress.Report((.8 * p) + 15)); + + await RegisterResources(innerProgress).ConfigureAwait(false); FindParts(); + progress.Report(95); await InstallIsoMounters(CancellationToken.None).ConfigureAwait(false); - } - - /// <summary> - /// Called when [logger loaded]. - /// </summary> - protected virtual void OnLoggerLoaded() - { + progress.Report(100); } protected virtual IJsonSerializer CreateJsonSerializer() @@ -341,7 +350,7 @@ namespace MediaBrowser.Common.Implementations /// Registers resources that classes will depend on /// </summary> /// <returns>Task.</returns> - protected virtual Task RegisterResources() + protected virtual Task RegisterResources(IProgress<double> progress) { return Task.Run(() => { diff --git a/MediaBrowser.Common.Implementations/BaseApplicationPaths.cs b/MediaBrowser.Common.Implementations/BaseApplicationPaths.cs index 6acaac5c9..668b1395d 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationPaths.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationPaths.cs @@ -2,7 +2,6 @@ using System; using System.Configuration; using System.IO; -using System.Reflection; namespace MediaBrowser.Common.Implementations { @@ -82,10 +81,6 @@ namespace MediaBrowser.Common.Implementations } /// <summary> - /// The _image cache path - /// </summary> - private string _imageCachePath; - /// <summary> /// Gets the image cache path. /// </summary> /// <value>The image cache path.</value> @@ -93,22 +88,11 @@ namespace MediaBrowser.Common.Implementations { get { - if (_imageCachePath == null) - { - _imageCachePath = Path.Combine(CachePath, "images"); - - Directory.CreateDirectory(_imageCachePath); - } - - return _imageCachePath; + return Path.Combine(CachePath, "images"); } } /// <summary> - /// The _plugins path - /// </summary> - private string _pluginsPath; - /// <summary> /// Gets the path to the plugin directory /// </summary> /// <value>The plugins path.</value> @@ -116,21 +100,11 @@ namespace MediaBrowser.Common.Implementations { get { - if (_pluginsPath == null) - { - _pluginsPath = Path.Combine(ProgramDataPath, "plugins"); - Directory.CreateDirectory(_pluginsPath); - } - - return _pluginsPath; + return Path.Combine(ProgramDataPath, "plugins"); } } /// <summary> - /// The _plugin configurations path - /// </summary> - private string _pluginConfigurationsPath; - /// <summary> /// Gets the path to the plugin configurations directory /// </summary> /// <value>The plugin configurations path.</value> @@ -138,17 +112,10 @@ namespace MediaBrowser.Common.Implementations { get { - if (_pluginConfigurationsPath == null) - { - _pluginConfigurationsPath = Path.Combine(PluginsPath, "configurations"); - Directory.CreateDirectory(_pluginConfigurationsPath); - } - - return _pluginConfigurationsPath; + return Path.Combine(PluginsPath, "configurations"); } } - private string _tempUpdatePath; /// <summary> /// Gets the path to where temporary update files will be stored /// </summary> @@ -157,21 +124,11 @@ namespace MediaBrowser.Common.Implementations { get { - if (_tempUpdatePath == null) - { - _tempUpdatePath = Path.Combine(ProgramDataPath, "updates"); - Directory.CreateDirectory(_tempUpdatePath); - } - - return _tempUpdatePath; + return Path.Combine(ProgramDataPath, "updates"); } } /// <summary> - /// The _log directory path - /// </summary> - private string _logDirectoryPath; - /// <summary> /// Gets the path to the log directory /// </summary> /// <value>The log directory path.</value> @@ -179,20 +136,11 @@ namespace MediaBrowser.Common.Implementations { get { - if (_logDirectoryPath == null) - { - _logDirectoryPath = Path.Combine(ProgramDataPath, "logs"); - Directory.CreateDirectory(_logDirectoryPath); - } - return _logDirectoryPath; + return Path.Combine(ProgramDataPath, "logs"); } } /// <summary> - /// The _configuration directory path - /// </summary> - private string _configurationDirectoryPath; - /// <summary> /// Gets the path to the application configuration root directory /// </summary> /// <value>The configuration directory path.</value> @@ -200,12 +148,7 @@ namespace MediaBrowser.Common.Implementations { get { - if (_configurationDirectoryPath == null) - { - _configurationDirectoryPath = Path.Combine(ProgramDataPath, "config"); - Directory.CreateDirectory(_configurationDirectoryPath); - } - return _configurationDirectoryPath; + return Path.Combine(ProgramDataPath, "config"); } } @@ -233,7 +176,7 @@ namespace MediaBrowser.Common.Implementations { get { - if (_cachePath == null) + if (string.IsNullOrEmpty(_cachePath)) { _cachePath = Path.Combine(ProgramDataPath, "cache"); @@ -242,13 +185,13 @@ namespace MediaBrowser.Common.Implementations return _cachePath; } + set + { + _cachePath = value; + } } /// <summary> - /// The _temp directory - /// </summary> - private string _tempDirectory; - /// <summary> /// Gets the folder path to the temp directory within the cache folder /// </summary> /// <value>The temp directory.</value> @@ -256,14 +199,7 @@ namespace MediaBrowser.Common.Implementations { get { - if (_tempDirectory == null) - { - _tempDirectory = Path.Combine(CachePath, "temp"); - - Directory.CreateDirectory(_tempDirectory); - } - - return _tempDirectory; + return Path.Combine(CachePath, "temp"); } } @@ -273,7 +209,7 @@ namespace MediaBrowser.Common.Implementations /// <returns>System.String.</returns> private string GetProgramDataPath() { - var programDataPath = _useDebugPath ? ConfigurationManager.AppSettings["DebugProgramDataPath"] : Path.Combine(ConfigurationManager.AppSettings["ReleaseProgramDataPath"], ConfigurationManager.AppSettings["ProgramDataFolderName"]); + var programDataPath = _useDebugPath ? ConfigurationManager.AppSettings["DebugProgramDataPath"] : ConfigurationManager.AppSettings["ReleaseProgramDataPath"]; programDataPath = programDataPath.Replace("%ApplicationData%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); diff --git a/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs b/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs index 317a288ff..8c4840ea7 100644 --- a/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs +++ b/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Configuration; +using System.IO; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Logging; @@ -84,6 +85,8 @@ namespace MediaBrowser.Common.Implementations.Configuration CommonApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; Logger = logManager.GetLogger(GetType().Name); + + UpdateCachePath(); } /// <summary> @@ -96,9 +99,13 @@ namespace MediaBrowser.Common.Implementations.Configuration /// </summary> public void SaveConfiguration() { + var path = CommonApplicationPaths.SystemConfigurationFilePath; + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + lock (_configurationSaveLock) { - XmlSerializer.SerializeToFile(CommonConfiguration, CommonApplicationPaths.SystemConfigurationFilePath); + XmlSerializer.SerializeToFile(CommonConfiguration, path); } OnConfigurationUpdated(); @@ -109,6 +116,8 @@ namespace MediaBrowser.Common.Implementations.Configuration /// </summary> protected virtual void OnConfigurationUpdated() { + UpdateCachePath(); + EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger); } @@ -124,8 +133,40 @@ namespace MediaBrowser.Common.Implementations.Configuration throw new ArgumentNullException("newConfiguration"); } + ValidateCachePath(newConfiguration); + CommonConfiguration = newConfiguration; SaveConfiguration(); } + + /// <summary> + /// Updates the items by name path. + /// </summary> + private void UpdateCachePath() + { + ((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ? + null : + CommonConfiguration.CachePath; + } + + /// <summary> + /// Replaces the cache path. + /// </summary> + /// <param name="newConfig">The new configuration.</param> + /// <exception cref="System.IO.DirectoryNotFoundException"></exception> + private void ValidateCachePath(BaseApplicationConfiguration newConfig) + { + var newPath = newConfig.CachePath; + + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath)) + { + // Validate + if (!Directory.Exists(newPath)) + { + throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); + } + } + } } } diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs index 19091885d..a5b241b4b 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -64,6 +64,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager _logger = logger; _fileSystem = fileSystem; _appPaths = appPaths; + + // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c + ServicePointManager.Expect100Continue = false; } /// <summary> @@ -132,7 +135,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager #if __MonoCS__ return GetMonoRequest(options, method, enableHttpCompression); #endif - + var request = HttpWebRequest.CreateHttp(options.Url); if (!string.IsNullOrEmpty(options.AcceptHeader)) @@ -172,9 +175,64 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager /// <returns>Task{HttpResponseInfo}.</returns> /// <exception cref="HttpException"> /// </exception> - public async Task<HttpResponseInfo> GetResponse(HttpRequestOptions options) + public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options) + { + return SendAsync(options, "GET"); + } + + /// <summary> + /// Performs a GET request and returns the resulting stream + /// </summary> + /// <param name="options">The options.</param> + /// <returns>Task{Stream}.</returns> + /// <exception cref="HttpException"></exception> + /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception> + public async Task<Stream> Get(HttpRequestOptions options) + { + var response = await GetResponse(options).ConfigureAwait(false); + + return response.Content; + } + + /// <summary> + /// Performs a GET request and returns the resulting stream + /// </summary> + /// <param name="url">The URL.</param> + /// <param name="resourcePool">The resource pool.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + public Task<Stream> Get(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken) + { + return Get(new HttpRequestOptions + { + Url = url, + ResourcePool = resourcePool, + CancellationToken = cancellationToken, + }); + } + + /// <summary> + /// Gets the specified URL. + /// </summary> + /// <param name="url">The URL.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + public Task<Stream> Get(string url, CancellationToken cancellationToken) + { + return Get(url, null, cancellationToken); + } + + /// <summary> + /// send as an asynchronous operation. + /// </summary> + /// <param name="options">The options.</param> + /// <param name="httpMethod">The HTTP method.</param> + /// <returns>Task{HttpResponseInfo}.</returns> + /// <exception cref="HttpException"> + /// </exception> + private async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod) { - ValidateParams(options.Url, options.CancellationToken); + ValidateParams(options); options.CancellationToken.ThrowIfCancellationRequested(); @@ -185,7 +243,17 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) { IsTimedOut = true }; } - var httpWebRequest = GetRequest(options, "GET", options.EnableHttpCompression); + var httpWebRequest = GetRequest(options, httpMethod, options.EnableHttpCompression); + + if (!string.IsNullOrEmpty(options.RequestContent) || string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase)) + { + var content = options.RequestContent ?? string.Empty; + var bytes = Encoding.UTF8.GetBytes(content); + + httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded"; + httpWebRequest.ContentLength = bytes.Length; + httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length); + } if (options.ResourcePool != null) { @@ -202,7 +270,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true }; } - _logger.Info("HttpClientManager.GET url: {0}", options.Url); + _logger.Info("HttpClientManager {0}: {1}", httpMethod.ToUpper(), options.Url); try { @@ -275,46 +343,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } } - /// <summary> - /// Performs a GET request and returns the resulting stream - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{Stream}.</returns> - /// <exception cref="HttpException"></exception> - /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception> - public async Task<Stream> Get(HttpRequestOptions options) + public Task<HttpResponseInfo> Post(HttpRequestOptions options) { - var response = await GetResponse(options).ConfigureAwait(false); - - return response.Content; - } - - /// <summary> - /// Performs a GET request and returns the resulting stream - /// </summary> - /// <param name="url">The URL.</param> - /// <param name="resourcePool">The resource pool.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{Stream}.</returns> - public Task<Stream> Get(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken) - { - return Get(new HttpRequestOptions - { - Url = url, - ResourcePool = resourcePool, - CancellationToken = cancellationToken, - }); - } - - /// <summary> - /// Gets the specified URL. - /// </summary> - /// <param name="url">The URL.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{Stream}.</returns> - public Task<Stream> Get(string url, CancellationToken cancellationToken) - { - return Get(url, null, cancellationToken); + return SendAsync(options, "POST"); } /// <summary> @@ -329,82 +360,15 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception> public async Task<Stream> Post(HttpRequestOptions options, Dictionary<string, string> postData) { - ValidateParams(options.Url, options.CancellationToken); - - options.CancellationToken.ThrowIfCancellationRequested(); - - var httpWebRequest = GetRequest(options, "POST", options.EnableHttpCompression); - var strings = postData.Keys.Select(key => string.Format("{0}={1}", key, postData[key])); var postContent = string.Join("&", strings.ToArray()); - var bytes = Encoding.UTF8.GetBytes(postContent); - - httpWebRequest.ContentType = "application/x-www-form-urlencoded"; - httpWebRequest.ContentLength = bytes.Length; - httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length); - - if (options.ResourcePool != null) - { - await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); - } - - _logger.Info("HttpClientManager.POST url: {0}", options.Url); - try - { - options.CancellationToken.ThrowIfCancellationRequested(); + options.RequestContent = postContent; + options.RequestContentType = "application/x-www-form-urlencoded"; - using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false)) - { - var httpResponse = (HttpWebResponse)response; - - EnsureSuccessStatusCode(httpResponse); - - options.CancellationToken.ThrowIfCancellationRequested(); - - using (var stream = httpResponse.GetResponseStream()) - { - var memoryStream = new MemoryStream(); - - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - - memoryStream.Position = 0; + var response = await Post(options).ConfigureAwait(false); - return memoryStream; - } - } - } - catch (OperationCanceledException ex) - { - var exception = GetCancellationException(options.Url, options.CancellationToken, ex); - - throw exception; - } - catch (HttpRequestException ex) - { - _logger.ErrorException("Error getting response from " + options.Url, ex); - - throw new HttpException(ex.Message, ex); - } - catch (WebException ex) - { - _logger.ErrorException("Error getting response from " + options.Url, ex); - - throw new HttpException(ex.Message, ex); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting response from " + options.Url, ex); - - throw; - } - finally - { - if (options.ResourcePool != null) - { - options.ResourcePool.Release(); - } - } + return response.Content; } /// <summary> @@ -443,7 +407,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options) { - ValidateParams(options.Url, options.CancellationToken); + ValidateParams(options); + + Directory.CreateDirectory(_appPaths.TempDirectory); var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp"); @@ -590,7 +556,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager { return new HttpException(ex.Message, ex); } - + return ex; } @@ -606,17 +572,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } } - /// <summary> - /// Validates the params. - /// </summary> - /// <param name="url">The URL.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <exception cref="System.ArgumentNullException">url</exception> - private void ValidateParams(string url, CancellationToken cancellationToken) + private void ValidateParams(HttpRequestOptions options) { - if (string.IsNullOrEmpty(url)) + if (string.IsNullOrEmpty(options.Url)) { - throw new ArgumentNullException("url"); + throw new ArgumentNullException("options"); } } diff --git a/MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs b/MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs index ed9baf3b2..616981d50 100644 --- a/MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs +++ b/MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs @@ -216,6 +216,48 @@ namespace MediaBrowser.Common.Implementations.IO return new FileStream(path, mode, access, share); } + + /// <summary> + /// Swaps the files. + /// </summary> + /// <param name="file1">The file1.</param> + /// <param name="file2">The file2.</param> + public void SwapFiles(string file1, string file2) + { + var temp1 = Path.GetTempFileName(); + var temp2 = Path.GetTempFileName(); + + // Copying over will fail against hidden files + RemoveHiddenAttribute(file1); + RemoveHiddenAttribute(file2); + + File.Copy(file1, temp1, true); + File.Copy(file2, temp2, true); + + File.Copy(temp1, file2, true); + File.Copy(temp2, file1, true); + + File.Delete(temp1); + File.Delete(temp2); + } + + /// <summary> + /// Removes the hidden attribute. + /// </summary> + /// <param name="path">The path.</param> + private void RemoveHiddenAttribute(string path) + { + var currentFile = new FileInfo(path); + + // This will fail if the file is hidden + if (currentFile.Exists) + { + if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + { + currentFile.Attributes &= ~FileAttributes.Hidden; + } + } + } } /// <summary> diff --git a/MediaBrowser.Common.Implementations/Logging/NlogManager.cs b/MediaBrowser.Common.Implementations/Logging/NlogManager.cs index 56f2b5e29..fb7fd1698 100644 --- a/MediaBrowser.Common.Implementations/Logging/NlogManager.cs +++ b/MediaBrowser.Common.Implementations/Logging/NlogManager.cs @@ -186,6 +186,8 @@ namespace MediaBrowser.Common.Implementations.Logging { LogFilePath = Path.Combine(LogDirectory, LogFilePrefix + "-" + decimal.Round(DateTime.Now.Ticks / 10000000) + ".log"); + Directory.CreateDirectory(Path.GetDirectoryName(LogFilePath)); + AddFileTarget(LogFilePath, level); LogSeverity = level; diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index 7d75cfd3d..66567fc16 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Common.Implementations</RootNamespace> <AssemblyName>MediaBrowser.Common.Implementations</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -24,6 +24,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -32,18 +33,23 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release Mono\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup> <RunPostBuildEvent>Always</RunPostBuildEvent> </PropertyGroup> <ItemGroup> - <Reference Include="ServiceStack.Text, Version=3.9.70.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Text.dll</HintPath> - </Reference> - <Reference Include="SharpCompress, Version=0.10.2.0, Culture=neutral, PublicKeyToken=beaf6f427e128133, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\sharpcompress.0.10.2\lib\net40\SharpCompress.dll</HintPath> + <Reference Include="SimpleInjector.Diagnostics"> + <HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Configuration" /> @@ -55,8 +61,14 @@ <Reference Include="NLog"> <HintPath>..\packages\NLog.2.1.0\lib\net45\NLog.dll</HintPath> </Reference> + <Reference Include="SharpCompress"> + <HintPath>..\packages\sharpcompress.0.10.2\lib\net40\SharpCompress.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Text"> + <HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath> + </Reference> <Reference Include="SimpleInjector"> - <HintPath>..\packages\SimpleInjector.2.3.6\lib\net40-client\SimpleInjector.dll</HintPath> + <HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.dll</HintPath> </Reference> </ItemGroup> <ItemGroup> @@ -106,9 +118,9 @@ </ItemGroup> <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> + <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <PropertyGroup> - <PostBuildEvent>if $(ConfigurationName) == Release ( + <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' ( xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i )</PostBuildEvent> </PropertyGroup> diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 2406d0470..477dc4aee 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks { LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () => { - var path = GetHistoryFilePath(false); + var path = GetHistoryFilePath(); try { @@ -432,43 +432,28 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <summary> /// Gets the scheduled tasks configuration directory. /// </summary> - /// <param name="create">if set to <c>true</c> [create].</param> /// <returns>System.String.</returns> - private string GetScheduledTasksConfigurationDirectory(bool create) + private string GetScheduledTasksConfigurationDirectory() { - var path = Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); - - if (create) - { - Directory.CreateDirectory(path); - } - - return path; + return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); } /// <summary> /// Gets the scheduled tasks data directory. /// </summary> - /// <param name="create">if set to <c>true</c> [create].</param> /// <returns>System.String.</returns> - private string GetScheduledTasksDataDirectory(bool create) + private string GetScheduledTasksDataDirectory() { - var path = Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks"); - - if (create) - { - Directory.CreateDirectory(path); - } - return path; + return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks"); } /// <summary> /// Gets the history file path. /// </summary> /// <value>The history file path.</value> - private string GetHistoryFilePath(bool createDirectory) + private string GetHistoryFilePath() { - return Path.Combine(GetScheduledTasksDataDirectory(createDirectory), Id + ".js"); + return Path.Combine(GetScheduledTasksDataDirectory(), Id + ".js"); } /// <summary> @@ -477,7 +462,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <returns>System.String.</returns> private string GetConfigurationFilePath() { - return Path.Combine(GetScheduledTasksConfigurationDirectory(false), Id + ".js"); + return Path.Combine(GetScheduledTasksConfigurationDirectory(), Id + ".js"); } /// <summary> @@ -512,9 +497,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks { var path = GetConfigurationFilePath(); - var parentPath = Path.GetDirectoryName(path); - - Directory.CreateDirectory(parentPath); + Directory.CreateDirectory(Path.GetDirectoryName(path)); JsonSerializer.SerializeToFile(triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), path); } @@ -545,7 +528,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks result.ErrorMessage = ex.Message; } - JsonSerializer.SerializeToFile(result, GetHistoryFilePath(true)); + var path = GetHistoryFilePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + JsonSerializer.SerializeToFile(result, path); LastExecutionResult = result; diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 6d886bc69..d02984bad 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// <summary> /// Deletes old cache files /// </summary> - public class DeleteCacheFileTask : IScheduledTask + public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask { /// <summary> /// Gets or sets the application paths. @@ -160,5 +160,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks return "Maintenance"; } } + + /// <summary> + /// Gets a value indicating whether this instance is hidden. + /// </summary> + /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value> + public bool IsHidden + { + get { return true; } + } } } diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 7c7833ae6..e5cb7aa10 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// <summary> /// Deletes old log files /// </summary> - public class DeleteLogFileTask : IScheduledTask + public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask { /// <summary> /// Gets or sets the configuration manager. @@ -115,5 +115,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks return "Maintenance"; } } + + /// <summary> + /// Gets a value indicating whether this instance is hidden. + /// </summary> + /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value> + public bool IsHidden + { + get { return true; } + } } } diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs index 00928255c..9a65046bf 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// <summary> /// Class ReloadLoggerFileTask /// </summary> - public class ReloadLoggerFileTask : IScheduledTask + public class ReloadLoggerFileTask : IScheduledTask, IConfigurableScheduledTask { /// <summary> /// Gets or sets the log manager. @@ -91,5 +91,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks { get { return "Application"; } } + + public bool IsHidden + { + get { return true; } + } } } diff --git a/MediaBrowser.Common.Implementations/Security/MBLicenseFile.cs b/MediaBrowser.Common.Implementations/Security/MBLicenseFile.cs index 163a368bf..c5d5f28d6 100644 --- a/MediaBrowser.Common.Implementations/Security/MBLicenseFile.cs +++ b/MediaBrowser.Common.Implementations/Security/MBLicenseFile.cs @@ -11,7 +11,6 @@ namespace MediaBrowser.Common.Implementations.Security { private readonly IApplicationPaths _appPaths; - private readonly string _filename; public string RegKey { get { return _regKey; } @@ -26,6 +25,14 @@ namespace MediaBrowser.Common.Implementations.Security } } + private string Filename + { + get + { + return Path.Combine(_appPaths.ConfigurationDirectoryPath, "mb.lic"); + } + } + public string LegacyKey { get; set; } private Dictionary<Guid, DateTime> UpdateRecords { get; set; } private readonly object _lck = new object(); @@ -35,8 +42,6 @@ namespace MediaBrowser.Common.Implementations.Security { _appPaths = appPaths; - _filename = Path.Combine(_appPaths.ConfigurationDirectoryPath, "mb.lic"); - UpdateRecords = new Dictionary<Guid, DateTime>(); Load(); } @@ -64,15 +69,16 @@ namespace MediaBrowser.Common.Implementations.Security private void Load() { string[] contents = null; + var licenseFile = Filename; lock (_lck) { try { - contents = File.ReadAllLines(_filename); + contents = File.ReadAllLines(licenseFile); } catch (FileNotFoundException) { - (File.Create(_filename)).Close(); + (File.Create(licenseFile)).Close(); } } if (contents != null && contents.Length > 0) @@ -100,7 +106,9 @@ namespace MediaBrowser.Common.Implementations.Security lines.Add(pair.Value.Ticks.ToString()); } - lock(_lck) File.WriteAllLines(_filename, lines); + var licenseFile = Filename; + Directory.CreateDirectory(Path.GetDirectoryName(licenseFile)); + lock (_lck) File.WriteAllLines(licenseFile, lines); } } } diff --git a/MediaBrowser.Common.Implementations/Updates/InstallationManager.cs b/MediaBrowser.Common.Implementations/Updates/InstallationManager.cs index 0581343d3..18462ba9b 100644 --- a/MediaBrowser.Common.Implementations/Updates/InstallationManager.cs +++ b/MediaBrowser.Common.Implementations/Updates/InstallationManager.cs @@ -528,6 +528,7 @@ namespace MediaBrowser.Common.Implementations.Updates // Success - move it to the real target try { + Directory.CreateDirectory(Path.GetDirectoryName(target)); File.Copy(tempFile, target, true); //If it is an archive - write out a version file so we know what it is if (isArchive) diff --git a/MediaBrowser.Common.Implementations/packages.config b/MediaBrowser.Common.Implementations/packages.config index 16324ab90..81647c114 100644 --- a/MediaBrowser.Common.Implementations/packages.config +++ b/MediaBrowser.Common.Implementations/packages.config @@ -2,5 +2,5 @@ <packages> <package id="NLog" version="2.1.0" targetFramework="net45" /> <package id="sharpcompress" version="0.10.2" targetFramework="net45" /> - <package id="SimpleInjector" version="2.3.6" targetFramework="net45" /> + <package id="SimpleInjector" version="2.4.0" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/MediaBrowser.Common/Configuration/ConfigurationHelper.cs b/MediaBrowser.Common/Configuration/ConfigurationHelper.cs index 1f86c5c02..64c2e87de 100644 --- a/MediaBrowser.Common/Configuration/ConfigurationHelper.cs +++ b/MediaBrowser.Common/Configuration/ConfigurationHelper.cs @@ -42,6 +42,8 @@ namespace MediaBrowser.Common.Configuration // If the file didn't exist before, or if something has changed, re-save if (buffer == null || !buffer.SequenceEqual(newBytes)) { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + // Save it after load in case we got new items File.WriteAllBytes(path, newBytes); } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 56b86a3c1..1c7ffe424 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -129,8 +129,9 @@ namespace MediaBrowser.Common /// <summary> /// Inits this instance. /// </summary> + /// <param name="progress">The progress.</param> /// <returns>Task.</returns> - Task Init(); + Task Init(IProgress<double> progress); /// <summary> /// Creates the instance. diff --git a/MediaBrowser.Common/IO/IFileSystem.cs b/MediaBrowser.Common/IO/IFileSystem.cs index d307b74e5..8fba63195 100644 --- a/MediaBrowser.Common/IO/IFileSystem.cs +++ b/MediaBrowser.Common/IO/IFileSystem.cs @@ -74,5 +74,12 @@ namespace MediaBrowser.Common.IO /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param> /// <returns>FileStream.</returns> FileStream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share, bool isAsync = false); + + /// <summary> + /// Swaps the files. + /// </summary> + /// <param name="file1">The file1.</param> + /// <param name="file2">The file2.</param> + void SwapFiles(string file1, string file2); } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index fce9c18cf..6098c4bb3 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Common</RootNamespace> <AssemblyName>MediaBrowser.Common</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -25,6 +25,7 @@ <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <PlatformTarget>AnyCPU</PlatformTarget> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -33,6 +34,16 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release Mono\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <ItemGroup> <Reference Include="System" /> @@ -101,9 +112,9 @@ </ItemGroup> <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> + <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <PropertyGroup> - <PostBuildEvent>if $(ConfigurationName) == Release ( + <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' ( xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i )</PostBuildEvent> </PropertyGroup> diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 977a6aabe..db78fc927 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -66,6 +66,10 @@ namespace MediaBrowser.Common.Net public Dictionary<string, string> RequestHeaders { get; private set; } + public string RequestContentType { get; set; } + + public string RequestContent { get; set; } + private string GetHeaderValue(string name) { string value; diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index 54d6665e2..a3d90cb0d 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -65,6 +65,13 @@ namespace MediaBrowser.Common.Net Task<Stream> Post(string url, Dictionary<string, string> postData, CancellationToken cancellationToken); /// <summary> + /// Posts the specified options. + /// </summary> + /// <param name="options">The options.</param> + /// <returns>Task{HttpResponseInfo}.</returns> + Task<HttpResponseInfo> Post(HttpRequestOptions options); + + /// <summary> /// Downloads the contents of a given url into a temporary location /// </summary> /// <param name="options">The options.</param> diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs index c11ff59d5..47536a341 100644 --- a/MediaBrowser.Common/Net/MimeTypes.cs +++ b/MediaBrowser.Common/Net/MimeTypes.cs @@ -218,6 +218,11 @@ namespace MediaBrowser.Common.Net return "image/svg+xml"; } + if (ext.Equals(".srt", StringComparison.OrdinalIgnoreCase)) + { + return "text/plain"; + } + throw new ArgumentException("Argument not supported: " + path); } } diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 795d22100..aa1369c4e 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -267,6 +267,8 @@ namespace MediaBrowser.Common.Plugins { lock (_configurationSaveLock) { + Directory.CreateDirectory(Path.GetDirectoryName(ConfigurationFilePath)); + XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 1a8583489..2ecf3ec9a 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Drawing /// <param name="item">The item.</param> /// <param name="imageType">Type of the image.</param> /// <returns>IEnumerable{IImageEnhancer}.</returns> - IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType); + IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType); /// <summary> /// Gets the image cache tag. @@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Drawing /// <param name="imageType">Type of the image.</param> /// <param name="imagePath">The image path.</param> /// <returns>Guid.</returns> - Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath); + Guid GetImageCacheTag(IHasImages item, ImageType imageType, string imagePath); /// <summary> /// Gets the image cache tag. @@ -67,7 +67,7 @@ namespace MediaBrowser.Controller.Drawing /// <param name="dateModified">The date modified.</param> /// <param name="imageEnhancers">The image enhancers.</param> /// <returns>Guid.</returns> - Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified, + Guid GetImageCacheTag(IHasImages item, ImageType imageType, string originalImagePath, DateTime dateModified, List<IImageEnhancer> imageEnhancers); /// <summary> @@ -85,6 +85,6 @@ namespace MediaBrowser.Controller.Drawing /// <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); + Task<string> GetEnhancedImage(IHasImages item, ImageType imageType, int imageIndex); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index ce4bf6c32..506d6fd3d 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Drawing { public class ImageProcessingOptions { - public BaseItem Item { get; set; } + public IHasImages Item { get; set; } public ImageType ImageType { get; set; } diff --git a/MediaBrowser.Controller/Entities/AdultVideo.cs b/MediaBrowser.Controller/Entities/AdultVideo.cs index 9bb0f8355..f81cfa1f6 100644 --- a/MediaBrowser.Controller/Entities/AdultVideo.cs +++ b/MediaBrowser.Controller/Entities/AdultVideo.cs @@ -1,7 +1,14 @@ namespace MediaBrowser.Controller.Entities { - public class AdultVideo : Video + public class AdultVideo : Video, IHasPreferredMetadataLanguage { + public string PreferredMetadataLanguage { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 63c907c1f..028fc964d 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,4 +1,5 @@ -using System; +using MediaBrowser.Model.Configuration; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; @@ -8,7 +9,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class Audio /// </summary> - public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLanguage + public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres { public Audio() { @@ -16,12 +17,6 @@ namespace MediaBrowser.Controller.Entities.Audio } /// <summary> - /// Gets or sets the language. - /// </summary> - /// <value>The language.</value> - public string Language { get; set; } - - /// <summary> /// Gets or sets a value indicating whether this instance has embedded image. /// </summary> /// <value><c>true</c> if this instance has embedded image; otherwise, <c>false</c>.</value> @@ -131,5 +126,10 @@ namespace MediaBrowser.Controller.Entities.Audio return base.GetUserDataKey(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedMusic; + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 3facccec1..203e6dc43 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.Linq; @@ -109,6 +110,11 @@ namespace MediaBrowser.Controller.Entities.Audio return base.GetUserDataKey(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedMusic; + } } public class MusicAlbumDisc : Folder diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 3be555f49..860d34fd8 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; @@ -96,7 +97,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// </summary> /// <param name="item">The item.</param> /// <returns>System.String.</returns> - public static string GetUserDataKey(BaseItem item) + private static string GetUserDataKey(MusicArtist item) { var id = item.GetProviderId(MetadataProviders.Musicbrainz); @@ -107,5 +108,10 @@ namespace MediaBrowser.Controller.Entities.Audio return "Artist-" + item.Name; } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedMusic; + } } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 541887598..11562b3d7 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; @@ -22,7 +23,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class BaseItem /// </summary> - public abstract class BaseItem : IHasProviderIds, ILibraryItem + public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData { protected BaseItem() { @@ -132,8 +133,8 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public string PrimaryImagePath { - get { return GetImage(ImageType.Primary); } - set { SetImage(ImageType.Primary, value); } + get { return this.GetImagePath(ImageType.Primary); } + set { this.SetImagePath(ImageType.Primary, value); } } /// <summary> @@ -956,6 +957,66 @@ namespace MediaBrowser.Controller.Entities } /// <summary> + /// Gets the preferred metadata language. + /// </summary> + /// <returns>System.String.</returns> + public string GetPreferredMetadataLanguage() + { + string lang = null; + + var hasLang = this as IHasPreferredMetadataLanguage; + + if (hasLang != null) + { + lang = hasLang.PreferredMetadataLanguage; + } + + if (string.IsNullOrEmpty(lang)) + { + lang = Parents.OfType<IHasPreferredMetadataLanguage>() + .Select(i => i.PreferredMetadataLanguage) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + 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 = null; + + var hasLang = this as IHasPreferredMetadataLanguage; + + if (hasLang != null) + { + lang = hasLang.PreferredMetadataCountryCode; + } + + if (string.IsNullOrEmpty(lang)) + { + lang = Parents.OfType<IHasPreferredMetadataLanguage>() + .Select(i => i.PreferredMetadataCountryCode) + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + } + + if (string.IsNullOrEmpty(lang)) + { + lang = ConfigurationManager.Configuration.MetadataCountryCode; + } + + return lang; + } + + /// <summary> /// Determines if a given user has access to this item /// </summary> /// <param name="user">The user.</param> @@ -985,7 +1046,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { - return !user.Configuration.BlockNotRated; + return !GetBlockUnratedValue(user.Configuration); } var value = localizationManager.GetRatingLevel(rating); @@ -1000,6 +1061,16 @@ namespace MediaBrowser.Controller.Entities } /// <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(UserConfiguration config) + { + return config.BlockNotRated; + } + + /// <summary> /// Determines if this folder should be visible to a given user. /// Default is just parental allowed. Can be overridden for more functionality. /// </summary> @@ -1064,6 +1135,11 @@ namespace MediaBrowser.Controller.Entities { throw new ArgumentNullException(); } + if (IsInMixedFolder != copy.IsInMixedFolder) + { + Logger.Debug(Name + " changed due to different value for IsInMixedFolder."); + return true; + } var changed = copy.DateModified != DateModified; if (changed) @@ -1310,31 +1386,10 @@ namespace MediaBrowser.Controller.Entities /// Gets an image /// </summary> /// <param name="type">The type.</param> - /// <returns>System.String.</returns> - /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> - public string GetImage(ImageType type) - { - if (type == ImageType.Backdrop) - { - throw new ArgumentException("Backdrops should be accessed using Item.Backdrops"); - } - if (type == ImageType.Screenshot) - { - throw new ArgumentException("Screenshots should be accessed using Item.Screenshots"); - } - - string val; - Images.TryGetValue(type, out val); - return val; - } - - /// <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) + public bool HasImage(ImageType type, int imageIndex) { if (type == ImageType.Backdrop) { @@ -1345,16 +1400,10 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentException("Screenshots should be accessed using Item.Screenshots"); } - return !string.IsNullOrEmpty(GetImage(type)); + return !string.IsNullOrEmpty(this.GetImagePath(type)); } - /// <summary> - /// Sets an image - /// </summary> - /// <param name="type">The type.</param> - /// <param name="path">The path.</param> - /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> - public void SetImage(ImageType type, string path) + public void SetImagePath(ImageType type, int index, string path) { if (type == ImageType.Backdrop) { @@ -1423,10 +1472,10 @@ namespace MediaBrowser.Controller.Entities else { // Delete the source file - DeleteImagePath(GetImage(type)); + DeleteImagePath(this.GetImagePath(type)); // Remove it from the item - SetImage(type, null); + this.SetImagePath(type, null); } // Refresh metadata @@ -1597,13 +1646,13 @@ namespace MediaBrowser.Controller.Entities { if (imageType == ImageType.Backdrop) { - return BackdropImagePaths[imageIndex]; + return BackdropImagePaths.Count > imageIndex ? BackdropImagePaths[imageIndex] : null; } if (imageType == ImageType.Screenshot) { var hasScreenshots = (IHasScreenshots)this; - return hasScreenshots.ScreenshotImagePaths[imageIndex]; + return hasScreenshots.ScreenshotImagePaths.Count > imageIndex ? hasScreenshots.ScreenshotImagePaths[imageIndex] : null; } if (imageType == ImageType.Chapter) @@ -1611,7 +1660,9 @@ namespace MediaBrowser.Controller.Entities return ItemRepository.GetChapter(Id, imageIndex).ImagePath; } - return GetImage(imageType); + string val; + Images.TryGetValue(imageType, out val); + return val; } /// <summary> @@ -1658,5 +1709,21 @@ namespace MediaBrowser.Controller.Entities { return new[] { Path }; } + + public Task SwapImages(ImageType type, int index1, int index2) + { + if (type != ImageType.Screenshot && type != ImageType.Backdrop) + { + throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); + } + + var file1 = GetImagePath(type, index1); + var file2 = GetImagePath(type, index2); + + FileSystem.SwapFiles(file1, file2); + + // Directory watchers should repeat this, but do a quick refresh first + return RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false); + } } } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 87b90b824..298941378 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using MediaBrowser.Model.Configuration; +using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { - public class Book : BaseItem, IHasTags + public class Book : BaseItem, IHasTags, IHasPreferredMetadataLanguage { public override string MediaType { @@ -11,6 +12,7 @@ namespace MediaBrowser.Controller.Entities return Model.Entities.MediaType.Book; } } + /// <summary> /// Gets or sets the tags. /// </summary> @@ -19,6 +21,14 @@ namespace MediaBrowser.Controller.Entities public string SeriesName { get; set; } + public string PreferredMetadataLanguage { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } + /// <summary> /// /// </summary> @@ -42,5 +52,10 @@ namespace MediaBrowser.Controller.Entities { Tags = new List<string>(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedBooks; + } } } diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index f032d9318..351385533 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -58,13 +58,6 @@ namespace MediaBrowser.Controller.Entities /// <returns>Task.</returns> protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) { - //we don't directly validate our children - //but we do need to clear out the index cache... - if (IndexCache != null) - { - IndexCache.Clear(); - } - ResetDynamicChildren(); return NullTaskResult; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e7593b075..912b8fa93 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -8,7 +8,6 @@ using MediaBrowser.Model.Entities; using MoreLinq; using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -209,315 +208,32 @@ namespace MediaBrowser.Controller.Entities #region Indexing /// <summary> - /// The _index by options - /// </summary> - private Dictionary<string, Func<User, IEnumerable<BaseItem>>> _indexByOptions; - /// <summary> - /// Dictionary of index options - consists of a display value and an indexing function - /// which takes User as a parameter and returns an IEnum of BaseItem - /// </summary> - /// <value>The index by options.</value> - [IgnoreDataMember] - public Dictionary<string, Func<User, IEnumerable<BaseItem>>> IndexByOptions - { - get { return _indexByOptions ?? (_indexByOptions = GetIndexByOptions()); } - } - - /// <summary> /// Returns the valid set of index by options for this folder type. /// Override or extend to modify. /// </summary> /// <returns>Dictionary{System.StringFunc{UserIEnumerable{BaseItem}}}.</returns> - protected virtual Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions() + protected virtual IEnumerable<string> GetIndexByOptions() { - return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> { - {LocalizedStrings.Instance.GetString("NoneDispPref"), null}, - {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer}, - {LocalizedStrings.Instance.GetString("GenreDispPref"), GetIndexByGenre}, - {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector}, - {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear}, + return new List<string> { + {LocalizedStrings.Instance.GetString("NoneDispPref")}, + {LocalizedStrings.Instance.GetString("PerformerDispPref")}, + {LocalizedStrings.Instance.GetString("GenreDispPref")}, + {LocalizedStrings.Instance.GetString("DirectorDispPref")}, + {LocalizedStrings.Instance.GetString("YearDispPref")}, //{LocalizedStrings.Instance.GetString("OfficialRatingDispPref"), null}, - {LocalizedStrings.Instance.GetString("StudioDispPref"), GetIndexByStudio} + {LocalizedStrings.Instance.GetString("StudioDispPref")} }; } /// <summary> - /// Gets the index by actor. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - protected IEnumerable<BaseItem> GetIndexByPerformer(User user) - { - return GetIndexByPerson(user, new List<string> { PersonType.Actor, PersonType.GuestStar }, true, LocalizedStrings.Instance.GetString("PerformerDispPref")); - } - - /// <summary> - /// Gets the index by director. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - protected IEnumerable<BaseItem> GetIndexByDirector(User user) - { - return GetIndexByPerson(user, new List<string> { PersonType.Director }, false, LocalizedStrings.Instance.GetString("DirectorDispPref")); - } - - /// <summary> - /// Gets the index by person. - /// </summary> - /// <param name="user">The user.</param> - /// <param name="personTypes">The person types we should match on</param> - /// <param name="includeAudio">if set to <c>true</c> [include audio].</param> - /// <param name="indexName">Name of the index.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - private IEnumerable<BaseItem> GetIndexByPerson(User user, List<string> personTypes, bool includeAudio, string indexName) - { - // Even though this implementation means multiple iterations over the target list - it allows us to defer - // the retrieval of the individual children for each index value until they are requested. - using (new Profiler(indexName + " Index Build for " + Name, Logger)) - { - // Put this in a local variable to avoid an implicitly captured closure - var currentIndexName = indexName; - - var us = this; - var recursiveChildren = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); - - // Get the candidates, but handle audio separately - var candidates = recursiveChildren.Where(i => i.AllPeople != null && !(i is Audio.Audio)).ToList(); - - var indexFolders = candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type)) - .Select(a => a.Name)) - .Distinct() - .Select(i => - { - try - { - return LibraryManager.GetPerson(i); - } - catch (IOException ex) - { - Logger.ErrorException("Error getting person {0}", ex, i); - return null; - } - catch (AggregateException ex) - { - Logger.ErrorException("Error getting person {0}", ex, i); - return null; - } - }) - .Where(i => i != null) - .Select(a => new IndexFolder(us, a, - candidates.Where(i => i.AllPeople.Any(p => personTypes.Contains(p.Type) && p.Name.Equals(a.Name, StringComparison.OrdinalIgnoreCase)) - ), currentIndexName)).AsEnumerable(); - - if (includeAudio) - { - var songs = recursiveChildren.OfType<Audio.Audio>().ToList(); - - indexFolders = songs.SelectMany(i => i.Artists) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(i => - { - try - { - return LibraryManager.GetArtist(i); - } - catch (IOException ex) - { - Logger.ErrorException("Error getting artist {0}", ex, i); - return null; - } - catch (AggregateException ex) - { - Logger.ErrorException("Error getting artist {0}", ex, i); - return null; - } - }) - .Where(i => i != null) - .Select(a => new IndexFolder(us, a, - songs.Where(i => i.Artists.Contains(a.Name, StringComparer.OrdinalIgnoreCase) - ), currentIndexName)).Concat(indexFolders); - } - - return indexFolders; - } - } - - /// <summary> - /// Gets the index by studio. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - protected IEnumerable<BaseItem> GetIndexByStudio(User user) - { - // Even though this implementation means multiple iterations over the target list - it allows us to defer - // the retrieval of the individual children for each index value until they are requested. - using (new Profiler("Studio Index Build for " + Name, Logger)) - { - var indexName = LocalizedStrings.Instance.GetString("StudioDispPref"); - - var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); - - return candidates.AsParallel().SelectMany(i => i.AllStudios) - .Distinct() - .Select(i => - { - try - { - return LibraryManager.GetStudio(i); - } - catch (IOException ex) - { - Logger.ErrorException("Error getting studio {0}", ex, i); - return null; - } - catch (AggregateException ex) - { - Logger.ErrorException("Error getting studio {0}", ex, i); - return null; - } - }) - .Where(i => i != null) - .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.AllStudios.Any(s => s.Equals(ndx.Name, StringComparison.OrdinalIgnoreCase))), indexName)); - } - } - - /// <summary> - /// Gets the index by genre. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - protected IEnumerable<BaseItem> GetIndexByGenre(User user) - { - // Even though this implementation means multiple iterations over the target list - it allows us to defer - // the retrieval of the individual children for each index value until they are requested. - using (new Profiler("Genre Index Build for " + Name, Logger)) - { - var indexName = LocalizedStrings.Instance.GetString("GenreDispPref"); - - //we need a copy of this so we don't double-recurse - var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); - - return candidates.AsParallel().SelectMany(i => i.AllGenres) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(i => - { - try - { - return LibraryManager.GetGenre(i); - } - catch (Exception ex) - { - Logger.ErrorException("Error getting genre {0}", ex, i); - return null; - } - }) - .Where(i => i != null) - .Select(genre => new IndexFolder(this, genre, candidates.Where(i => i.AllGenres.Any(g => g.Equals(genre.Name, StringComparison.OrdinalIgnoreCase))), indexName) - ); - } - } - - /// <summary> - /// Gets the index by year. - /// </summary> - /// <param name="user">The user.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - protected IEnumerable<BaseItem> GetIndexByYear(User user) - { - // Even though this implementation means multiple iterations over the target list - it allows us to defer - // the retrieval of the individual children for each index value until they are requested. - using (new Profiler("Production Year Index Build for " + Name, Logger)) - { - var indexName = LocalizedStrings.Instance.GetString("YearDispPref"); - - //we need a copy of this so we don't double-recurse - var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList(); - - return candidates.AsParallel().Select(i => i.ProductionYear.Value) - .Distinct() - .Select(i => - { - try - { - return LibraryManager.GetYear(i); - } - catch (IOException ex) - { - Logger.ErrorException("Error getting year {0}", ex, i); - return null; - } - catch (AggregateException ex) - { - Logger.ErrorException("Error getting year {0}", ex, i); - return null; - } - }) - .Where(i => i != null) - - .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.ProductionYear == int.Parse(ndx.Name)), indexName)); - - } - } - - /// <summary> - /// Returns the indexed children for this user from the cache. Caches them if not already there. - /// </summary> - /// <param name="user">The user.</param> - /// <param name="indexBy">The index by.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - private IEnumerable<BaseItem> GetIndexedChildren(User user, string indexBy) - { - List<BaseItem> result = null; - var cacheKey = user.Name + indexBy; - - if (IndexCache != null) - { - IndexCache.TryGetValue(cacheKey, out result); - } - - if (result == null) - { - //not cached - cache it - Func<User, IEnumerable<BaseItem>> indexing; - IndexByOptions.TryGetValue(indexBy, out indexing); - result = BuildIndex(indexBy, indexing, user); - } - return result; - } - - /// <summary> /// Get the list of indexy by choices for this folder (localized). /// </summary> /// <value>The index by option strings.</value> [IgnoreDataMember] public IEnumerable<string> IndexByOptionStrings { - get { return IndexByOptions.Keys; } - } - - /// <summary> - /// The index cache - /// </summary> - protected ConcurrentDictionary<string, List<BaseItem>> IndexCache; - - /// <summary> - /// Builds the index. - /// </summary> - /// <param name="indexKey">The index key.</param> - /// <param name="indexFunction">The index function.</param> - /// <param name="user">The user.</param> - /// <returns>List{BaseItem}.</returns> - protected virtual List<BaseItem> BuildIndex(string indexKey, Func<User, IEnumerable<BaseItem>> indexFunction, User user) - { - if (IndexCache == null) - { - IndexCache = new ConcurrentDictionary<string, List<BaseItem>>(); - } - - return indexFunction != null - ? IndexCache[user.Name + indexKey] = indexFunction(user).ToList() - : null; + get { return GetIndexByOptions(); } } #endregion @@ -648,130 +364,127 @@ namespace MediaBrowser.Controller.Entities { var locationType = LocationType; - // Nothing to do here - if (locationType == LocationType.Remote || locationType == LocationType.Virtual) - { - return; - } - cancellationToken.ThrowIfCancellationRequested(); - IEnumerable<BaseItem> nonCachedChildren; + var validChildren = new List<Tuple<BaseItem, bool>>(); - try - { - nonCachedChildren = GetNonCachedChildren(); - } - catch (IOException ex) + if (locationType != LocationType.Remote && locationType != LocationType.Virtual) { - nonCachedChildren = new BaseItem[] { }; + IEnumerable<BaseItem> nonCachedChildren; - Logger.ErrorException("Error getting file system entries for {0}", ex, Path); - } + try + { + nonCachedChildren = GetNonCachedChildren(); + } + catch (IOException ex) + { + nonCachedChildren = new BaseItem[] {}; - if (nonCachedChildren == null) return; //nothing to validate + Logger.ErrorException("Error getting file system entries for {0}", ex, Path); + } - progress.Report(5); + if (nonCachedChildren == null) return; //nothing to validate - //build a dictionary of the current children we have now by Id so we can compare quickly and easily - var currentChildren = ActualChildren.ToDictionary(i => i.Id); + progress.Report(5); - //create a list for our validated children - var validChildren = new List<Tuple<BaseItem, bool>>(); - var newItems = new List<BaseItem>(); + //build a dictionary of the current children we have now by Id so we can compare quickly and easily + var currentChildren = ActualChildren.ToDictionary(i => i.Id); - cancellationToken.ThrowIfCancellationRequested(); + //create a list for our validated children + var newItems = new List<BaseItem>(); - foreach (var child in nonCachedChildren) - { - BaseItem currentChild; + cancellationToken.ThrowIfCancellationRequested(); - if (currentChildren.TryGetValue(child.Id, out currentChild)) + foreach (var child in nonCachedChildren) { - currentChild.ResetResolveArgs(child.ResolveArgs); + BaseItem currentChild; - //existing item - check if it has changed - if (currentChild.HasChanged(child)) + if (currentChildren.TryGetValue(child.Id, out currentChild)) { - var currentChildLocationType = currentChild.LocationType; - if (currentChildLocationType != LocationType.Remote && - currentChildLocationType != LocationType.Virtual) + currentChild.ResetResolveArgs(child.ResolveArgs); + + //existing item - check if it has changed + if (currentChild.HasChanged(child)) { - EntityResolutionHelper.EnsureDates(FileSystem, currentChild, child.ResolveArgs, false); + var currentChildLocationType = currentChild.LocationType; + if (currentChildLocationType != LocationType.Remote && + currentChildLocationType != LocationType.Virtual) + { + EntityResolutionHelper.EnsureDates(FileSystem, currentChild, child.ResolveArgs, false); + } + + currentChild.IsInMixedFolder = child.IsInMixedFolder; + validChildren.Add(new Tuple<BaseItem, bool>(currentChild, true)); + } + else + { + validChildren.Add(new Tuple<BaseItem, bool>(currentChild, false)); } - validChildren.Add(new Tuple<BaseItem, bool>(currentChild, true)); + currentChild.IsOffline = false; } else { - validChildren.Add(new Tuple<BaseItem, bool>(currentChild, false)); - } + //brand new item - needs to be added + newItems.Add(child); - currentChild.IsOffline = false; + validChildren.Add(new Tuple<BaseItem, bool>(child, true)); + } } - else + + // If any items were added or removed.... + if (newItems.Count > 0 || currentChildren.Count != validChildren.Count) { - //brand new item - needs to be added - newItems.Add(child); + var newChildren = validChildren.Select(c => c.Item1).ToList(); - validChildren.Add(new Tuple<BaseItem, bool>(child, true)); - } - } + // That's all the new and changed ones - now see if there are any that are missing + var itemsRemoved = currentChildren.Values.Except(newChildren).ToList(); - // If any items were added or removed.... - if (newItems.Count > 0 || currentChildren.Count != validChildren.Count) - { - var newChildren = validChildren.Select(c => c.Item1).ToList(); + var actualRemovals = new List<BaseItem>(); - // That's all the new and changed ones - now see if there are any that are missing - var itemsRemoved = currentChildren.Values.Except(newChildren).ToList(); + foreach (var item in itemsRemoved) + { + if (item.LocationType == LocationType.Virtual || + item.LocationType == LocationType.Remote) + { + // Don't remove these because there's no way to accurately validate them. + validChildren.Add(new Tuple<BaseItem, bool>(item, false)); + } - var actualRemovals = new List<BaseItem>(); + else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path)) + { + item.IsOffline = true; - foreach (var item in itemsRemoved) - { - if (item.LocationType == LocationType.Virtual || - item.LocationType == LocationType.Remote) - { - // Don't remove these because there's no way to accurately validate them. - continue; + validChildren.Add(new Tuple<BaseItem, bool>(item, false)); + } + else + { + item.IsOffline = false; + actualRemovals.Add(item); + } } - - if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path)) - { - item.IsOffline = true; - validChildren.Add(new Tuple<BaseItem, bool>(item, false)); - } - else + if (actualRemovals.Count > 0) { - item.IsOffline = false; - actualRemovals.Add(item); - } - } + RemoveChildrenInternal(actualRemovals); - if (actualRemovals.Count > 0) - { - RemoveChildrenInternal(actualRemovals); - - foreach (var item in actualRemovals) - { - LibraryManager.ReportItemRemoved(item); + foreach (var item in actualRemovals) + { + LibraryManager.ReportItemRemoved(item); + } } - } - - await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false); - AddChildrenInternal(newItems); + await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false); - await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); + AddChildrenInternal(newItems); - //force the indexes to rebuild next time - if (IndexCache != null) - { - IndexCache.Clear(); + await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); } } + else + { + validChildren.AddRange(ActualChildren.Select(i => new Tuple<BaseItem, bool>(i, false))); + } progress.Report(10); @@ -988,10 +701,9 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="user">The user.</param> /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> - /// <param name="indexBy">The index by.</param> /// <returns>IEnumerable{BaseItem}.</returns> /// <exception cref="System.ArgumentNullException"></exception> - public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren, string indexBy = null) + public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren) { if (user == null) { @@ -999,19 +711,7 @@ namespace MediaBrowser.Controller.Entities } //the true root should return our users root folder children - if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren, indexBy); - - IEnumerable<BaseItem> result = null; - - if (!string.IsNullOrEmpty(indexBy)) - { - result = GetIndexedChildren(user, indexBy); - } - - if (result != null) - { - return result; - } + if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren); var list = new List<BaseItem>(); @@ -1359,13 +1059,24 @@ namespace MediaBrowser.Controller.Entities Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); } - //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy return RecursiveChildren.Where(i => i.LocationType != LocationType.Virtual).FirstOrDefault(i => { try { - return string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) - || i.ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase); + if (string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (i.LocationType != LocationType.Remote) + { + if (i.ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; } catch (IOException ex) { diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs index c15a31dd3..da95b7c44 100644 --- a/MediaBrowser.Controller/Entities/Game.cs +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -1,16 +1,25 @@ -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { - public class Game : BaseItem, IHasSoundtracks, IHasTrailers, IHasThemeMedia, IHasTags, IHasLanguage, IHasScreenshots + public class Game : BaseItem, IHasSoundtracks, IHasTrailers, IHasThemeMedia, IHasTags, IHasScreenshots, IHasPreferredMetadataLanguage { public List<Guid> SoundtrackIds { get; set; } public List<Guid> ThemeSongIds { get; set; } public List<Guid> ThemeVideoIds { get; set; } + public string PreferredMetadataLanguage { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } + public Game() { MultiPartGameFiles = new List<string>(); @@ -23,12 +32,6 @@ namespace MediaBrowser.Controller.Entities ScreenshotImagePaths = new List<string>(); } - /// <summary> - /// Gets or sets the language. - /// </summary> - /// <value>The language.</value> - public string Language { get; set; } - public List<Guid> LocalTrailerIds { get; set; } /// <summary> @@ -129,5 +132,10 @@ namespace MediaBrowser.Controller.Entities return base.GetDeletePaths(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedGames; + } } } diff --git a/MediaBrowser.Controller/Entities/GameSystem.cs b/MediaBrowser.Controller/Entities/GameSystem.cs index 054071b35..63af8082a 100644 --- a/MediaBrowser.Controller/Entities/GameSystem.cs +++ b/MediaBrowser.Controller/Entities/GameSystem.cs @@ -1,4 +1,5 @@ -using System; +using MediaBrowser.Model.Configuration; +using System; namespace MediaBrowser.Controller.Entities { @@ -38,5 +39,11 @@ namespace MediaBrowser.Controller.Entities } return base.GetUserDataKey(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + // Don't block. Determine by game + return false; + } } } diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 0fa49639b..53bc64194 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,5 +1,4 @@ using MediaBrowser.Model.Dto; -using System; using System.Collections.Generic; using System.Runtime.Serialization; diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs new file mode 100644 index 000000000..d800acd9b --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -0,0 +1,115 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasImages + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the path. + /// </summary> + /// <value>The path.</value> + string Path { get; } + + /// <summary> + /// Gets the identifier. + /// </summary> + /// <value>The identifier.</value> + Guid Id { get; } + + /// <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> + string GetImagePath(ImageType imageType, int imageIndex); + + /// <summary> + /// Gets the image date modified. + /// </summary> + /// <param name="imagePath">The image path.</param> + /// <returns>DateTime.</returns> + DateTime GetImageDateModified(string imagePath); + + /// <summary> + /// Sets the image. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="index">The index.</param> + /// <param name="path">The path.</param> + void SetImagePath(ImageType type, int index, string path); + + /// <summary> + /// Determines whether the specified type has 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> + bool HasImage(ImageType type, int imageIndex); + + /// <summary> + /// Swaps the images. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="index1">The index1.</param> + /// <param name="index2">The index2.</param> + /// <returns>Task.</returns> + Task SwapImages(ImageType type, int index1, int index2); + + /// <summary> + /// Gets the display type of the media. + /// </summary> + /// <value>The display type of the media.</value> + string DisplayMediaType { get; set; } + + /// <summary> + /// Gets or sets the primary image path. + /// </summary> + /// <value>The primary image path.</value> + string PrimaryImagePath { get; set; } + + /// <summary> + /// Gets the preferred metadata language. + /// </summary> + /// <returns>System.String.</returns> + string GetPreferredMetadataLanguage(); + } + + public static class HasImagesExtensions + { + /// <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 IHasImages item, ImageType imageType) + { + return item.GetImagePath(imageType, 0); + } + + public static bool HasImage(this IHasImages 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="path">The path.</param> + public static void SetImagePath(this IHasImages item, ImageType imageType, string path) + { + item.SetImagePath(imageType, 0, path); + } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasLanguage.cs b/MediaBrowser.Controller/Entities/IHasLanguage.cs deleted file mode 100644 index a1bb80098..000000000 --- a/MediaBrowser.Controller/Entities/IHasLanguage.cs +++ /dev/null @@ -1,15 +0,0 @@ - -namespace MediaBrowser.Controller.Entities -{ - /// <summary> - /// Interface IHasLanguage - /// </summary> - public interface IHasLanguage - { - /// <summary> - /// Gets or sets the language. - /// </summary> - /// <value>The language.</value> - string Language { get; set; } - } -} diff --git a/MediaBrowser.Controller/Entities/IHasPreferredMetadataLanguage.cs b/MediaBrowser.Controller/Entities/IHasPreferredMetadataLanguage.cs new file mode 100644 index 000000000..e3a233e49 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasPreferredMetadataLanguage.cs @@ -0,0 +1,21 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasPreferredMetadataLanguage + /// </summary> + public interface IHasPreferredMetadataLanguage + { + /// <summary> + /// Gets or sets the preferred metadata language. + /// </summary> + /// <value>The preferred metadata language.</value> + string PreferredMetadataLanguage { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + string PreferredMetadataCountryCode { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/IHasUserData.cs b/MediaBrowser.Controller/Entities/IHasUserData.cs new file mode 100644 index 000000000..780181a61 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasUserData.cs @@ -0,0 +1,15 @@ + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// Interface IHasUserData + /// </summary> + public interface IHasUserData + { + /// <summary> + /// Gets the user data key. + /// </summary> + /// <returns>System.String.</returns> + string GetUserDataKey(); + } +} diff --git a/MediaBrowser.Controller/Entities/IndexFolder.cs b/MediaBrowser.Controller/Entities/IndexFolder.cs deleted file mode 100644 index 57e4a35d3..000000000 --- a/MediaBrowser.Controller/Entities/IndexFolder.cs +++ /dev/null @@ -1,206 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Model.Entities; -using MoreLinq; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Entities -{ - /// <summary> - /// Class IndexFolder - /// </summary> - public class IndexFolder : Folder - { - /// <summary> - /// Initializes a new instance of the <see cref="IndexFolder" /> class. - /// </summary> - /// <param name="parent">The parent.</param> - /// <param name="shadow">The shadow.</param> - /// <param name="children">The children.</param> - /// <param name="indexName">Name of the index.</param> - /// <param name="groupContents">if set to <c>true</c> [group contents].</param> - public IndexFolder(Folder parent, BaseItem shadow, IEnumerable<BaseItem> children, string indexName, bool groupContents = true) - { - ChildSource = children; - ShadowItem = shadow; - GroupContents = groupContents; - if (shadow == null) - { - Name = ForcedSortName = "<Unknown>"; - } - else - { - SetShadowValues(); - } - Id = (parent.Id.ToString() + Name).GetMBId(typeof(IndexFolder)); - - IndexName = indexName; - Parent = parent; - } - - /// <summary> - /// Resets the parent. - /// </summary> - /// <param name="parent">The parent.</param> - public void ResetParent(Folder parent) - { - Parent = parent; - Id = (parent.Id.ToString() + Name).GetMBId(typeof(IndexFolder)); - } - - /// <summary> - /// Override this to true if class should be grouped under a container in indicies - /// The container class should be defined via IndexContainer - /// </summary> - /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public override bool GroupInIndex - { - get - { - return ShadowItem != null && ShadowItem.GroupInIndex; - } - } - - public override LocationType LocationType - { - get - { - return LocationType.Virtual; - } - } - - /// <summary> - /// Override this to return the folder that should be used to construct a container - /// for this item in an index. GroupInIndex should be true as well. - /// </summary> - /// <value>The index container.</value> - [IgnoreDataMember] - public override Folder IndexContainer - { - get { return ShadowItem != null ? ShadowItem.IndexContainer : new IndexFolder(this, null, null, "<Unknown>", false); } - } - - /// <summary> - /// Gets or sets a value indicating whether [group contents]. - /// </summary> - /// <value><c>true</c> if [group contents]; otherwise, <c>false</c>.</value> - protected bool GroupContents { get; set; } - /// <summary> - /// Gets or sets the child source. - /// </summary> - /// <value>The child source.</value> - protected IEnumerable<BaseItem> ChildSource { get; set; } - /// <summary> - /// Gets or sets our children. - /// </summary> - /// <value>Our children.</value> - protected ConcurrentBag<BaseItem> OurChildren { get; set; } - /// <summary> - /// Gets the name of the index. - /// </summary> - /// <value>The name of the index.</value> - public string IndexName { get; private set; } - - /// <summary> - /// Override to return the children defined to us when we were created - /// </summary> - /// <value>The actual children.</value> - protected override IEnumerable<BaseItem> LoadChildren() - { - var originalChildSource = ChildSource.ToList(); - - var kids = originalChildSource; - if (GroupContents) - { - // Recursively group up the chain - var group = true; - var isSubsequentLoop = false; - - while (group) - { - kids = isSubsequentLoop || kids.Any(i => i.GroupInIndex) - ? GroupedSource(kids).ToList() - : originalChildSource; - - group = kids.Any(i => i.GroupInIndex); - isSubsequentLoop = true; - } - } - - // Now - since we built the index grouping from the bottom up - we now need to properly set Parents from the top down - SetParents(this, kids.OfType<IndexFolder>()); - - return kids.DistinctBy(i => i.Id); - } - - /// <summary> - /// Sets the parents. - /// </summary> - /// <param name="parent">The parent.</param> - /// <param name="kids">The kids.</param> - private void SetParents(Folder parent, IEnumerable<IndexFolder> kids) - { - foreach (var child in kids) - { - child.ResetParent(parent); - child.SetParents(child, child.Children.OfType<IndexFolder>()); - } - } - - /// <summary> - /// Groupeds the source. - /// </summary> - /// <param name="source">The source.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - protected IEnumerable<BaseItem> GroupedSource(IEnumerable<BaseItem> source) - { - return source.GroupBy(i => i.IndexContainer).Select(container => new IndexFolder(this, container.Key, container, null, false)); - } - - /// <summary> - /// The item we are shadowing as a folder (Genre, Actor, etc.) - /// We inherit the images and other meta from this item - /// </summary> - /// <value>The shadow item.</value> - protected BaseItem ShadowItem { get; set; } - - /// <summary> - /// Sets the shadow values. - /// </summary> - protected void SetShadowValues() - { - if (ShadowItem != null) - { - Name = ShadowItem.Name; - ForcedSortName = ShadowItem.SortName; - Genres = ShadowItem.Genres; - Studios = ShadowItem.Studios; - OfficialRating = ShadowItem.OfficialRatingForComparison; - BackdropImagePaths = ShadowItem.BackdropImagePaths; - Images = ShadowItem.Images; - Overview = ShadowItem.Overview; - DisplayMediaType = ShadowItem.GetType().Name; - } - } - - /// <summary> - /// Overrides the base implementation to refresh metadata for local trailers - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="forceSave">if set to <c>true</c> [is new item].</param> - /// <param name="forceRefresh">if set to <c>true</c> [force].</param> - /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param> - /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param> - /// <returns>Task{System.Boolean}.</returns> - public override Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) - { - // We should never get in here since these are not part of the library - return Task.FromResult(false); - } - } -} diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index a1154482c..6144bdd71 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,5 +1,6 @@ -using System; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; +using System; using System.Collections.Generic; namespace MediaBrowser.Controller.Entities.Movies @@ -7,7 +8,7 @@ namespace MediaBrowser.Controller.Entities.Movies /// <summary> /// Class BoxSet /// </summary> - public class BoxSet : Folder, IHasTrailers, IHasTags + public class BoxSet : Folder, IHasTrailers, IHasTags, IHasPreferredMetadataLanguage { public BoxSet() { @@ -29,5 +30,18 @@ namespace MediaBrowser.Controller.Entities.Movies /// </summary> /// <value>The tags.</value> public List<string> Tags { get; set; } + + public string PreferredMetadataLanguage { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedMovies; + } } } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index b4cf6c047..f9d3f845c 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.IO; @@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Entities.Movies /// <summary> /// Class Movie /// </summary> - public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasTags + public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasTags, IHasPreferredMetadataLanguage { public List<Guid> SpecialFeatureIds { get; set; } @@ -19,6 +20,14 @@ namespace MediaBrowser.Controller.Entities.Movies public List<Guid> ThemeSongIds { get; set; } public List<Guid> ThemeVideoIds { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } + + public string PreferredMetadataLanguage { get; set; } public Movie() { @@ -180,5 +189,9 @@ namespace MediaBrowser.Controller.Entities.Movies }); } + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedMovies; + } } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 68ad4630a..d9eff8fbe 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; @@ -48,5 +49,10 @@ namespace MediaBrowser.Controller.Entities { return this.GetProviderId(MetadataProviders.Tmdb) ?? this.GetProviderId(MetadataProviders.Imdb) ?? base.GetUserDataKey(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedMusic; + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index e9f250d2a..42897e09f 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Entities.TV { @@ -41,6 +42,23 @@ namespace MediaBrowser.Controller.Entities.TV public int? AirsBeforeEpisodeNumber { get; set; } /// <summary> + /// Gets or sets the DVD season number. + /// </summary> + /// <value>The DVD season number.</value> + public int? DvdSeasonNumber { get; set; } + /// <summary> + /// Gets or sets the DVD episode number. + /// </summary> + /// <value>The DVD episode number.</value> + public float? DvdEpisodeNumber { get; set; } + + /// <summary> + /// Gets or sets the absolute episode number. + /// </summary> + /// <value>The absolute episode number.</value> + public int? AbsoluteEpisodeNumber { get; set; } + + /// <summary> /// We want to group into series not show individually in an index /// </summary> /// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value> @@ -275,5 +293,10 @@ namespace MediaBrowser.Controller.Entities.TV { return new[] { Path }; } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedSeries; + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 78e0b8bc4..2d781118e 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,5 +1,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; using System.IO; @@ -55,13 +58,13 @@ namespace MediaBrowser.Controller.Entities.TV } // Genre, Rating and Stuido will all be the same - protected override Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions() + protected override IEnumerable<string> GetIndexByOptions() { - return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> { - {LocalizedStrings.Instance.GetString("NoneDispPref"), null}, - {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer}, - {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector}, - {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear}, + return new List<string> { + {LocalizedStrings.Instance.GetString("NoneDispPref")}, + {LocalizedStrings.Instance.GetString("PerformerDispPref")}, + {LocalizedStrings.Instance.GetString("DirectorDispPref")}, + {LocalizedStrings.Instance.GetString("YearDispPref")}, }; } @@ -186,7 +189,7 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public bool IsMissingSeason { - get { return LocationType == Model.Entities.LocationType.Virtual && GetEpisodes().All(i => i.IsMissingEpisode); } + get { return LocationType == LocationType.Virtual && GetEpisodes().All(i => i.IsMissingEpisode); } } [IgnoreDataMember] @@ -198,13 +201,13 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public bool IsVirtualUnaired { - get { return LocationType == Model.Entities.LocationType.Virtual && IsUnaired; } + get { return LocationType == LocationType.Virtual && IsUnaired; } } [IgnoreDataMember] public bool IsMissingOrVirtualUnaired { - get { return LocationType == Model.Entities.LocationType.Virtual && GetEpisodes().All(i => i.IsVirtualUnaired || i.IsMissingEpisode); } + get { return LocationType == LocationType.Virtual && GetEpisodes().All(i => i.IsVirtualUnaired || i.IsMissingEpisode); } } [IgnoreDataMember] @@ -212,5 +215,57 @@ namespace MediaBrowser.Controller.Entities.TV { get { return (IndexNumber ?? -1) == 0; } } + + /// <summary> + /// Gets the episodes. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Episode}.</returns> + public IEnumerable<Episode> GetEpisodes(User user) + { + var config = user.Configuration; + + return GetEpisodes(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); + } + + public IEnumerable<Episode> GetEpisodes(User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes) + { + if (IndexNumber.HasValue) + { + var series = Series; + + if (series != null) + { + return series.GetEpisodes(user, IndexNumber.Value, includeMissingEpisodes, includeVirtualUnairedEpisodes); + } + } + + var episodes = GetRecursiveChildren(user) + .OfType<Episode>(); + + if (!includeMissingEpisodes) + { + episodes = episodes.Where(i => !i.IsMissingEpisode); + } + if (!includeVirtualUnairedEpisodes) + { + episodes = episodes.Where(i => !i.IsVirtualUnaired); + } + + return LibraryManager + .Sort(episodes, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) + .Cast<Episode>(); + } + + public override IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren) + { + return GetEpisodes(user); + } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + // Don't block. Let either the entire series rating or episode rating determine it + return false; + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 2312df2a1..f7e78ccd4 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,6 +1,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; using System.IO; @@ -12,13 +14,19 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Series /// </summary> - public class Series : Folder, IHasSoundtracks, IHasTrailers, IHasTags + public class Series : Folder, IHasSoundtracks, IHasTrailers, IHasTags, IHasPreferredMetadataLanguage { public List<Guid> SpecialFeatureIds { get; set; } public List<Guid> SoundtrackIds { get; set; } public int SeasonCount { get; set; } + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } + public Series() { AirDays = new List<DayOfWeek>(); @@ -28,8 +36,11 @@ namespace MediaBrowser.Controller.Entities.TV RemoteTrailers = new List<MediaUrl>(); LocalTrailerIds = new List<Guid>(); Tags = new List<string>(); + DisplaySpecialsWithSeasons = true; } + public bool DisplaySpecialsWithSeasons { get; set; } + public List<Guid> LocalTrailerIds { get; set; } public List<MediaUrl> RemoteTrailers { get; set; } @@ -85,13 +96,13 @@ namespace MediaBrowser.Controller.Entities.TV } // Studio, Genre and Rating will all be the same so makes no sense to index by these - protected override Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions() + protected override IEnumerable<string> GetIndexByOptions() { - return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> { - {LocalizedStrings.Instance.GetString("NoneDispPref"), null}, - {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer}, - {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector}, - {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear}, + return new List<string> { + {LocalizedStrings.Instance.GetString("NoneDispPref")}, + {LocalizedStrings.Instance.GetString("PerformerDispPref")}, + {LocalizedStrings.Instance.GetString("DirectorDispPref")}, + {LocalizedStrings.Instance.GetString("YearDispPref")}, }; } @@ -117,5 +128,108 @@ namespace MediaBrowser.Controller.Entities.TV return Children.OfType<Video>().Any(); } } + + public override IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren) + { + return GetSeasons(user); + } + + public IEnumerable<Season> GetSeasons(User user) + { + var config = user.Configuration; + + return GetSeasons(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); + } + + public IEnumerable<Season> GetSeasons(User user, bool includeMissingSeasons, bool includeVirtualUnaired) + { + var seasons = base.GetChildren(user, true) + .OfType<Season>(); + + if (!includeMissingSeasons && !includeVirtualUnaired) + { + seasons = seasons.Where(i => !i.IsMissingOrVirtualUnaired); + } + else + { + if (!includeMissingSeasons) + { + seasons = seasons.Where(i => !i.IsMissingSeason); + } + if (!includeVirtualUnaired) + { + seasons = seasons.Where(i => !i.IsVirtualUnaired); + } + } + + return LibraryManager + .Sort(seasons, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) + .Cast<Season>(); + } + + public IEnumerable<Episode> GetEpisodes(User user, int seasonNumber) + { + var config = user.Configuration; + + return GetEpisodes(user, seasonNumber, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); + } + + public IEnumerable<Episode> GetEpisodes(User user, int seasonNumber, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes) + { + var episodes = GetRecursiveChildren(user) + .OfType<Episode>(); + + episodes = FilterEpisodesBySeason(episodes, seasonNumber, DisplaySpecialsWithSeasons); + + if (!includeMissingEpisodes) + { + episodes = episodes.Where(i => !i.IsMissingEpisode); + } + if (!includeVirtualUnairedEpisodes) + { + episodes = episodes.Where(i => !i.IsVirtualUnaired); + } + + var sortBy = seasonNumber == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder; + + return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending) + .Cast<Episode>(); + } + + /// <summary> + /// Filters the episodes by season. + /// </summary> + /// <param name="episodes">The episodes.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="includeSpecials">if set to <c>true</c> [include specials].</param> + /// <returns>IEnumerable{Episode}.</returns> + public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials) + { + if (!includeSpecials || seasonNumber < 1) + { + return episodes.Where(i => (i.PhysicalSeasonNumber ?? -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(UserConfiguration config) + { + return config.BlockUnratedSeries; + } + + public string PreferredMetadataLanguage { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 591fea14a..7000d04d3 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -8,9 +9,17 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Trailer /// </summary> - public class Trailer : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers, IHasTaglines, IHasTags + public class Trailer : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasTrailers, IHasTaglines, IHasTags, IHasPreferredMetadataLanguage { public List<Guid> SoundtrackIds { get; set; } + + public string PreferredMetadataLanguage { get; set; } + + /// <summary> + /// Gets or sets the preferred metadata country code. + /// </summary> + /// <value>The preferred metadata country code.</value> + public string PreferredMetadataCountryCode { get; set; } public Trailer() { @@ -113,5 +122,10 @@ namespace MediaBrowser.Controller.Entities return base.GetUserDataKey(); } + + protected override bool GetBlockUnratedValue(UserConfiguration config) + { + return config.BlockUnratedTrailers; + } } } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 06f50e552..466e709dd 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; @@ -58,6 +57,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> + [IgnoreDataMember] public override string Path { get @@ -76,14 +76,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> private UserRootFolder _rootFolder; /// <summary> - /// The _user root folder initialized - /// </summary> - private bool _userRootFolderInitialized; - /// <summary> - /// The _user root folder sync lock - /// </summary> - private object _userRootFolderSyncLock = new object(); - /// <summary> /// Gets the root folder. /// </summary> /// <value>The root folder.</value> @@ -92,17 +84,11 @@ namespace MediaBrowser.Controller.Entities { get { - LazyInitializer.EnsureInitialized(ref _rootFolder, ref _userRootFolderInitialized, ref _userRootFolderSyncLock, () => LibraryManager.GetUserRootFolder(RootFolderPath)); - return _rootFolder; + return _rootFolder ?? (LibraryManager.GetUserRootFolder(RootFolderPath)); } private set { _rootFolder = value; - - if (_rootFolder == null) - { - _userRootFolderInitialized = false; - } } } @@ -154,22 +140,6 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets the last date modified of the configuration - /// </summary> - /// <value>The configuration date last modified.</value> - [IgnoreDataMember] - public DateTime ConfigurationDateLastModified - { - get - { - // Ensure it's been lazy loaded - var config = Configuration; - - return FileSystem.GetLastWriteTimeUtc(ConfigurationFilePath); - } - } - - /// <summary> /// Reloads the root media folder /// </summary> /// <param name="cancellationToken">The cancellation token.</param> @@ -203,13 +173,22 @@ namespace MediaBrowser.Controller.Entities { // Move configuration var newConfigDirectory = GetConfigurationDirectoryPath(newName); + var oldConfigurationDirectory = ConfigurationDirectoryPath; // Exceptions will be thrown if these paths already exist if (Directory.Exists(newConfigDirectory)) { Directory.Delete(newConfigDirectory, true); } - Directory.Move(ConfigurationDirectoryPath, newConfigDirectory); + + if (Directory.Exists(oldConfigurationDirectory)) + { + Directory.Move(oldConfigurationDirectory, newConfigDirectory); + } + else + { + Directory.CreateDirectory(newConfigDirectory); + } var customLibraryPath = GetRootFolderPath(Name); @@ -228,7 +207,6 @@ namespace MediaBrowser.Controller.Entities Name = newName; // Force these to be lazy loaded again - _configurationDirectoryPath = null; RootFolder = null; // Kick off a task to validate the media library @@ -238,25 +216,15 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// The _configuration directory path - /// </summary> - private string _configurationDirectoryPath; - /// <summary> /// Gets the path to the user's configuration directory /// </summary> /// <value>The configuration directory path.</value> + [IgnoreDataMember] private string ConfigurationDirectoryPath { get { - if (_configurationDirectoryPath == null) - { - _configurationDirectoryPath = GetConfigurationDirectoryPath(Name); - - Directory.CreateDirectory(_configurationDirectoryPath); - } - - return _configurationDirectoryPath; + return GetConfigurationDirectoryPath(Name); } } @@ -267,6 +235,11 @@ namespace MediaBrowser.Controller.Entities /// <returns>System.String.</returns> private string GetConfigurationDirectoryPath(string username) { + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentNullException("username"); + } + var safeFolderName = FileSystem.GetValidFilename(username); return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); @@ -276,6 +249,7 @@ namespace MediaBrowser.Controller.Entities /// Gets the path to the user's configuration file /// </summary> /// <value>The configuration file path.</value> + [IgnoreDataMember] public string ConfigurationFilePath { get @@ -289,7 +263,9 @@ namespace MediaBrowser.Controller.Entities /// </summary> public void SaveConfiguration(IXmlSerializer serializer) { - serializer.SerializeToFile(Configuration, ConfigurationFilePath); + var xmlPath = ConfigurationFilePath; + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(xmlPath)); + serializer.SerializeToFile(Configuration, xmlPath); } /// <summary> diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 57cb7c7dd..f8b78299c 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -26,7 +26,7 @@ namespace MediaBrowser.Controller /// Gets the path to the Images By Name directory /// </summary> /// <value>The images by name path.</value> - string ItemsByNamePath { get; set; } + string ItemsByNamePath { get; } /// <summary> /// Gets the path to the People directory @@ -51,13 +51,13 @@ namespace MediaBrowser.Controller /// </summary> /// <value>The game genre path.</value> string GameGenrePath { get; } - + /// <summary> /// Gets the artists path. /// </summary> /// <value>The artists path.</value> string ArtistsPath { get; } - + /// <summary> /// Gets the path to the Studio directory /// </summary> @@ -87,7 +87,7 @@ namespace MediaBrowser.Controller /// </summary> /// <value>The media info images path.</value> string MediaInfoImagesPath { get; } - + /// <summary> /// Gets the path to the user configuration directory /// </summary> diff --git a/MediaBrowser.Controller/Kernel.cs b/MediaBrowser.Controller/Kernel.cs deleted file mode 100644 index 37a1648c1..000000000 --- a/MediaBrowser.Controller/Kernel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediaBrowser.Controller.MediaInfo; - -namespace MediaBrowser.Controller -{ - /// <summary> - /// Class Kernel - /// </summary> - public class Kernel - { - /// <summary> - /// Gets the instance. - /// </summary> - /// <value>The instance.</value> - public static Kernel Instance { get; private set; } - - /// <summary> - /// Gets the FFMPEG controller. - /// </summary> - /// <value>The FFMPEG controller.</value> - public FFMpegManager FFMpegManager { get; set; } - - /// <summary> - /// Creates a kernel based on a Data path, which is akin to our current programdata path - /// </summary> - public Kernel() - { - Instance = this; - } - } -} diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 338edd568..ae34621cb 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -116,6 +116,11 @@ namespace MediaBrowser.Controller.Library Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken); /// <summary> + /// Queues the library scan. + /// </summary> + void QueueLibraryScan(); + + /// <summary> /// Gets the default view. /// </summary> /// <returns>IEnumerable{VirtualFolderInfo}.</returns> diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index d6d5f99aa..2bec9e3de 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Library /// <param name="reason">The reason.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - Task SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + Task SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); /// <summary> /// Gets the user data. diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index e296e0f18..c34180eb6 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -130,7 +130,7 @@ namespace MediaBrowser.Controller.Library { get { - return IsDirectory && Path.Equals(_appPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase); + return IsDirectory && string.Equals(Path, _appPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index 65308bd10..d7504a86e 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -32,7 +32,9 @@ namespace MediaBrowser.Controller.Library "sæson", "temporada", "saison", - "staffel" + "staffel", + "series", + "сезон" }; /// <summary> @@ -122,6 +124,11 @@ namespace MediaBrowser.Controller.Library { var filename = Path.GetFileName(path); + if (string.Equals(path, "specials", StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + // Look for one of the season folder names foreach (var name in SeasonFolderNames) { diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs index 87e7f647a..ba328ff75 100644 --- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs +++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs @@ -37,6 +37,6 @@ namespace MediaBrowser.Controller.Library /// Gets or sets the item. /// </summary> /// <value>The item.</value> - public BaseItem Item { get; set; } + public IHasUserData Item { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/Channel.cs b/MediaBrowser.Controller/LiveTv/Channel.cs deleted file mode 100644 index 8097cea1d..000000000 --- a/MediaBrowser.Controller/LiveTv/Channel.cs +++ /dev/null @@ -1,73 +0,0 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace MediaBrowser.Controller.LiveTv -{ - public class Channel : BaseItem, IItemByName - { - public Channel() - { - UserItemCountList = new List<ItemByNameCounts>(); - } - - /// <summary> - /// Gets the user data key. - /// </summary> - /// <returns>System.String.</returns> - public override string GetUserDataKey() - { - return "Channel-" + Name; - } - - [IgnoreDataMember] - public List<ItemByNameCounts> UserItemCountList { get; set; } - - /// <summary> - /// Gets or sets the number. - /// </summary> - /// <value>The number.</value> - public string ChannelNumber { get; set; } - - /// <summary> - /// Get or sets the Id. - /// </summary> - /// <value>The id of the channel.</value> - public string ChannelId { get; set; } - - /// <summary> - /// Gets or sets the name of the service. - /// </summary> - /// <value>The name of the service.</value> - public string ServiceName { get; set; } - - /// <summary> - /// Gets or sets the type of the channel. - /// </summary> - /// <value>The type of the channel.</value> - public ChannelType ChannelType { get; set; } - - protected override string CreateSortName() - { - double number = 0; - - if (!string.IsNullOrEmpty(ChannelNumber)) - { - double.TryParse(ChannelNumber, out number); - } - - return number.ToString("000-") + (Name ?? string.Empty); - } - - public override string MediaType - { - get - { - return ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; - } - } - } -} diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index 27fc59630..cdc9c76c8 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -30,5 +30,24 @@ namespace MediaBrowser.Controller.LiveTv /// </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; } + } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index b8dfbe05d..c26e29d94 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Model.LiveTv; +using System.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using System.Collections.Generic; using System.Threading; @@ -24,13 +26,21 @@ namespace MediaBrowser.Controller.LiveTv IReadOnlyList<ILiveTvService> Services { get; } /// <summary> - /// Schedules the recording. + /// Gets the new timer defaults asynchronous. /// </summary> - /// <param name="programId">The program identifier.</param> - /// <returns>Task.</returns> - Task ScheduleRecording(string programId); + /// <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> /// Deletes the recording. /// </summary> /// <param name="id">The identifier.</param> @@ -45,6 +55,13 @@ namespace MediaBrowser.Controller.LiveTv 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> @@ -54,18 +71,29 @@ namespace MediaBrowser.Controller.LiveTv /// Gets the channels. /// </summary> /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>IEnumerable{Channel}.</returns> - QueryResult<ChannelInfoDto> GetChannels(ChannelQuery query); + Task<QueryResult<ChannelInfoDto>> GetChannels(ChannelQuery query, CancellationToken cancellationToken); /// <summary> /// Gets the recording. /// </summary> /// <param name="id">The identifier.</param> + /// <param name="user">The user.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{RecordingInfoDto}.</returns> - Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken); + Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken, User user = null); /// <summary> + /// Gets the channel. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="user">The user.</param> + /// <returns>Task{RecordingInfoDto}.</returns> + Task<ChannelInfoDto> GetChannel(string id, CancellationToken cancellationToken, User user = null); + + /// <summary> /// Gets the timer. /// </summary> /// <param name="id">The identifier.</param> @@ -74,6 +102,14 @@ namespace MediaBrowser.Controller.LiveTv 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> @@ -90,26 +126,106 @@ namespace MediaBrowser.Controller.LiveTv 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. /// </summary> /// <param name="id">The identifier.</param> /// <returns>Channel.</returns> - Channel GetChannel(string id); + LiveTvChannel GetInternalChannel(string id); /// <summary> - /// Gets the channel. + /// Gets the internal program. /// </summary> /// <param name="id">The identifier.</param> - /// <param name="userId">The user identifier.</param> - /// <returns>Channel.</returns> - ChannelInfoDto GetChannelInfoDto(string id, string userId); + /// <returns>LiveTvProgram.</returns> + LiveTvProgram GetInternalProgram(string id); + + /// <summary> + /// Gets the recording. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>LiveTvRecording.</returns> + Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken); + + /// <summary> + /// Gets the recording stream. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + Task<StreamResponseInfo> GetRecordingStream(string id, CancellationToken cancellationToken); /// <summary> + /// Gets the channel stream. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{StreamResponseInfo}.</returns> + Task<StreamResponseInfo> GetChannelStream(string id, 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<ProgramInfoDto> GetProgram(string id, CancellationToken cancellationToken, User user = null); + + /// <summary> /// Gets the programs. /// </summary> /// <param name="query">The query.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>IEnumerable{ProgramInfo}.</returns> Task<QueryResult<ProgramInfoDto>> GetPrograms(ProgramQuery query, 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 recording groups. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{QueryResult{RecordingGroupDto}}.</returns> + Task<QueryResult<RecordingGroupDto>> GetRecordingGroups(RecordingGroupQuery query, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index a6c60d468..f8efbce63 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common.Net; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -32,6 +31,14 @@ namespace MediaBrowser.Controller.LiveTv 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> /// Deletes the recording asynchronous. /// </summary> /// <param name="recordingId">The recording identifier.</param> @@ -56,6 +63,14 @@ namespace MediaBrowser.Controller.LiveTv 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> @@ -64,20 +79,29 @@ namespace MediaBrowser.Controller.LiveTv Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken); /// <summary> - /// Gets the channel image asynchronous. + /// Gets the channel image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to ChannelInfo /// </summary> /// <param name="channelId">The channel identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{Stream}.</returns> - Task<ImageResponseInfo> GetChannelImageAsync(string channelId, CancellationToken cancellationToken); + Task<StreamResponseInfo> GetChannelImageAsync(string channelId, CancellationToken cancellationToken); + + /// <summary> + /// Gets the recording image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to RecordingInfo + /// </summary> + /// <param name="recordingId">The recording identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{ImageResponseInfo}.</returns> + Task<StreamResponseInfo> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken); /// <summary> - /// Gets the program image asynchronous. + /// Gets the program image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to ProgramInfo /// </summary> /// <param name="programId">The program identifier.</param> + /// <param name="channelId">The channel identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{ImageResponseInfo}.</returns> - Task<ImageResponseInfo> GetProgramImageAsync(string programId, CancellationToken cancellationToken); + Task<StreamResponseInfo> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken); /// <summary> /// Gets the recordings asynchronous. @@ -94,6 +118,13 @@ namespace MediaBrowser.Controller.LiveTv Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken); /// <summary> + /// Gets the timer defaults asynchronous. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{TimerInfo}.</returns> + Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken); + + /// <summary> /// Gets the series timers asynchronous. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> @@ -107,5 +138,21 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{IEnumerable{ProgramInfo}}.</returns> Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, CancellationToken cancellationToken); + + /// <summary> + /// Gets the recording stream. + /// </summary> + /// <param name="recordingId">The recording identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + Task<StreamResponseInfo> GetRecordingStream(string recordingId, CancellationToken cancellationToken); + + /// <summary> + /// Gets the channel stream. + /// </summary> + /// <param name="channelId">The channel identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + Task<StreamResponseInfo> GetChannelStream(string channelId, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs new file mode 100644 index 000000000..1e6d74ce8 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -0,0 +1,57 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvChannel : BaseItem, IItemByName + { + public LiveTvChannel() + { + UserItemCountList = new List<ItemByNameCounts>(); + } + + /// <summary> + /// Gets the user data key. + /// </summary> + /// <returns>System.String.</returns> + public override string GetUserDataKey() + { + return GetClientTypeName() + "-" + Name; + } + + [IgnoreDataMember] + public List<ItemByNameCounts> UserItemCountList { get; set; } + + public ChannelInfo ChannelInfo { get; set; } + + public string ServiceName { get; set; } + + protected override string CreateSortName() + { + double number = 0; + + if (!string.IsNullOrEmpty(ChannelInfo.Number)) + { + double.TryParse(ChannelInfo.Number, out number); + } + + return number.ToString("000-") + (Name ?? string.Empty); + } + + public override string MediaType + { + get + { + return ChannelInfo.ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + } + } + + public override string GetClientTypeName() + { + return "Channel"; + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvException.cs b/MediaBrowser.Controller/LiveTv/LiveTvException.cs new file mode 100644 index 000000000..0a68180ca --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MediaBrowser.Controller.LiveTv +{ + /// <summary> + /// Class LiveTvException. + /// </summary> + public class LiveTvException : Exception + { + } + + /// <summary> + /// Class LiveTvConflictException. + /// </summary> + public class LiveTvConflictException : LiveTvException + { + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs new file mode 100644 index 000000000..abacc0c18 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvProgram : BaseItem + { + /// <summary> + /// Gets the user data key. + /// </summary> + /// <returns>System.String.</returns> + public override string GetUserDataKey() + { + return GetClientTypeName() + "-" + Name; + } + + public ProgramInfo ProgramInfo { get; set; } + + public ChannelType ChannelType { get; set; } + + public string ServiceName { get; set; } + + public override string MediaType + { + get + { + return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio; + } + } + + public override string GetClientTypeName() + { + return "Program"; + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvRecording.cs b/MediaBrowser.Controller/LiveTv/LiveTvRecording.cs new file mode 100644 index 000000000..1c453ab5a --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/LiveTvRecording.cs @@ -0,0 +1,43 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.LiveTv +{ + public class LiveTvRecording : BaseItem + { + /// <summary> + /// Gets the user data key. + /// </summary> + /// <returns>System.String.</returns> + public override string GetUserDataKey() + { + return GetClientTypeName() + "-" + Name; + } + + public RecordingInfo RecordingInfo { get; set; } + + public string ServiceName { get; set; } + + public override string MediaType + { + get + { + return RecordingInfo.ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + } + } + + public override LocationType LocationType + { + get + { + return LocationType.Remote; + } + } + + public override string GetClientTypeName() + { + return "Recording"; + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 8059c1100..d672340e4 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -45,12 +45,6 @@ namespace MediaBrowser.Controller.LiveTv public DateTime EndDate { get; set; } /// <summary> - /// Gets or sets the aspect ratio. - /// </summary> - /// <value>The aspect ratio.</value> - public string AspectRatio { get; set; } - - /// <summary> /// Genre of the program. /// </summary> public List<string> Genres { get; set; } @@ -90,7 +84,67 @@ namespace MediaBrowser.Controller.LiveTv /// </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; } + + /// <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 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; } + public ProgramInfo() { Genres = new List<string>(); diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 1ffbb7e23..6a0d135c8 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -12,14 +12,15 @@ namespace MediaBrowser.Controller.LiveTv public string Id { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the series timer identifier. /// </summary> - public string ChannelId { get; set; } - + /// <value>The series timer identifier.</value> + public string SeriesTimerId { get; set; } + /// <summary> - /// ChannelName of the recording. + /// ChannelId of the recording. /// </summary> - public string ChannelName { get; set; } + public string ChannelId { get; set; } /// <summary> /// Gets or sets the type of the channel. @@ -39,6 +40,12 @@ namespace MediaBrowser.Controller.LiveTv 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> @@ -96,6 +103,48 @@ namespace MediaBrowser.Controller.LiveTv public ProgramAudio? Audio { 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> @@ -107,6 +156,25 @@ namespace MediaBrowser.Controller.LiveTv /// <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; } + + public RecordingInfo() { Genres = new List<string>(); diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs index 44594882c..1be6549ff 100644 --- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Model.LiveTv; -using System; +using System; using System.Collections.Generic; namespace MediaBrowser.Controller.LiveTv @@ -15,18 +14,13 @@ namespace MediaBrowser.Controller.LiveTv /// ChannelId of the recording. /// </summary> public string ChannelId { get; set; } - - /// <summary> - /// ChannelName of the recording. - /// </summary> - public string ChannelName { 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> @@ -35,7 +29,7 @@ namespace MediaBrowser.Controller.LiveTv /// <summary> /// Description of the recording. /// </summary> - public string Description { get; set; } + public string Overview { get; set; } /// <summary> /// The start date of the recording, in UTC. @@ -48,23 +42,23 @@ namespace MediaBrowser.Controller.LiveTv public DateTime EndDate { get; set; } /// <summary> - /// Gets or sets the pre padding seconds. + /// Gets or sets a value indicating whether [record any time]. /// </summary> - /// <value>The pre padding seconds.</value> - public int PrePaddingSeconds { get; set; } + /// <value><c>true</c> if [record any time]; otherwise, <c>false</c>.</value> + public bool RecordAnyTime { get; set; } /// <summary> - /// Gets or sets the post padding seconds. + /// Gets or sets a value indicating whether [record any channel]. /// </summary> - /// <value>The post padding seconds.</value> - public int PostPaddingSeconds { get; set; } + /// <value><c>true</c> if [record any channel]; otherwise, <c>false</c>.</value> + public bool RecordAnyChannel { get; set; } /// <summary> - /// Gets or sets the type of the recurrence. + /// Gets or sets a value indicating whether [record new only]. /// </summary> - /// <value>The type of the recurrence.</value> - public RecurrenceType RecurrenceType { get; set; } - + /// <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> @@ -77,6 +71,30 @@ namespace MediaBrowser.Controller.LiveTv /// <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; } + public SeriesTimerInfo() { Days = new List<DayOfWeek>(); diff --git a/MediaBrowser.Controller/LiveTv/ImageResponseInfo.cs b/MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs index d454a1ef8..c3b438c5e 100644 --- a/MediaBrowser.Controller/LiveTv/ImageResponseInfo.cs +++ b/MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs @@ -2,7 +2,7 @@ namespace MediaBrowser.Controller.LiveTv { - public class ImageResponseInfo + public class StreamResponseInfo { /// <summary> /// Gets or sets the stream. diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index 26e5869ce..5d92a212f 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -22,11 +22,6 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelId { get; set; } /// <summary> - /// ChannelName of the recording. - /// </summary> - public string ChannelName { get; set; } - - /// <summary> /// Gets or sets the program identifier. /// </summary> /// <value>The program identifier.</value> @@ -40,7 +35,7 @@ namespace MediaBrowser.Controller.LiveTv /// <summary> /// Description of the recording. /// </summary> - public string Description { get; set; } + public string Overview { get; set; } /// <summary> /// The start date of the recording, in UTC. @@ -59,27 +54,33 @@ namespace MediaBrowser.Controller.LiveTv public RecordingStatus Status { get; set; } /// <summary> - /// Gets or sets the requested pre padding seconds. + /// Gets or sets the pre padding seconds. /// </summary> - /// <value>The requested pre padding seconds.</value> - public int RequestedPrePaddingSeconds { get; set; } + /// <value>The pre padding seconds.</value> + public int PrePaddingSeconds { get; set; } /// <summary> - /// Gets or sets the requested post padding seconds. + /// Gets or sets the post padding seconds. /// </summary> - /// <value>The requested post padding seconds.</value> - public int RequestedPostPaddingSeconds { get; set; } + /// <value>The post padding seconds.</value> + public int PostPaddingSeconds { get; set; } /// <summary> - /// Gets or sets the required pre padding seconds. + /// Gets or sets a value indicating whether this instance is pre padding required. /// </summary> - /// <value>The required pre padding seconds.</value> - public int RequiredPrePaddingSeconds { get; set; } + /// <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 the required post padding seconds. + /// 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 priority. /// </summary> - /// <value>The required post padding seconds.</value> - public int RequiredPostPaddingSeconds { get; set; } + /// <value>The priority.</value> + public int Priority { get; set; } } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index f1f2aaf5e..0c5c0a5cd 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Controller</RootNamespace> <AssemblyName>MediaBrowser.Controller</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -24,6 +24,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -32,36 +33,21 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup> <RunPostBuildEvent>Always</RunPostBuildEvent> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> - <DebugSymbols>true</DebugSymbols> - <OutputPath>bin\x86\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> - <DebugType>full</DebugType> - <PlatformTarget>x86</PlatformTarget> - <ErrorReport>prompt</ErrorReport> - <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> - <WarningLevel>4</WarningLevel> - <Optimize>false</Optimize> - </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> - <OutputPath>bin\x86\Release\</OutputPath> - <DefineConstants>TRACE</DefineConstants> - <Optimize>true</Optimize> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> <DebugType>pdbonly</DebugType> - <PlatformTarget>x86</PlatformTarget> + <Optimize>true</Optimize> + <OutputPath>bin\Release Mono\</OutputPath> + <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> - <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <ItemGroup> - <Reference Include="ServiceStack.Interfaces, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> - </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Data" /> @@ -74,6 +60,9 @@ <Reference Include="MoreLinq"> <HintPath>..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll</HintPath> </Reference> + <Reference Include="ServiceStack.Interfaces"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> + </Reference> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -96,8 +85,9 @@ <Compile Include="Entities\IHasAspectRatio.cs" /> <Compile Include="Entities\IHasBudget.cs" /> <Compile Include="Entities\IHasCriticRating.cs" /> - <Compile Include="Entities\IHasLanguage.cs" /> + <Compile Include="Entities\IHasImages.cs" /> <Compile Include="Entities\IHasMediaStreams.cs" /> + <Compile Include="Entities\IHasPreferredMetadataLanguage.cs" /> <Compile Include="Entities\IHasProductionLocations.cs" /> <Compile Include="Entities\IHasScreenshots.cs" /> <Compile Include="Entities\IHasSoundtracks.cs" /> @@ -105,6 +95,7 @@ <Compile Include="Entities\IHasTags.cs" /> <Compile Include="Entities\IHasThemeMedia.cs" /> <Compile Include="Entities\IHasTrailers.cs" /> + <Compile Include="Entities\IHasUserData.cs" /> <Compile Include="Entities\IItemByName.cs" /> <Compile Include="Entities\ILibraryItem.cs" /> <Compile Include="Entities\ImageSourceInfo.cs" /> @@ -117,11 +108,14 @@ <Compile Include="Library\ItemUpdateType.cs" /> <Compile Include="Library\IUserDataManager.cs" /> <Compile Include="Library\UserDataSaveEventArgs.cs" /> - <Compile Include="LiveTv\Channel.cs" /> + <Compile Include="LiveTv\LiveTvChannel.cs" /> <Compile Include="LiveTv\ChannelInfo.cs" /> <Compile Include="LiveTv\ILiveTvManager.cs" /> <Compile Include="LiveTv\ILiveTvService.cs" /> - <Compile Include="LiveTv\ImageResponseInfo.cs" /> + <Compile Include="LiveTv\LiveTvException.cs" /> + <Compile Include="LiveTv\StreamResponseInfo.cs" /> + <Compile Include="LiveTv\LiveTvProgram.cs" /> + <Compile Include="LiveTv\LiveTvRecording.cs" /> <Compile Include="LiveTv\ProgramInfo.cs" /> <Compile Include="LiveTv\RecordingInfo.cs" /> <Compile Include="LiveTv\SeriesTimerInfo.cs" /> @@ -148,7 +142,6 @@ <Compile Include="Entities\Folder.cs" /> <Compile Include="Entities\Genre.cs" /> <Compile Include="Entities\ICollectionFolder.cs" /> - <Compile Include="Entities\IndexFolder.cs" /> <Compile Include="Entities\IVirtualFolderCreator.cs" /> <Compile Include="Entities\Movies\BoxSet.cs" /> <Compile Include="Entities\Movies\Movie.cs" /> @@ -200,10 +193,10 @@ <Compile Include="Library\TVUtils.cs" /> <Compile Include="Library\ItemResolveArgs.cs" /> <Compile Include="IO\FileData.cs" /> - <Compile Include="Kernel.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Providers\BaseMetadataProvider.cs" /> <Compile Include="Session\ISessionController.cs" /> + <Compile Include="Session\ISessionControllerFactory.cs" /> <Compile Include="Session\PlaybackInfo.cs" /> <Compile Include="Session\PlaybackProgressInfo.cs" /> <Compile Include="Session\PlaybackStopInfo.cs" /> @@ -227,7 +220,7 @@ </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> - <PostBuildEvent>if $(ConfigurationName) == Release ( + <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' ( xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i )</PostBuildEvent> </PropertyGroup> @@ -235,7 +228,7 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i <PreBuildEvent> </PreBuildEvent> </PropertyGroup> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> + <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs index e53acfc02..ced53299d 100644 --- a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs +++ b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs @@ -1,12 +1,16 @@ -using MediaBrowser.Common.IO; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -19,79 +23,89 @@ namespace MediaBrowser.Controller.MediaInfo /// </summary> public class FFMpegManager { - /// <summary> - /// Gets or sets the video image cache. - /// </summary> - /// <value>The video image cache.</value> - internal FileSystemRepository VideoImageCache { get; set; } - - /// <summary> - /// Gets or sets the subtitle cache. - /// </summary> - /// <value>The subtitle cache.</value> - internal FileSystemRepository SubtitleCache { get; set; } - - private readonly IServerApplicationPaths _appPaths; + private readonly IServerConfigurationManager _config; private readonly IMediaEncoder _encoder; private readonly ILogger _logger; private readonly IItemRepository _itemRepo; private readonly IFileSystem _fileSystem; + public static FFMpegManager Instance { get; private set; } + /// <summary> /// Initializes a new instance of the <see cref="FFMpegManager" /> class. /// </summary> - /// <param name="appPaths">The app paths.</param> /// <param name="encoder">The encoder.</param> /// <param name="logger">The logger.</param> /// <param name="itemRepo">The item repo.</param> /// <exception cref="System.ArgumentNullException">zipClient</exception> - public FFMpegManager(IServerApplicationPaths appPaths, IMediaEncoder encoder, ILogger logger, IItemRepository itemRepo, IFileSystem fileSystem) + public FFMpegManager(IMediaEncoder encoder, ILogger logger, IItemRepository itemRepo, IFileSystem fileSystem, IServerConfigurationManager config) { - _appPaths = appPaths; _encoder = encoder; _logger = logger; _itemRepo = itemRepo; _fileSystem = fileSystem; + _config = config; - VideoImageCache = new FileSystemRepository(VideoImagesDataPath); - SubtitleCache = new FileSystemRepository(SubtitleCachePath); + // TODO: Remove this static instance + Instance = this; } /// <summary> - /// Gets the video images data path. + /// Gets the chapter images data path. /// </summary> - /// <value>The video images data path.</value> - public string VideoImagesDataPath + /// <value>The chapter images data path.</value> + public string ChapterImagesPath { get { - return Path.Combine(_appPaths.DataPath, "extracted-video-images"); + return Path.Combine(_config.ApplicationPaths.DataPath, "chapter-images"); } } /// <summary> - /// Gets the audio images data path. + /// Gets the subtitle cache path. /// </summary> - /// <value>The audio images data path.</value> - public string AudioImagesDataPath + /// <value>The subtitle cache path.</value> + private string SubtitleCachePath { get { - return Path.Combine(_appPaths.DataPath, "extracted-audio-images"); + return Path.Combine(_config.ApplicationPaths.CachePath, "subtitles"); } } /// <summary> - /// Gets the subtitle cache path. + /// Determines whether [is eligible for chapter image extraction] [the specified video]. /// </summary> - /// <value>The subtitle cache path.</value> - public string SubtitleCachePath + /// <param name="video">The video.</param> + /// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns> + private bool IsEligibleForChapterImageExtraction(Video video) { - get + if (video is Movie) + { + if (!_config.Configuration.EnableMovieChapterImageExtraction) + { + return false; + } + } + else if (video is Episode) { - return Path.Combine(_appPaths.CachePath, "subtitles"); + if (!_config.Configuration.EnableEpisodeChapterImageExtraction) + { + return false; + } + } + else + { + if (!_config.Configuration.EnableOtherVideoChapterImageExtraction) + { + return false; + } } + + // Can't extract images if there are no video streams + return video.DefaultVideoStreamIndex.HasValue; } /// <summary> @@ -111,8 +125,7 @@ namespace MediaBrowser.Controller.MediaInfo /// <exception cref="System.ArgumentNullException"></exception> public async Task<bool> PopulateChapterImages(Video video, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { - // Can't extract images if there are no video streams - if (!video.DefaultVideoStreamIndex.HasValue) + if (!IsEligibleForChapterImageExtraction(video)) { return true; } @@ -122,6 +135,8 @@ namespace MediaBrowser.Controller.MediaInfo var runtimeTicks = video.RunTimeTicks ?? 0; + var currentImages = GetSavedChapterImages(video); + foreach (var chapter in chapters) { if (chapter.StartPositionTicks >= runtimeTicks) @@ -130,11 +145,9 @@ namespace MediaBrowser.Controller.MediaInfo break; } - var filename = video.Path + "_" + video.DateModified.Ticks + "_" + chapter.StartPositionTicks; - - var path = VideoImageCache.GetResourcePath(filename, ".jpg"); + var path = GetChapterImagePath(video, chapter.StartPositionTicks); - if (!File.Exists(path)) + if (!currentImages.Contains(path, StringComparer.OrdinalIgnoreCase)) { if (extractImages) { @@ -157,7 +170,7 @@ namespace MediaBrowser.Controller.MediaInfo InputType type; - var inputPath = MediaEncoderHelpers.GetInputArgument(video, null, out type); + var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, false, video.VideoType, video.IsoType, null, video.PlayableStreamFileNames, out type); try { @@ -188,39 +201,87 @@ namespace MediaBrowser.Controller.MediaInfo await _itemRepo.SaveChapters(video.Id, chapters, cancellationToken).ConfigureAwait(false); } + DeleteDeadImages(currentImages, chapters); + return success; } + private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters) + { + var deadImages = images + .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparer.OrdinalIgnoreCase)) + .ToList(); + + foreach (var image in deadImages) + { + _logger.Debug("Deleting dead chapter image {0}", image); + + try + { + File.Delete(image); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting {0}.", ex, image); + } + } + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + /// <summary> /// Gets the subtitle cache path. /// </summary> - /// <param name="input">The input.</param> - /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param> + /// <param name="mediaPath">The media path.</param> + /// <param name="subtitleStream">The subtitle stream.</param> /// <param name="offset">The offset.</param> /// <param name="outputExtension">The output extension.</param> /// <returns>System.String.</returns> - public string GetSubtitleCachePath(Video input, int subtitleStreamIndex, TimeSpan? offset, string outputExtension) + public string GetSubtitleCachePath(string mediaPath, MediaStream subtitleStream, TimeSpan? offset, string outputExtension) { var ticksParam = offset.HasValue ? "_" + offset.Value.Ticks : ""; - var stream = _itemRepo.GetMediaStreams(new MediaStreamQuery + if (subtitleStream.IsExternal) { - ItemId = input.Id, - Index = subtitleStreamIndex + ticksParam += _fileSystem.GetLastWriteTimeUtc(subtitleStream.Path).Ticks; + } + + var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); - }).FirstOrDefault(); + var filename = (mediaPath + "_" + subtitleStream.Index.ToString(_usCulture) + "_" + date.Ticks.ToString(_usCulture) + ticksParam).GetMD5() + outputExtension; - if (stream == null) + var prefix = filename.Substring(0, 1); + + return Path.Combine(SubtitleCachePath, prefix, filename); + } + + public string GetChapterImagePath(Video video, long chapterPositionTicks) + { + var filename = video.DateModified.Ticks.ToString(_usCulture) + "_" + chapterPositionTicks.ToString(_usCulture) + ".jpg"; + + var videoId = video.Id.ToString(); + var prefix = videoId.Substring(0, 1); + + return Path.Combine(ChapterImagesPath, prefix, videoId, filename); + } + + public List<string> GetSavedChapterImages(Video video) + { + var videoId = video.Id.ToString(); + var prefix = videoId.Substring(0, 1); + + var path = Path.Combine(ChapterImagesPath, prefix, videoId); + + try { - return null; + return Directory.EnumerateFiles(path) + .ToList(); } - - if (stream.IsExternal) + catch (DirectoryNotFoundException) { - ticksParam += _fileSystem.GetLastWriteTimeUtc(stream.Path).Ticks; + return new List<string>(); } - - return SubtitleCache.GetResourcePath(input.Id + "_" + subtitleStreamIndex + "_" + input.DateModified.Ticks + ticksParam, outputExtension); } } } diff --git a/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs index 8c2f7c219..904ecdf93 100644 --- a/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs @@ -1,5 +1,8 @@ -using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Common.MediaInfo; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -13,43 +16,47 @@ namespace MediaBrowser.Controller.MediaInfo /// <summary> /// Gets the input argument. /// </summary> - /// <param name="video">The video.</param> + /// <param name="videoPath">The video path.</param> + /// <param name="isRemote">if set to <c>true</c> [is remote].</param> + /// <param name="videoType">Type of the video.</param> + /// <param name="isoType">Type of the iso.</param> /// <param name="isoMount">The iso mount.</param> + /// <param name="playableStreamFileNames">The playable stream file names.</param> /// <param name="type">The type.</param> /// <returns>System.String[][].</returns> - public static string[] GetInputArgument(Video video, IIsoMount isoMount, out InputType type) + public static string[] GetInputArgument(string videoPath, bool isRemote, VideoType videoType, IsoType? isoType, IIsoMount isoMount, IEnumerable<string> playableStreamFileNames, out InputType type) { - var inputPath = isoMount == null ? new[] { video.Path } : new[] { isoMount.MountedPath }; + var inputPath = isoMount == null ? new[] { videoPath } : new[] { isoMount.MountedPath }; type = InputType.VideoFile; - switch (video.VideoType) + switch (videoType) { case VideoType.BluRay: type = InputType.Bluray; break; case VideoType.Dvd: type = InputType.Dvd; - inputPath = video.GetPlayableStreamFiles(inputPath[0]).ToArray(); + inputPath = GetPlayableStreamFiles(inputPath[0], playableStreamFileNames).ToArray(); break; case VideoType.Iso: - if (video.IsoType.HasValue) + if (isoType.HasValue) { - switch (video.IsoType.Value) + switch (isoType.Value) { case IsoType.BluRay: type = InputType.Bluray; break; case IsoType.Dvd: type = InputType.Dvd; - inputPath = video.GetPlayableStreamFiles(inputPath[0]).ToArray(); + inputPath = GetPlayableStreamFiles(inputPath[0], playableStreamFileNames).ToArray(); break; } } break; case VideoType.VideoFile: { - if (video.LocationType == LocationType.Remote) + if (isRemote) { type = InputType.Url; } @@ -60,6 +67,17 @@ namespace MediaBrowser.Controller.MediaInfo return inputPath; } + public static List<string> GetPlayableStreamFiles(string rootPath, IEnumerable<string> filenames) + { + var allFiles = Directory + .EnumerateFiles(rootPath, "*", SearchOption.AllDirectories) + .ToList(); + + return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase))) + .Where(f => !string.IsNullOrEmpty(f)) + .ToList(); + } + /// <summary> /// Gets the type of the input. /// </summary> diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index 3ad16b643..799f339f1 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -269,10 +269,10 @@ namespace MediaBrowser.Controller.Providers { var val = reader.ReadElementContentAsString(); - var hasLanguage = item as IHasLanguage; + var hasLanguage = item as IHasPreferredMetadataLanguage; if (hasLanguage != null) { - hasLanguage.Language = val; + hasLanguage.PreferredMetadataLanguage = val; } break; @@ -676,6 +676,7 @@ namespace MediaBrowser.Controller.Providers break; case "TMDbCollectionId": + case "CollectionNumber": var tmdbCollection = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(tmdbCollection)) { @@ -758,11 +759,30 @@ namespace MediaBrowser.Controller.Providers break; } - case "MediaInfo": + case "Format3D": { - using (var subtree = reader.ReadSubtree()) + var video = item as Video; + + if (video != null) { - FetchFromMediaInfoNode(subtree, item); + var val = reader.ReadElementContentAsString(); + + if (string.Equals("HSBS", val)) + { + video.Video3DFormat = Video3DFormat.HalfSideBySide; + } + else if (string.Equals("HTAB", val)) + { + video.Video3DFormat = Video3DFormat.HalfTopAndBottom; + } + else if (string.Equals("FTAB", val)) + { + video.Video3DFormat = Video3DFormat.FullTopAndBottom; + } + else if (string.Equals("FSBS", val)) + { + video.Video3DFormat = Video3DFormat.FullSideBySide; + } } break; } @@ -774,89 +794,6 @@ namespace MediaBrowser.Controller.Providers } /// <summary> - /// Fetches from media info node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="item">The item.</param> - private void FetchFromMediaInfoNode(XmlReader reader, T item) - { - reader.MoveToContent(); - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Video": - { - using (var subtree = reader.ReadSubtree()) - { - FetchFromMediaInfoVideoNode(subtree, item); - } - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - - /// <summary> - /// Fetches from media info video node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="item">The item.</param> - private void FetchFromMediaInfoVideoNode(XmlReader reader, T item) - { - reader.MoveToContent(); - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Format3D": - { - var video = item as Video; - - if (video != null) - { - var val = reader.ReadElementContentAsString(); - - if (string.Equals("HSBS", val)) - { - video.Video3DFormat = Video3DFormat.HalfSideBySide; - } - else if (string.Equals("HTAB", val)) - { - video.Video3DFormat = Video3DFormat.HalfTopAndBottom; - } - else if (string.Equals("FTAB", val)) - { - video.Video3DFormat = Video3DFormat.FullTopAndBottom; - } - else if (string.Equals("FSBS", val)) - { - video.Video3DFormat = Video3DFormat.FullSideBySide; - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - - /// <summary> /// Fetches from taglines node. /// </summary> /// <param name="reader">The reader.</param> diff --git a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs index 40afe0b54..67475b329 100644 --- a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs @@ -223,6 +223,11 @@ namespace MediaBrowser.Controller.Providers throw new ArgumentNullException("providerInfo"); } + if (providerInfo.LastRefreshed == default(DateTime)) + { + return true; + } + if (NeedsRefreshBasedOnCompareDate(item, providerInfo)) { return true; @@ -325,6 +330,16 @@ namespace MediaBrowser.Controller.Providers return !item.ResolveArgs.IsDirectory; } + protected virtual IEnumerable<BaseItem> GetItemsForFileStampComparison(BaseItem item) + { + if (UseParentFileSystemStamp(item) && item.Parent != null) + { + return new[] { item.Parent }; + } + + return new[] { item }; + } + /// <summary> /// Gets the item's current file system stamp /// </summary> @@ -332,12 +347,7 @@ namespace MediaBrowser.Controller.Providers /// <returns>Guid.</returns> private Guid GetCurrentFileSystemStamp(BaseItem item) { - if (UseParentFileSystemStamp(item) && item.Parent != null) - { - return GetFileSystemStamp(item.Parent); - } - - return GetFileSystemStamp(item); + return GetFileSystemStamp(GetItemsForFileStampComparison(item)); } private Dictionary<string, string> _fileStampExtensionsDictionary; @@ -355,44 +365,40 @@ namespace MediaBrowser.Controller.Providers /// <summary> /// Gets the file system stamp. /// </summary> - /// <param name="item">The item.</param> + /// <param name="items">The items.</param> /// <returns>Guid.</returns> - protected virtual Guid GetFileSystemStamp(BaseItem item) + protected virtual Guid GetFileSystemStamp(IEnumerable<BaseItem> items) { - // If there's no path or the item is a file, there's nothing to do - if (item.LocationType != LocationType.FileSystem) - { - return Guid.Empty; - } + var sb = new StringBuilder(); - ItemResolveArgs resolveArgs; + var extensions = FileStampExtensionsDictionary; + var numExtensions = FilestampExtensions.Length; - try + foreach (var item in items) { - resolveArgs = item.ResolveArgs; - } - catch (IOException ex) - { - Logger.ErrorException("Error determining if path is directory: {0}", ex, item.Path); - throw; + // If there's no path or the item is a file, there's nothing to do + if (item.LocationType == LocationType.FileSystem) + { + var resolveArgs = item.ResolveArgs; + + if (resolveArgs.IsDirectory) + { + // Record the name of each file + // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order + AddFiles(sb, resolveArgs.FileSystemChildren, extensions, numExtensions); + AddFiles(sb, resolveArgs.MetadataFiles, extensions, numExtensions); + } + } } - if (!resolveArgs.IsDirectory) + var stamp = sb.ToString(); + + if (string.IsNullOrEmpty(stamp)) { return Guid.Empty; } - var sb = new StringBuilder(); - - var extensions = FileStampExtensionsDictionary; - var numExtensions = FilestampExtensions.Length; - - // Record the name of each file - // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order - AddFiles(sb, resolveArgs.FileSystemChildren, extensions, numExtensions); - AddFiles(sb, resolveArgs.MetadataFiles, extensions, numExtensions); - - return sb.ToString().GetMD5(); + return stamp.GetMD5(); } private static readonly Dictionary<string, string> FoldersToMonitor = new[] { "extrafanart", "extrathumbs" } diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs index 54ba6d322..ae605ec0d 100644 --- a/MediaBrowser.Controller/Providers/IImageEnhancer.cs +++ b/MediaBrowser.Controller/Providers/IImageEnhancer.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Providers /// <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); + bool Supports(IHasImages item, ImageType imageType); /// <summary> /// Gets the priority or order in which this enhancer should be run. @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Providers /// <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); + string GetConfigurationCacheKey(IHasImages item, ImageType imageType); /// <summary> /// Gets the size of the enhanced image. @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Providers /// <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); + ImageSize GetEnhancedImageSize(IHasImages item, ImageType imageType, int imageIndex, ImageSize originalImageSize); /// <summary> /// Enhances the image async. @@ -49,6 +49,6 @@ namespace MediaBrowser.Controller.Providers /// <param name="imageIndex">Index of the image.</param> /// <returns>Task{Image}.</returns> /// <exception cref="System.ArgumentNullException"></exception> - Task<Image> EnhanceImageAsync(BaseItem item, Image originalImage, ImageType imageType, int imageIndex); + Task<Image> EnhanceImageAsync(IHasImages item, Image originalImage, ImageType imageType, int imageIndex); } }
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs index d70532b59..ccf199844 100644 --- a/MediaBrowser.Controller/Providers/IImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <param name="item">The item.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - bool Supports(BaseItem item); + bool Supports(IHasImages item); /// <summary> /// Gets the images. @@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Providers /// <param name="imageType">Type of the image.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> - Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken); + Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken); /// <summary> /// Gets the images. @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Providers /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> - Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken); + Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken); /// <summary> /// Gets the priority. diff --git a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs index 7d9739448..3cd38da45 100644 --- a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs +++ b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs @@ -133,6 +133,19 @@ namespace MediaBrowser.Controller.Resolvers /// <param name="includeCreationTime">if set to <c>true</c> [include creation time].</param> public static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args, bool includeCreationTime) { + if (fileSystem == null) + { + throw new ArgumentNullException("fileSystem"); + } + if (item == null) + { + throw new ArgumentNullException("item"); + } + if (args == null) + { + throw new ArgumentNullException("args"); + } + // See if a different path came out of the resolver than what went in if (!string.Equals(args.Path, item.Path, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Session/ISessionControllerFactory.cs b/MediaBrowser.Controller/Session/ISessionControllerFactory.cs new file mode 100644 index 000000000..92862e462 --- /dev/null +++ b/MediaBrowser.Controller/Session/ISessionControllerFactory.cs @@ -0,0 +1,16 @@ + +namespace MediaBrowser.Controller.Session +{ + /// <summary> + /// Interface ISesssionControllerFactory + /// </summary> + public interface ISessionControllerFactory + { + /// <summary> + /// Gets the session controller. + /// </summary> + /// <param name="session">The session.</param> + /// <returns>ISessionController.</returns> + ISessionController GetSessionController(SessionInfo session); + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 771d8f72e..ec138bfb4 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -35,16 +35,23 @@ namespace MediaBrowser.Controller.Session IEnumerable<SessionInfo> Sessions { get; } /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="sessionFactories">The session factories.</param> + void AddParts(IEnumerable<ISessionControllerFactory> sessionFactories); + + /// <summary> /// Logs the user activity. /// </summary> /// <param name="clientType">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> /// <returns>Task.</returns> /// <exception cref="System.ArgumentNullException">user</exception> - Task<SessionInfo> LogSessionActivity(string clientType, string appVersion, string deviceId, string deviceName, User user); + Task<SessionInfo> LogSessionActivity(string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user); /// <summary> /// Used to report that playback has started for an item diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index ed2fcda67..82e9328ac 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -15,6 +15,12 @@ namespace MediaBrowser.Controller.Session } /// <summary> + /// Gets or sets the remote end point. + /// </summary> + /// <value>The remote end point.</value> + public string RemoteEndPoint { get; set; } + + /// <summary> /// Gets or sets a value indicating whether this instance can seek. /// </summary> /// <value><c>true</c> if this instance can seek; otherwise, <c>false</c>.</value> diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index f6e2725a6..f8b12da9e 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -16,7 +16,7 @@ <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> - <FodyPath>..\packages\Fody.1.13.12</FodyPath> + <FodyPath>..\packages\Fody.1.19.1.0</FodyPath> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -77,6 +77,9 @@ <Compile Include="..\MediaBrowser.Model\Configuration\BaseApplicationConfiguration.cs"> <Link>Configuration\BaseApplicationConfiguration.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\Configuration\ImageDownloadOptions.cs"> + <Link>Configuration\ImageDownloadOptions.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\Configuration\ManualLoginCategory.cs"> <Link>Configuration\ManualLoginCategory.cs</Link> </Compile> @@ -146,9 +149,6 @@ <Compile Include="..\MediaBrowser.Model\Entities\IHasProviderIds.cs"> <Link>Entities\IHasProviderIds.cs</Link> </Compile> - <Compile Include="..\MediaBrowser.Model\Entities\ImageDownloadOptions.cs"> - <Link>Entities\ImageDownloadOptions.cs</Link> - </Compile> <Compile Include="..\MediaBrowser.Model\Entities\ImageType.cs"> <Link>Entities\ImageType.cs</Link> </Compile> @@ -239,6 +239,9 @@ <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramQuery.cs"> <Link>LiveTv\ProgramQuery.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingGroupDto.cs"> + <Link>LiveTv\RecordingGroupDto.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingInfoDto.cs"> <Link>LiveTv\RecordingInfoDto.cs</Link> </Compile> @@ -248,6 +251,9 @@ <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingStatus.cs"> <Link>LiveTv\RecordingStatus.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\LiveTv\SeriesTimerInfoDto.cs"> + <Link>LiveTv\SeriesTimerInfoDto.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\LiveTv\TimerInfoDto.cs"> <Link>LiveTv\TimerInfoDto.cs</Link> </Compile> diff --git a/MediaBrowser.Model.Portable/packages.config b/MediaBrowser.Model.Portable/packages.config index 23768650a..2d74129b8 100644 --- a/MediaBrowser.Model.Portable/packages.config +++ b/MediaBrowser.Model.Portable/packages.config @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Fody" version="1.13.12" targetFramework="portable-net45+sl40+wp71+win" /> + <package id="Fody" version="1.19.1.0" targetFramework="portable-win+net45+sl40+wp71" developmentDependency="true" /> <package id="Microsoft.Bcl" version="1.0.19" targetFramework="portable-win+net45+sl40+wp71" /> <package id="Microsoft.Bcl.Async" version="1.0.16" targetFramework="portable-win+net45+sl40+wp71" /> <package id="Microsoft.Bcl.Build" version="1.0.8" targetFramework="portable-win+net45+sl40+wp71" /> diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index efafc7f62..d69cf56c6 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -64,6 +64,9 @@ <Compile Include="..\MediaBrowser.Model\Configuration\BaseApplicationConfiguration.cs"> <Link>Configuration\BaseApplicationConfiguration.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\Configuration\ImageDownloadOptions.cs"> + <Link>Configuration\ImageDownloadOptions.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\Configuration\ManualLoginCategory.cs"> <Link>Configuration\ManualLoginCategory.cs</Link> </Compile> @@ -133,9 +136,6 @@ <Compile Include="..\MediaBrowser.Model\Entities\IHasProviderIds.cs"> <Link>Entities\IHasProviderIds.cs</Link> </Compile> - <Compile Include="..\MediaBrowser.Model\Entities\ImageDownloadOptions.cs"> - <Link>Entities\ImageDownloadOptions.cs</Link> - </Compile> <Compile Include="..\MediaBrowser.Model\Entities\ImageType.cs"> <Link>Entities\ImageType.cs</Link> </Compile> @@ -226,6 +226,9 @@ <Compile Include="..\MediaBrowser.Model\LiveTv\ProgramQuery.cs"> <Link>LiveTv\ProgramQuery.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingGroupDto.cs"> + <Link>LiveTv\RecordingGroupDto.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingInfoDto.cs"> <Link>LiveTv\RecordingInfoDto.cs</Link> </Compile> @@ -235,6 +238,9 @@ <Compile Include="..\MediaBrowser.Model\LiveTv\RecordingStatus.cs"> <Link>LiveTv\RecordingStatus.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\LiveTv\SeriesTimerInfoDto.cs"> + <Link>LiveTv\SeriesTimerInfoDto.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\LiveTv\TimerInfoDto.cs"> <Link>LiveTv\TimerInfoDto.cs</Link> </Compile> diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs index b99fefcca..19620890e 100644 --- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs +++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs @@ -43,7 +43,13 @@ namespace MediaBrowser.Model.Configuration /// </summary> /// <value><c>true</c> if this instance is first run; otherwise, <c>false</c>.</value> public bool IsStartupWizardCompleted { get; set; } - + + /// <summary> + /// Gets or sets the cache path. + /// </summary> + /// <value>The cache path.</value> + public string CachePath { get; set; } + /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationConfiguration" /> class. /// </summary> diff --git a/MediaBrowser.Model/Entities/ImageDownloadOptions.cs b/MediaBrowser.Model/Configuration/ImageDownloadOptions.cs index 92e989a34..603112110 100644 --- a/MediaBrowser.Model/Entities/ImageDownloadOptions.cs +++ b/MediaBrowser.Model/Configuration/ImageDownloadOptions.cs @@ -1,5 +1,5 @@ -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Configuration { /// <summary> /// Class ImageDownloadOptions @@ -62,4 +62,20 @@ namespace MediaBrowser.Model.Entities Banner = true; } } + + /// <summary> + /// Class MetadataOptions. + /// </summary> + public class MetadataOptions + { + public int MaxBackdrops { get; set; } + + public int MinBackdropWidth { get; set; } + + public MetadataOptions() + { + MaxBackdrops = 3; + MinBackdropWidth = 1280; + } + } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 1f75969be..c8c205404 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Weather; +using MediaBrowser.Model.Weather; using System; namespace MediaBrowser.Model.Configuration @@ -88,12 +87,6 @@ namespace MediaBrowser.Model.Configuration public string MetadataCountryCode { get; set; } /// <summary> - /// Gets or sets the max backdrops. - /// </summary> - /// <value>The max backdrops.</value> - public int MaxBackdrops { get; set; } - - /// <summary> /// Options for specific art to download for movies. /// </summary> public ImageDownloadOptions DownloadMovieImages { get; set; } @@ -143,12 +136,6 @@ namespace MediaBrowser.Model.Configuration public bool ShowLogWindow { get; set; } /// <summary> - /// The list of types that will NOT be allowed to have internet providers run against them even if they are turned on. - /// </summary> - /// <value>The internet provider exclude types.</value> - public string[] InternetProviderExcludeTypes { get; set; } - - /// <summary> /// Gets or sets the recent item days. /// </summary> /// <value>The recent item days.</value> @@ -181,12 +168,6 @@ namespace MediaBrowser.Model.Configuration public int FileWatcherDelay { get; set; } /// <summary> - /// Gets or sets a value indicating whether [enable developer tools]. - /// </summary> - /// <value><c>true</c> if [enable developer tools]; otherwise, <c>false</c>.</value> - public bool EnableDeveloperTools { get; set; } - - /// <summary> /// Gets or sets a value indicating whether [enable dashboard response caching]. /// Allows potential contributors without visual studio to modify production dashboard code and test changes. /// </summary> @@ -217,22 +198,26 @@ namespace MediaBrowser.Model.Configuration public ImageSavingConvention ImageSavingConvention { get; set; } /// <summary> - /// Gets or sets the width of the min movie backdrop. + /// Gets or sets a value indicating whether [enable people prefix sub folders]. /// </summary> - /// <value>The width of the min movie backdrop.</value> - public int MinMovieBackdropDownloadWidth { get; set; } + /// <value><c>true</c> if [enable people prefix sub folders]; otherwise, <c>false</c>.</value> + public bool EnablePeoplePrefixSubFolders { get; set; } /// <summary> - /// Gets or sets the width of the min series backdrop. + /// Gets or sets the encoding quality. /// </summary> - /// <value>The width of the min series backdrop.</value> - public int MinSeriesBackdropDownloadWidth { get; set; } + /// <value>The encoding quality.</value> + public EncodingQuality EncodingQuality { get; set; } - /// <summary> - /// Gets or sets a value indicating whether [enable people prefix sub folders]. - /// </summary> - /// <value><c>true</c> if [enable people prefix sub folders]; otherwise, <c>false</c>.</value> - public bool EnablePeoplePrefixSubFolders { get; set; } + public bool EnableMovieChapterImageExtraction { get; set; } + public bool EnableEpisodeChapterImageExtraction { get; set; } + public bool EnableOtherVideoChapterImageExtraction { get; set; } + + public MetadataOptions MovieOptions { get; set; } + public MetadataOptions TvOptions { get; set; } + public MetadataOptions MusicOptions { get; set; } + public MetadataOptions GameOptions { get; set; } + public MetadataOptions BookOptions { get; set; } /// <summary> /// Initializes a new instance of the <see cref="ServerConfiguration" /> class. @@ -247,9 +232,9 @@ namespace MediaBrowser.Model.Configuration EnableDashboardResponseCaching = true; EnableVideoImageExtraction = true; -#if (DEBUG) - EnableDeveloperTools = true; -#endif + EnableMovieChapterImageExtraction = true; + EnableEpisodeChapterImageExtraction = false; + EnableOtherVideoChapterImageExtraction = false; MinResumePct = 5; MaxResumePct = 90; @@ -260,7 +245,6 @@ namespace MediaBrowser.Model.Configuration RecentItemDays = 10; EnableInternetProviders = true; //initial installs will need these - InternetProviderExcludeTypes = new string[] { }; ManualLoginClients = new ManualLoginCategory[] { }; @@ -275,7 +259,6 @@ namespace MediaBrowser.Model.Configuration }; DownloadMusicArtistImages = new ImageDownloadOptions(); DownloadMusicAlbumImages = new ImageDownloadOptions(); - MaxBackdrops = 3; SortReplaceCharacters = new[] { ".", "+", "%" }; SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; @@ -283,8 +266,20 @@ namespace MediaBrowser.Model.Configuration SeasonZeroDisplayName = "Specials"; - MinMovieBackdropDownloadWidth = 1280; - MinSeriesBackdropDownloadWidth = 1280; + MovieOptions = new MetadataOptions(); + TvOptions = new MetadataOptions(); + + MusicOptions = new MetadataOptions() + { + MaxBackdrops = 1 + }; + + GameOptions = new MetadataOptions(); + + BookOptions = new MetadataOptions + { + MaxBackdrops = 1 + }; } } @@ -293,4 +288,12 @@ namespace MediaBrowser.Model.Configuration Legacy, Compatible } + + public enum EncodingQuality + { + Auto, + HighSpeed, + HighQuality, + MaxQuality + } } diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index b736474e0..90accff94 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -60,6 +60,13 @@ namespace MediaBrowser.Model.Configuration public bool DisplayUnairedEpisodes { get; set; } public bool EnableRemoteControlOfOtherUsers { get; set; } + public bool BlockUnratedMovies { get; set; } + public bool BlockUnratedTrailers { get; set; } + public bool BlockUnratedSeries { get; set; } + public bool BlockUnratedMusic { get; set; } + public bool BlockUnratedGames { get; set; } + public bool BlockUnratedBooks { get; set; } + /// <summary> /// Initializes a new instance of the <see cref="UserConfiguration" /> class. /// </summary> diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 20f60586c..9adfcfa99 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -30,11 +30,25 @@ namespace MediaBrowser.Model.Dto /// <value>The date created.</value> public DateTime? DateCreated { get; set; } + public int? AirsBeforeSeasonNumber { get; set; } + public int? AirsAfterSeasonNumber { get; set; } + public int? AirsBeforeEpisodeNumber { get; set; } + public int? AbsoluteEpisodeNumber { get; set; } + public bool? DisplaySpecialsWithSeasons { get; set; } + + public string PreferredMetadataLanguage { get; set; } + public string PreferredMetadataCountryCode { get; set; } + + /// <summary> + /// Gets or sets the DVD season number. + /// </summary> + /// <value>The DVD season number.</value> + public int? DvdSeasonNumber { get; set; } /// <summary> - /// Gets or sets the special season number. + /// Gets or sets the DVD episode number. /// </summary> - /// <value>The special season number.</value> - public int? SpecialSeasonNumber { get; set; } + /// <value>The DVD episode number.</value> + public float? DvdEpisodeNumber { get; set; } /// <summary> /// Gets or sets the name of the sort. @@ -77,7 +91,7 @@ namespace MediaBrowser.Model.Dto /// </summary> /// <value>The path.</value> public string Path { get; set; } - + /// <summary> /// Gets or sets the official rating. /// </summary> @@ -199,12 +213,6 @@ namespace MediaBrowser.Model.Dto public Dictionary<string, string> ProviderIds { get; set; } /// <summary> - /// Gets or sets the language. - /// </summary> - /// <value>The language.</value> - public string Language { get; set; } - - /// <summary> /// Gets or sets a value indicating whether this instance is HD. /// </summary> /// <value><c>null</c> if [is HD] contains no value, <c>true</c> if [is HD]; otherwise, <c>false</c>.</value> diff --git a/MediaBrowser.Model/Entities/MetadataFields.cs b/MediaBrowser.Model/Entities/MetadataFields.cs index 85f2da31e..a99fd0fe0 100644 --- a/MediaBrowser.Model/Entities/MetadataFields.cs +++ b/MediaBrowser.Model/Entities/MetadataFields.cs @@ -41,6 +41,18 @@ namespace MediaBrowser.Model.Entities /// <summary> /// The official rating /// </summary> - OfficialRating + OfficialRating, + /// <summary> + /// The images + /// </summary> + Images, + /// <summary> + /// The backdrops + /// </summary> + Backdrops, + /// <summary> + /// The screenshots + /// </summary> + Screenshots } } diff --git a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs index 020771e5e..89c92e6fd 100644 --- a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs @@ -23,6 +23,12 @@ namespace MediaBrowser.Model.LiveTv public string Id { get; set; } /// <summary> + /// Gets or sets the external identifier. + /// </summary> + /// <value>The external identifier.</value> + public string ExternalId { get; set; } + + /// <summary> /// Gets or sets the image tags. /// </summary> /// <value>The image tags.</value> diff --git a/MediaBrowser.Model/LiveTv/ProgramInfoDto.cs b/MediaBrowser.Model/LiveTv/ProgramInfoDto.cs index 5f35de086..0798a2294 100644 --- a/MediaBrowser.Model/LiveTv/ProgramInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/ProgramInfoDto.cs @@ -1,4 +1,6 @@ -using System; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; using System.Collections.Generic; namespace MediaBrowser.Model.LiveTv @@ -11,6 +13,18 @@ namespace MediaBrowser.Model.LiveTv public string Id { get; set; } /// <summary> + /// Gets or sets the timer identifier. + /// </summary> + /// <value>The timer identifier.</value> + public string TimerId { 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 external identifier. /// </summary> /// <value>The external identifier.</value> @@ -23,16 +37,16 @@ namespace MediaBrowser.Model.LiveTv public string ChannelId { get; set; } /// <summary> + /// Gets or sets the name of the channel. + /// </summary> + /// <value>The name of the channel.</value> + public string ChannelName { get; set; } + + /// <summary> /// Gets or sets the community rating. /// </summary> /// <value>The community rating.</value> public float? CommunityRating { get; set; } - - /// <summary> - /// Gets or sets the aspect ratio. - /// </summary> - /// <value>The aspect ratio.</value> - public string AspectRatio { get; set; } /// <summary> /// Gets or sets the official rating. @@ -101,14 +115,85 @@ namespace MediaBrowser.Model.LiveTv /// <value>The episode title.</value> public string EpisodeTitle { get; set; } + /// <summary> + /// Gets or sets the image tags. + /// </summary> + /// <value>The image tags.</value> + public Dictionary<ImageType, Guid> ImageTags { get; set; } + + /// <summary> + /// Gets or sets the user data. + /// </summary> + /// <value>The user data.</value> + public UserItemDataDto UserData { 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 the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + public long? RunTimeTicks { 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; } + public ProgramInfoDto() { Genres = new List<string>(); + ImageTags = new Dictionary<ImageType, Guid>(); } } public enum ProgramAudio { - Stereo + Mono, + Stereo, + Dolby, + DolbyDigital, + Thx } }
\ No newline at end of file diff --git a/MediaBrowser.Model/LiveTv/RecordingGroupDto.cs b/MediaBrowser.Model/LiveTv/RecordingGroupDto.cs new file mode 100644 index 000000000..29f0824fb --- /dev/null +++ b/MediaBrowser.Model/LiveTv/RecordingGroupDto.cs @@ -0,0 +1,27 @@ + +namespace MediaBrowser.Model.LiveTv +{ + /// <summary> + /// Class RecordingGroupDto. + /// </summary> + public class RecordingGroupDto + { + /// <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 recording count. + /// </summary> + /// <value>The recording count.</value> + public int RecordingCount { get; set; } + } +} diff --git a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs index a095e1751..47accbec5 100644 --- a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.LiveTv { @@ -11,6 +13,12 @@ namespace MediaBrowser.Model.LiveTv 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 external identifier. /// </summary> /// <value>The external identifier.</value> @@ -33,6 +41,12 @@ namespace MediaBrowser.Model.LiveTv public string ChannelName { get; set; } /// <summary> + /// Gets or sets the name of the service. + /// </summary> + /// <value>The name of the service.</value> + public string ServiceName { get; set; } + + /// <summary> /// Name of the recording. /// </summary> public string Name { get; set; } @@ -44,6 +58,12 @@ namespace MediaBrowser.Model.LiveTv public string Path { get; set; } /// <summary> + /// Gets or sets the URL. + /// </summary> + /// <value>The URL.</value> + public string Url { get; set; } + + /// <summary> /// Overview of the recording. /// </summary> public string Overview { get; set; } @@ -65,6 +85,12 @@ namespace MediaBrowser.Model.LiveTv public RecordingStatus Status { get; set; } /// <summary> + /// Gets or sets the name of the status. + /// </summary> + /// <value>The name of the status.</value> + public string StatusName { get; set; } + + /// <summary> /// Genre of the program. /// </summary> public List<string> Genres { get; set; } @@ -82,10 +108,10 @@ namespace MediaBrowser.Model.LiveTv public string EpisodeTitle { get; set; } /// <summary> - /// Gets or sets the duration ms. + /// Gets or sets the run time ticks. /// </summary> - /// <value>The duration ms.</value> - public int DurationMs { get; set; } + /// <value>The run time ticks.</value> + public long? RunTimeTicks { get; set; } /// <summary> /// Gets or sets the type of the media. @@ -123,9 +149,70 @@ namespace MediaBrowser.Model.LiveTv /// <value>The audio.</value> public ProgramAudio? Audio { 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 image tags. + /// </summary> + /// <value>The image tags.</value> + public Dictionary<ImageType, Guid> ImageTags { get; set; } + + /// <summary> + /// Gets or sets the user data. + /// </summary> + /// <value>The user data.</value> + public UserItemDataDto UserData { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + public RecordingInfoDto() { Genres = new List<string>(); + ImageTags = new Dictionary<ImageType, Guid>(); } } }
\ No newline at end of file diff --git a/MediaBrowser.Model/LiveTv/RecordingQuery.cs b/MediaBrowser.Model/LiveTv/RecordingQuery.cs index cd5ebe628..e63a250e6 100644 --- a/MediaBrowser.Model/LiveTv/RecordingQuery.cs +++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs @@ -10,6 +10,45 @@ /// </summary> /// <value>The channel identifier.</value> public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the user identifier. + /// </summary> + /// <value>The user identifier.</value> + public string UserId { get; set; } + + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + public string GroupId { get; set; } + + /// <summary> + /// Skips over a given number of items within the results. Use for paging. + /// </summary> + /// <value>The start index.</value> + public int? StartIndex { get; set; } + + /// <summary> + /// The maximum number of items to return + /// </summary> + /// <value>The limit.</value> + public int? Limit { get; set; } + } + + public class RecordingGroupQuery + { + /// <summary> + /// Gets or sets the user identifier. + /// </summary> + /// <value>The user identifier.</value> + public string UserId { get; set; } } public class TimerQuery @@ -20,4 +59,8 @@ /// <value>The channel identifier.</value> public string ChannelId { get; set; } } + + public class SeriesTimerQuery + { + } } diff --git a/MediaBrowser.Model/LiveTv/RecordingStatus.cs b/MediaBrowser.Model/LiveTv/RecordingStatus.cs index 08a7cfb0c..95e9dcb01 100644 --- a/MediaBrowser.Model/LiveTv/RecordingStatus.cs +++ b/MediaBrowser.Model/LiveTv/RecordingStatus.cs @@ -3,20 +3,21 @@ namespace MediaBrowser.Model.LiveTv { public enum RecordingStatus { - Pending, + New, + Scheduled, InProgress, Completed, - CompletedWithError, - Conflicted, - Deleted + Aborted, + Cancelled, + ConflictedOk, + ConflictedNotOk, + Error } - public enum RecurrenceType + public enum DayPattern { - Manual, - NewProgramEventsOneChannel, - AllProgramEventsOneChannel, - NewProgramEventsAllChannels, - AllProgramEventsAllChannels + Daily, + Weekdays, + Weekends } } diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs new file mode 100644 index 000000000..a8c6a2e37 --- /dev/null +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Model.LiveTv +{ + public class SeriesTimerInfoDto + { + /// <summary> + /// Id of the recording. + /// </summary> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the external identifier. + /// </summary> + /// <value>The external identifier.</value> + public string ExternalId { get; set; } + + /// <summary> + /// ChannelId of the recording. + /// </summary> + public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the name of the service. + /// </summary> + /// <value>The name of the service.</value> + public string ServiceName { get; set; } + + /// <summary> + /// Gets or sets the external channel identifier. + /// </summary> + /// <value>The external channel identifier.</value> + public string ExternalChannelId { get; set; } + + /// <summary> + /// ChannelName of the recording. + /// </summary> + public string ChannelName { 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 external program identifier. + /// </summary> + /// <value>The external program identifier.</value> + public string ExternalProgramId { get; set; } + + /// <summary> + /// Name of the recording. + /// </summary> + public string Name { 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; } + + /// <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 day pattern. + /// </summary> + /// <value>The day pattern.</value> + public DayPattern? DayPattern { 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; } + + public SeriesTimerInfoDto() + { + Days = new List<DayOfWeek>(); + } + } +} diff --git a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs index e8085fc92..507ba0947 100644 --- a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs @@ -21,17 +21,35 @@ namespace MediaBrowser.Model.LiveTv public string ChannelId { get; set; } /// <summary> + /// Gets or sets the external channel identifier. + /// </summary> + /// <value>The external channel identifier.</value> + public string ExternalChannelId { get; set; } + + /// <summary> /// ChannelName of the recording. /// </summary> public string ChannelName { get; set; } /// <summary> + /// Gets or sets the name of the service. + /// </summary> + /// <value>The name of the service.</value> + public string ServiceName { 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 external program identifier. + /// </summary> + /// <value>The external program identifier.</value> + public string ExternalProgramId { get; set; } + + /// <summary> /// Name of the recording. /// </summary> public string Name { get; set; } @@ -39,7 +57,7 @@ namespace MediaBrowser.Model.LiveTv /// <summary> /// Description of the recording. /// </summary> - public string Description { get; set; } + public string Overview { get; set; } /// <summary> /// The start date of the recording, in UTC. @@ -64,33 +82,51 @@ namespace MediaBrowser.Model.LiveTv public string SeriesTimerId { get; set; } /// <summary> - /// Gets or sets the requested pre padding seconds. + /// Gets or sets the external series timer identifier. + /// </summary> + /// <value>The external series timer identifier.</value> + public string ExternalSeriesTimerId { 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>The requested pre padding seconds.</value> - public int RequestedPrePaddingSeconds { get; set; } + /// <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 the requested post padding seconds. + /// Gets or sets a value indicating whether this instance is post padding required. /// </summary> - /// <value>The requested post padding seconds.</value> - public int RequestedPostPaddingSeconds { get; set; } + /// <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 required pre padding seconds. + /// Gets or sets the run time ticks. /// </summary> - /// <value>The required pre padding seconds.</value> - public int RequiredPrePaddingSeconds { get; set; } + /// <value>The run time ticks.</value> + public long? RunTimeTicks { get; set; } /// <summary> - /// Gets or sets the required post padding seconds. + /// Gets or sets the priority. /// </summary> - /// <value>The required post padding seconds.</value> - public int RequiredPostPaddingSeconds { get; set; } + /// <value>The priority.</value> + public int Priority { get; set; } /// <summary> - /// Gets or sets the duration ms. + /// Gets or sets the program information. /// </summary> - /// <value>The duration ms.</value> - public int DurationMs { get; set; } + /// <value>The program information.</value> + public ProgramInfoDto ProgramInfo { get; set; } } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 5175bee91..ab91416b7 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -9,13 +9,13 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Model</RootNamespace> <AssemblyName>MediaBrowser.Model</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> - <FodyPath>..\packages\Fody.1.17.0.0</FodyPath> + <FodyPath>..\packages\Fody.1.19.1.0</FodyPath> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -26,6 +26,7 @@ <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <PlatformTarget>AnyCPU</PlatformTarget> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -34,6 +35,16 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release Mono\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup> <RunPostBuildEvent>Always</RunPostBuildEvent> @@ -64,8 +75,10 @@ <Compile Include="LiveTv\ChannelQuery.cs" /> <Compile Include="LiveTv\ProgramInfoDto.cs" /> <Compile Include="LiveTv\ProgramQuery.cs" /> + <Compile Include="LiveTv\RecordingGroupDto.cs" /> <Compile Include="LiveTv\RecordingQuery.cs" /> <Compile Include="LiveTv\RecordingStatus.cs" /> + <Compile Include="LiveTv\SeriesTimerInfoDto.cs" /> <Compile Include="LiveTv\TimerInfoDto.cs" /> <Compile Include="Providers\ImageProviderInfo.cs" /> <Compile Include="Providers\RemoteImageInfo.cs" /> @@ -106,7 +119,7 @@ <Compile Include="Session\MessageCommand.cs" /> <Compile Include="Session\PlayRequest.cs" /> <Compile Include="Session\PlaystateCommand.cs" /> - <Compile Include="Entities\ImageDownloadOptions.cs" /> + <Compile Include="Configuration\ImageDownloadOptions.cs" /> <Compile Include="Logging\ILogManager.cs" /> <Compile Include="MediaInfo\BlurayDiscInfo.cs" /> <Compile Include="Entities\ChapterInfo.cs" /> @@ -194,11 +207,11 @@ </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> - <PostBuildEvent>if $(ConfigurationName) == Release ( + <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' ( xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\net45\" /y /d /r /i )</PostBuildEvent> </PropertyGroup> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> + <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <Import Project="Fody.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 71c3d59cd..6d61d8ac7 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Model.Querying /// <summary> /// The metadata settings /// </summary> - MetadataSettings, + Settings, /// <summary> /// The original run time ticks diff --git a/MediaBrowser.Model/Querying/ItemQuery.cs b/MediaBrowser.Model/Querying/ItemQuery.cs index 6602e031f..afc0540ef 100644 --- a/MediaBrowser.Model/Querying/ItemQuery.cs +++ b/MediaBrowser.Model/Querying/ItemQuery.cs @@ -141,12 +141,6 @@ namespace MediaBrowser.Model.Querying public string SearchTerm { get; set; } /// <summary> - /// The dynamic, localized index function name - /// </summary> - /// <value>The index by.</value> - public string IndexBy { get; set; } - - /// <summary> /// Gets or sets the image types. /// </summary> /// <value>The image types.</value> diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 1e16b0492..bebe23734 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -32,6 +32,12 @@ namespace MediaBrowser.Model.Search public int? IndexNumber { 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 parent index number. /// </summary> /// <value>The parent index number.</value> diff --git a/MediaBrowser.Model/Session/SessionInfoDto.cs b/MediaBrowser.Model/Session/SessionInfoDto.cs index 02b7f0226..80f6ea2c0 100644 --- a/MediaBrowser.Model/Session/SessionInfoDto.cs +++ b/MediaBrowser.Model/Session/SessionInfoDto.cs @@ -14,6 +14,12 @@ namespace MediaBrowser.Model.Session public bool CanSeek { 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 queueable media types. /// </summary> /// <value>The queueable media types.</value> diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 6a17ad133..d475517dc 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -99,6 +99,12 @@ namespace MediaBrowser.Model.System public string ItemsByNamePath { get; set; } /// <summary> + /// Gets or sets the cache path. + /// </summary> + /// <value>The cache path.</value> + public string CachePath { get; set; } + + /// <summary> /// Gets or sets the log path. /// </summary> /// <value>The log path.</value> @@ -111,6 +117,12 @@ namespace MediaBrowser.Model.System public int HttpServerPortNumber { get; set; } /// <summary> + /// Gets or sets the wan address. + /// </summary> + /// <value>The wan address.</value> + public string WanAddress { get; set; } + + /// <summary> /// Initializes a new instance of the <see cref="SystemInfo" /> class. /// </summary> public SystemInfo() diff --git a/MediaBrowser.Model/packages.config b/MediaBrowser.Model/packages.config index 622e6f72f..3d7793afb 100644 --- a/MediaBrowser.Model/packages.config +++ b/MediaBrowser.Model/packages.config @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Fody" version="1.17.0.0" targetFramework="net45" /> + <package id="Fody" version="1.19.1.0" targetFramework="net45" developmentDependency="true" /> <package id="PropertyChanged.Fody" version="1.41.0.0" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/MediaBrowser.Mono.sln b/MediaBrowser.Mono.sln index 397763489..c951fda72 100644 --- a/MediaBrowser.Mono.sln +++ b/MediaBrowser.Mono.sln @@ -24,64 +24,83 @@ Global Debug|x86 = Debug|x86 Release|x86 = Release|x86 Release|Any CPU = Release|Any CPU + Release Mono|Any CPU = Release Mono|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {175A9388-F352-4586-A6B4-070DED62B644}.Debug|x86.ActiveCfg = Debug|x86 {175A9388-F352-4586-A6B4-070DED62B644}.Debug|x86.Build.0 = Debug|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {175A9388-F352-4586-A6B4-070DED62B644}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {175A9388-F352-4586-A6B4-070DED62B644}.Release|Any CPU.ActiveCfg = Release|Any CPU {175A9388-F352-4586-A6B4-070DED62B644}.Release|Any CPU.Build.0 = Release|Any CPU {175A9388-F352-4586-A6B4-070DED62B644}.Release|x86.ActiveCfg = Release|x86 {175A9388-F352-4586-A6B4-070DED62B644}.Release|x86.Build.0 = Release|x86 - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.ActiveCfg = Debug|x86 - {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.Build.0 = Debug|x86 + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.Build.0 = Debug|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.ActiveCfg = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.Build.0 = Release|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|x86.ActiveCfg = Debug|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|x86.Build.0 = Debug|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Release|Any CPU.Build.0 = Release|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Release|x86.ActiveCfg = Release|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Release|x86.Build.0 = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|x86.ActiveCfg = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|x86.Build.0 = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.Build.0 = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|x86.ActiveCfg = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|x86.Build.0 = Release|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|x86.ActiveCfg = Debug|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|x86.Build.0 = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x86.ActiveCfg = Release|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x86.Build.0 = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|x86.ActiveCfg = Debug|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|x86.Build.0 = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.ActiveCfg = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.Build.0 = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.ActiveCfg = Debug|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.Build.0 = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.Build.0 = Release|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|x86.ActiveCfg = Debug|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|x86.Build.0 = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.ActiveCfg = Release|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.Build.0 = Release|Any CPU {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Debug|x86.ActiveCfg = Debug|Any CPU {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Debug|x86.Build.0 = Debug|Any CPU + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|Any CPU.Build.0 = Release|Any CPU {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.ActiveCfg = Release|Any CPU {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = MediaBrowser.Model\MediaBrowser.Model.csproj + StartupItem = MediaBrowser.Server.Mono\MediaBrowser.Server.Mono.csproj EndGlobalSection EndGlobal diff --git a/MediaBrowser.Mono.userprefs b/MediaBrowser.Mono.userprefs index 51ba9d804..1815e61ca 100644 --- a/MediaBrowser.Mono.userprefs +++ b/MediaBrowser.Mono.userprefs @@ -1,22 +1,9 @@ <Properties> - <MonoDevelop.Ide.Workspace ActiveConfiguration="Release" /> + <MonoDevelop.Ide.Workspace ActiveConfiguration="Release Mono" /> <MonoDevelop.Ide.Workbench ActiveDocument="MediaBrowser.Server.Mono\app.config"> <Files> - <File FileName="MediaBrowser.Server.Mono\app.config" Line="3" Column="19" /> + <File FileName="MediaBrowser.Server.Mono\app.config" Line="5" Column="20" /> </Files> - <Pads> - <Pad Id="ProjectPad"> - <State expanded="True" selected="True"> - <Node name="MediaBrowser.Server.Implementations" expanded="True" /> - </State> - </Pad> - <Pad Id="ClassPad"> - <State expanded="True" selected="True" /> - </Pad> - <Pad Id="MonoDevelop.Debugger.WatchPad"> - <State /> - </Pad> - </Pads> </MonoDevelop.Ide.Workbench> <MonoDevelop.Ide.DebuggingService.Breakpoints> <BreakpointStore> diff --git a/MediaBrowser.Providers/CollectionFolderImageProvider.cs b/MediaBrowser.Providers/CollectionFolderImageProvider.cs index 45b1b36c2..6c36dbf7e 100644 --- a/MediaBrowser.Providers/CollectionFolderImageProvider.cs +++ b/MediaBrowser.Providers/CollectionFolderImageProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.IO; +using System.Collections.Generic; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -35,9 +36,9 @@ namespace MediaBrowser.Providers .FirstOrDefault(i => i != null); } - protected override Guid GetFileSystemStamp(BaseItem item) + protected override Guid GetFileSystemStamp(IEnumerable<BaseItem> items) { - var files = item.ResolveArgs.PhysicalLocations + var files = items.SelectMany(i => i.ResolveArgs.PhysicalLocations) .Select(i => new DirectoryInfo(i)) .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) .Where(i => diff --git a/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs index 37d39f3d9..0b6accf33 100644 --- a/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs +++ b/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs @@ -58,6 +58,21 @@ namespace MediaBrowser.Providers return false; } + protected override IEnumerable<BaseItem> GetItemsForFileStampComparison(BaseItem item) + { + var season = item as Season; + if (season != null) + { + var series = season.Series; + if (series != null) + { + return new[] { item, series }; + } + } + + return base.GetItemsForFileStampComparison(item); + } + /// <summary> /// Gets the priority. /// </summary> @@ -197,7 +212,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Logo, image.FullName); + item.SetImagePath(ImageType.Logo, image.FullName); } // Clearart @@ -205,7 +220,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Art, image.FullName); + item.SetImagePath(ImageType.Art, image.FullName); } // Disc @@ -214,7 +229,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Disc, image.FullName); + item.SetImagePath(ImageType.Disc, image.FullName); } // Box Image @@ -222,7 +237,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Box, image.FullName); + item.SetImagePath(ImageType.Box, image.FullName); } // BoxRear Image @@ -230,7 +245,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.BoxRear, image.FullName); + item.SetImagePath(ImageType.BoxRear, image.FullName); } // Thumbnail Image @@ -238,7 +253,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Menu, image.FullName); + item.SetImagePath(ImageType.Menu, image.FullName); } PopulateBanner(item, args); @@ -296,7 +311,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Primary, image.FullName); + item.SetImagePath(ImageType.Primary, image.FullName); } } @@ -324,7 +339,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Banner, image.FullName); + item.SetImagePath(ImageType.Banner, image.FullName); } } @@ -336,7 +351,8 @@ namespace MediaBrowser.Providers private void PopulateThumb(BaseItem item, ItemResolveArgs args) { // Thumbnail Image - var image = GetImage(item, args, "thumb"); + var image = GetImage(item, args, "thumb") ?? + GetImage(item, args, "landscape"); if (image == null) { @@ -352,7 +368,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Thumb, image.FullName); + item.SetImagePath(ImageType.Thumb, image.FullName); } } diff --git a/MediaBrowser.Providers/ImagesByNameProvider.cs b/MediaBrowser.Providers/ImagesByNameProvider.cs index 590430823..8c5636580 100644 --- a/MediaBrowser.Providers/ImagesByNameProvider.cs +++ b/MediaBrowser.Providers/ImagesByNameProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.IO; +using System.Collections.Generic; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -69,9 +70,9 @@ namespace MediaBrowser.Providers return GetImageFromLocation(location, filenameWithoutExtension); } - protected override Guid GetFileSystemStamp(BaseItem item) + protected override Guid GetFileSystemStamp(IEnumerable<BaseItem> items) { - var location = GetLocation(item); + var location = GetLocation(items.First()); try { diff --git a/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs b/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs index b0bc1b875..8ee2553d0 100644 --- a/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs +++ b/MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Providers.LiveTv /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> public override bool Supports(BaseItem item) { - return item is Channel; + return item is LiveTvChannel; } /// <summary> @@ -74,7 +74,7 @@ namespace MediaBrowser.Providers.LiveTv try { - new BaseItemXmlParser<Channel>(Logger).Fetch((Channel)item, path, cancellationToken); + new BaseItemXmlParser<LiveTvChannel>(Logger).Fetch((LiveTvChannel)item, path, cancellationToken); } finally { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index b5b41c6d3..94d171ce1 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Providers</RootNamespace> <AssemblyName>MediaBrowser.Providers</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -24,6 +24,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -32,6 +33,16 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release Mono\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <ItemGroup> <Reference Include="System" /> @@ -75,7 +86,6 @@ <Compile Include="Movies\MovieProviderFromXml.cs" /> <Compile Include="Movies\OpenMovieDatabaseProvider.cs" /> <Compile Include="Movies\PersonProviderFromXml.cs" /> - <Compile Include="Movies\PersonUpdatesPreScanTask.cs" /> <Compile Include="Movies\MovieDbPersonProvider.cs" /> <Compile Include="Music\AlbumInfoFromSongProvider.cs" /> <Compile Include="Music\AlbumProviderFromXml.cs" /> @@ -110,6 +120,8 @@ <Compile Include="Savers\SeasonXmlSaver.cs" /> <Compile Include="Savers\SeriesXmlSaver.cs" /> <Compile Include="Savers\XmlSaverHelpers.cs" /> + <Compile Include="Studios\StudioImageProvider.cs" /> + <Compile Include="Studios\StudiosManualImageProvider.cs" /> <Compile Include="TV\EpisodeImageFromMediaLocationProvider.cs" /> <Compile Include="TV\EpisodeIndexNumberProvider.cs" /> <Compile Include="TV\EpisodeProviderFromXml.cs" /> @@ -123,6 +135,7 @@ <Compile Include="TV\ManualTvdbPersonImageProvider.cs" /> <Compile Include="TV\ManualTvdbSeasonImageProvider.cs" /> <Compile Include="TV\ManualTvdbSeriesImageProvider.cs" /> + <Compile Include="TV\SeasonIndexNumberProvider.cs" /> <Compile Include="TV\TvdbEpisodeProvider.cs" /> <Compile Include="TV\TvdbSeasonProvider.cs" /> <Compile Include="TV\TvdbSeriesProvider.cs" /> @@ -135,6 +148,7 @@ <Compile Include="TV\TvdbPrescanTask.cs" /> <Compile Include="TV\TvdbSeriesImageProvider.cs" /> <Compile Include="UserRootFolderNameProvider.cs" /> + <Compile Include="VirtualItemImageValidator.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> @@ -153,8 +167,14 @@ <ItemGroup> <None Include="packages.config" /> </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Studios\thumbs.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Studios\posters.txt" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> + <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index 264b24b87..5782e3e63 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -1,6 +1,5 @@ -using MediaBrowser.Common.IO; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -23,12 +22,6 @@ namespace MediaBrowser.Providers.MediaInfo public class AudioImageProvider : BaseMetadataProvider { /// <summary> - /// Gets or sets the image cache. - /// </summary> - /// <value>The image cache.</value> - public FileSystemRepository ImageCache { get; set; } - - /// <summary> /// The _locks /// </summary> private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>(); @@ -48,8 +41,6 @@ namespace MediaBrowser.Providers.MediaInfo : base(logManager, configurationManager) { _mediaEncoder = mediaEncoder; - - ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.AudioImagesDataPath); } /// <summary> @@ -113,7 +104,7 @@ namespace MediaBrowser.Providers.MediaInfo return ItemUpdateType.ImageUpdate; } } - + /// <summary> /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// </summary> @@ -154,13 +145,7 @@ namespace MediaBrowser.Providers.MediaInfo { cancellationToken.ThrowIfCancellationRequested(); - var album = item.Parent as MusicAlbum; - - var filename = item.Album ?? string.Empty; - filename += item.Artists.FirstOrDefault() ?? string.Empty; - filename += album == null ? item.Id.ToString("N") + item.DateModified.Ticks : album.Id.ToString("N") + album.DateModified.Ticks; - - var path = ImageCache.GetResourcePath(filename + "_primary", ".jpg"); + var path = GetAudioImagePath(item); if (!File.Exists(path)) { @@ -196,6 +181,38 @@ namespace MediaBrowser.Providers.MediaInfo } /// <summary> + /// Gets the audio image path. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + private string GetAudioImagePath(Audio item) + { + var album = item.Parent as MusicAlbum; + + var filename = item.Album ?? string.Empty; + filename += item.Artists.FirstOrDefault() ?? string.Empty; + filename += album == null ? item.Id.ToString("N") + item.DateModified.Ticks : album.Id.ToString("N") + album.DateModified.Ticks + "_primary"; + + filename = filename.GetMD5() + ".jpg"; + + var prefix = filename.Substring(0, 1); + + return Path.Combine(AudioImagesPath, prefix, filename); + } + + /// <summary> + /// Gets the audio images data path. + /// </summary> + /// <value>The audio images data path.</value> + public string AudioImagesPath + { + get + { + return Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "extracted-audio-images"); + } + } + + /// <summary> /// Gets the lock. /// </summary> /// <param name="filename">The filename.</param> diff --git a/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs index 5f285e6d8..0fdeddb49 100644 --- a/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs @@ -115,7 +115,7 @@ namespace MediaBrowser.Providers.MediaInfo if (video != null) { - inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type); + inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type); } return await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs index 673abea57..c38007288 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs @@ -151,8 +151,6 @@ namespace MediaBrowser.Providers.MediaInfo // Disc number audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc"); - audio.Language = GetDictionaryValue(tags, "language"); - audio.ProductionYear = GetDictionaryNumericValue(tags, "date"); // Several different forms of retaildate diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs index 7e3e3da3b..8e07bc266 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs @@ -1,8 +1,8 @@ using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -343,7 +343,7 @@ namespace MediaBrowser.Providers.MediaInfo video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle); - await Kernel.Instance.FFMpegManager.PopulateChapterImages(video, chapters, false, false, cancellationToken).ConfigureAwait(false); + await FFMpegManager.Instance.PopulateChapterImages(video, chapters, false, false, cancellationToken).ConfigureAwait(false); var videoFileChanged = CompareDate(video) > providerInfo.LastRefreshed; @@ -377,7 +377,7 @@ namespace MediaBrowser.Providers.MediaInfo if (!string.IsNullOrEmpty(genres)) { - video.Genres = genres.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries) + video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) .Select(i => i.Trim()) .ToList(); diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 2864983ce..d5815690f 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -1,6 +1,5 @@ -using MediaBrowser.Common.IO; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -12,7 +11,6 @@ using MediaBrowser.Model.Logging; using System; using System.Collections.Concurrent; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -21,12 +19,6 @@ namespace MediaBrowser.Providers.MediaInfo class VideoImageProvider : BaseMetadataProvider { /// <summary> - /// Gets or sets the image cache. - /// </summary> - /// <value>The image cache.</value> - public FileSystemRepository ImageCache { get; set; } - - /// <summary> /// The _locks /// </summary> private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>(); @@ -42,8 +34,6 @@ namespace MediaBrowser.Providers.MediaInfo { _mediaEncoder = mediaEncoder; _isoManager = isoManager; - - ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.VideoImagesDataPath); } /// <summary> @@ -206,9 +196,7 @@ namespace MediaBrowser.Providers.MediaInfo { cancellationToken.ThrowIfCancellationRequested(); - var filename = item.Path + "_" + item.DateModified.Ticks + "_primary"; - - var path = ImageCache.GetResourcePath(filename, ".jpg"); + var path = GetVideoImagePath(item); if (!File.Exists(path)) { @@ -265,7 +253,7 @@ namespace MediaBrowser.Providers.MediaInfo InputType type; - var inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type); + var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type); await _mediaEncoder.ExtractImage(inputPath, type, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false); @@ -310,5 +298,33 @@ namespace MediaBrowser.Providers.MediaInfo { return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); } + + /// <summary> + /// Gets the video images data path. + /// </summary> + /// <value>The video images data path.</value> + public string VideoImagesPath + { + get + { + return Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "extracted-video-images"); + } + } + + /// <summary> + /// Gets the audio image path. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>System.String.</returns> + private string GetVideoImagePath(Video item) + { + var filename = item.Path + "_" + item.DateModified.Ticks + "_primary"; + + filename = filename.GetMD5() + ".jpg"; + + var prefix = filename.Substring(0, 1); + + return Path.Combine(VideoImagesPath, prefix, filename); + } } } diff --git a/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs b/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs index e483b1d61..2682cf3c0 100644 --- a/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs +++ b/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs @@ -112,6 +112,11 @@ namespace MediaBrowser.Providers.Movies /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> public override bool Supports(BaseItem item) { + return SupportsItem(item); + } + + internal static bool SupportsItem(IHasImages item) + { var trailer = item as Trailer; if (trailer != null) @@ -295,7 +300,7 @@ namespace MediaBrowser.Providers.Movies cancellationToken.ThrowIfCancellationRequested(); - var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops; + var backdropLimit = ConfigurationManager.Configuration.MovieOptions.MaxBackdrops; if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit) { @@ -325,6 +330,7 @@ namespace MediaBrowser.Providers.Movies { continue; } + break; } } } diff --git a/MediaBrowser.Providers/Movies/FanArtMovieUpdatesPrescanTask.cs b/MediaBrowser.Providers/Movies/FanArtMovieUpdatesPrescanTask.cs index aff71c6db..9bd73bf65 100644 --- a/MediaBrowser.Providers/Movies/FanArtMovieUpdatesPrescanTask.cs +++ b/MediaBrowser.Providers/Movies/FanArtMovieUpdatesPrescanTask.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Providers.Movies { - class FanArtMovieUpdatesPrescanTask : ILibraryPrescanTask + class FanArtMovieUpdatesPrescanTask : ILibraryPostScanTask { private const string UpdatesUrl = "http://api.fanart.tv/webservice/newmovies/{0}/{1}/"; diff --git a/MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs b/MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs index d714128ea..fae8cd591 100644 --- a/MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs +++ b/MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs @@ -36,23 +36,24 @@ namespace MediaBrowser.Providers.Movies get { return "FanArt"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { - return FanArtMovieProvider.Current.Supports(item); + return FanArtMovieProvider.SupportsItem(item); } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { + var baseItem = (BaseItem)item; var list = new List<RemoteImageInfo>(); - var movieId = item.GetProviderId(MetadataProviders.Tmdb); + var movieId = baseItem.GetProviderId(MetadataProviders.Tmdb); if (!string.IsNullOrEmpty(movieId)) { @@ -68,10 +69,10 @@ namespace MediaBrowser.Providers.Movies } } - var language = _config.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - + // Sort first by width to prioritize HD versions list = list.OrderByDescending(i => i.Width ?? 0) .ThenByDescending(i => diff --git a/MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs b/MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs index e5bd3bf47..b9cabded7 100644 --- a/MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs +++ b/MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs @@ -35,19 +35,19 @@ namespace MediaBrowser.Providers.Movies get { return "TheMovieDb"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return MovieDbImagesProvider.SupportsItem(item); } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var list = new List<RemoteImageInfo>(); @@ -87,7 +87,7 @@ namespace MediaBrowser.Providers.Movies RatingType = RatingType.Score })); - var language = _config.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); @@ -114,17 +114,15 @@ namespace MediaBrowser.Providers.Movies .ThenByDescending(i => i.VoteCount ?? 0) .ToList(); } - + /// <summary> /// Gets the posters. /// </summary> /// <param name="images">The images.</param> /// <param name="item">The item.</param> /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns> - private IEnumerable<MovieDbProvider.Poster> GetPosters(MovieDbProvider.Images images, BaseItem item) + private IEnumerable<MovieDbProvider.Poster> GetPosters(MovieDbProvider.Images images, IHasImages item) { - var language = _config.Configuration.PreferredMetadataLanguage; - return images.posters ?? new List<MovieDbProvider.Poster>(); } @@ -134,7 +132,7 @@ namespace MediaBrowser.Providers.Movies /// <param name="images">The images.</param> /// <param name="item">The item.</param> /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns> - private IEnumerable<MovieDbProvider.Backdrop> GetBackdrops(MovieDbProvider.Images images, BaseItem item) + private IEnumerable<MovieDbProvider.Backdrop> GetBackdrops(MovieDbProvider.Images images, IHasImages item) { var eligibleBackdrops = images.backdrops == null ? new List<MovieDbProvider.Backdrop>() : images.backdrops @@ -150,9 +148,9 @@ namespace MediaBrowser.Providers.Movies /// <param name="item">The item.</param> /// <param name="jsonSerializer">The json serializer.</param> /// <returns>Task{MovieImages}.</returns> - private MovieDbProvider.Images FetchImages(BaseItem item, IJsonSerializer jsonSerializer) + private MovieDbProvider.Images FetchImages(IHasImages item, IJsonSerializer jsonSerializer) { - var path = MovieDbProvider.Current.GetImagesDataFilePath(item); + var path = MovieDbProvider.Current.GetDataFilePath((BaseItem)item); if (!string.IsNullOrEmpty(path)) { diff --git a/MediaBrowser.Providers/Movies/ManualMovieDbPersonImageProvider.cs b/MediaBrowser.Providers/Movies/ManualMovieDbPersonImageProvider.cs index b381de332..453284751 100644 --- a/MediaBrowser.Providers/Movies/ManualMovieDbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Movies/ManualMovieDbPersonImageProvider.cs @@ -6,7 +6,6 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -34,60 +33,44 @@ namespace MediaBrowser.Providers.Movies get { return "TheMovieDb"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Person; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { - return GetAllImagesInternal(item, true, cancellationToken); - } - - public async Task<IEnumerable<RemoteImageInfo>> GetAllImagesInternal(BaseItem item, bool retryOnMissingData, CancellationToken cancellationToken) - { - var id = item.GetProviderId(MetadataProviders.Tmdb); + var person = (Person)item; + var id = person.GetProviderId(MetadataProviders.Tmdb); if (!string.IsNullOrEmpty(id)) { - var dataFilePath = MovieDbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id); - - try - { - var result = _jsonSerializer.DeserializeFromFile<MovieDbPersonProvider.PersonResult>(dataFilePath); - - var images = result.images ?? new MovieDbPersonProvider.Images(); + await MovieDbPersonProvider.Current.DownloadPersonInfoIfNeeded(id, cancellationToken).ConfigureAwait(false); - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var dataFilePath = MovieDbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id); - var tmdbImageUrl = tmdbSettings.images.base_url + "original"; + var result = _jsonSerializer.DeserializeFromFile<MovieDbPersonProvider.PersonResult>(dataFilePath); - return GetImages(images, tmdbImageUrl); - } - catch (FileNotFoundException) - { + var images = result.images ?? new MovieDbPersonProvider.Images(); - } + var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - if (retryOnMissingData) - { - await MovieDbPersonProvider.Current.DownloadPersonInfo(id, cancellationToken).ConfigureAwait(false); + var tmdbImageUrl = tmdbSettings.images.base_url + "original"; - return await GetAllImagesInternal(item, false, cancellationToken).ConfigureAwait(false); - } + return GetImages(images, item.GetPreferredMetadataLanguage(), tmdbImageUrl); } return new List<RemoteImageInfo>(); } - - private IEnumerable<RemoteImageInfo> GetImages(MovieDbPersonProvider.Images images, string baseImageUrl) + + private IEnumerable<RemoteImageInfo> GetImages(MovieDbPersonProvider.Images images, string preferredLanguage, string baseImageUrl) { var list = new List<RemoteImageInfo>(); @@ -104,7 +87,7 @@ namespace MediaBrowser.Providers.Movies })); } - var language = _config.Configuration.PreferredMetadataLanguage; + var language = preferredLanguage; var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs b/MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs index d63fcec5c..7386f47f4 100644 --- a/MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Providers.Movies return SupportsItem(item); } - public static bool SupportsItem(BaseItem item) + internal static bool SupportsItem(IHasImages item) { var trailer = item as Trailer; @@ -132,7 +132,9 @@ namespace MediaBrowser.Providers.Movies } // Don't refresh if we already have both poster and backdrop and we're not refreshing images - if (item.HasImage(ImageType.Primary) && item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) + if (item.HasImage(ImageType.Primary) && + item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MovieOptions.MaxBackdrops && + !item.LockedFields.Contains(MetadataFields.Images)) { return false; } @@ -142,7 +144,7 @@ namespace MediaBrowser.Providers.Movies protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) { - var path = MovieDbProvider.Current.GetImagesDataFilePath(item); + var path = MovieDbProvider.Current.GetDataFilePath(item); if (!string.IsNullOrEmpty(path)) { @@ -167,7 +169,6 @@ namespace MediaBrowser.Providers.Movies public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) { var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualMovieDbImageProvider.ProviderName).ConfigureAwait(false); - await ProcessImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); SetLastRefreshed(item, DateTime.UtcNow, providerInfo); @@ -190,7 +191,7 @@ namespace MediaBrowser.Providers.Movies .ToList(); // poster - if (eligiblePosters.Count > 0 && !item.HasImage(ImageType.Primary)) + if (eligiblePosters.Count > 0 && !item.HasImage(ImageType.Primary) && !item.LockedFields.Contains(MetadataFields.Images)) { var poster = eligiblePosters[0]; @@ -210,13 +211,16 @@ namespace MediaBrowser.Providers.Movies cancellationToken.ThrowIfCancellationRequested(); var eligibleBackdrops = images - .Where(i => i.Type == ImageType.Backdrop && i.Width.HasValue && i.Width.Value >= ConfigurationManager.Configuration.MinMovieBackdropDownloadWidth) + .Where(i => i.Type == ImageType.Backdrop && i.Width.HasValue && i.Width.Value >= ConfigurationManager.Configuration.MovieOptions.MinBackdropWidth) .ToList(); - var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops; + var backdropLimit = ConfigurationManager.Configuration.MovieOptions.MaxBackdrops; // backdrops - only download if earlier providers didn't find any (fanart) - if (eligibleBackdrops.Count > 0 && ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit) + if (eligibleBackdrops.Count > 0 && + ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && + item.BackdropImagePaths.Count < backdropLimit && + !item.LockedFields.Contains(MetadataFields.Backdrops)) { for (var i = 0; i < eligibleBackdrops.Count; i++) { diff --git a/MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs b/MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs index 8fa2ea249..f6c908a7c 100644 --- a/MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs @@ -164,7 +164,6 @@ namespace MediaBrowser.Providers.Movies public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) { var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualMovieDbPersonImageProvider.ProviderName).ConfigureAwait(false); - await ProcessImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); SetLastRefreshed(item, DateTime.UtcNow, providerInfo); @@ -187,7 +186,7 @@ namespace MediaBrowser.Providers.Movies .ToList(); // poster - if (eligiblePosters.Count > 0 && !item.HasImage(ImageType.Primary)) + if (eligiblePosters.Count > 0 && !item.HasImage(ImageType.Primary) && !item.LockedFields.Contains(MetadataFields.Images)) { var poster = eligiblePosters[0]; diff --git a/MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs b/MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs index 3efd8d7fe..c16c50412 100644 --- a/MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs @@ -86,7 +86,7 @@ namespace MediaBrowser.Providers.Movies protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { - if (HasAltMeta(item) && !ConfigurationManager.Configuration.EnableTmdbUpdates) + if (HasAltMeta(item)) return false; return base.NeedsRefreshInternal(item, providerInfo); @@ -235,17 +235,12 @@ namespace MediaBrowser.Providers.Movies /// <returns>Task.</returns> private async Task FetchInfo(Person person, string id, bool isForcedRefresh, CancellationToken cancellationToken) { - var dataFilePath = GetPersonDataFilePath(ConfigurationManager.ApplicationPaths, id); + await DownloadPersonInfoIfNeeded(id, cancellationToken).ConfigureAwait(false); - // Only download if not already there - // The prescan task will take care of updates so we don't need to re-download here - if (!File.Exists(dataFilePath)) + if (isForcedRefresh || !HasAltMeta(person)) { - await DownloadPersonInfo(id, cancellationToken).ConfigureAwait(false); - } + var dataFilePath = GetPersonDataFilePath(ConfigurationManager.ApplicationPaths, id); - if (isForcedRefresh || ConfigurationManager.Configuration.EnableTmdbUpdates || !HasAltMeta(person)) - { var info = JsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath); cancellationToken.ThrowIfCancellationRequested(); @@ -254,10 +249,17 @@ namespace MediaBrowser.Providers.Movies } } - internal async Task DownloadPersonInfo(string id, CancellationToken cancellationToken) + internal async Task DownloadPersonInfoIfNeeded(string id, CancellationToken cancellationToken) { var personDataPath = GetPersonDataPath(ConfigurationManager.ApplicationPaths, id); + var fileInfo = _fileSystem.GetFileSystemInfo(personDataPath); + + if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7) + { + return; + } + var url = string.Format(@"http://api.themoviedb.org/3/person/{1}?api_key={0}&append_to_response=credits,images", MovieDbProvider.ApiKey, id); using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index ecf5a5951..dc267b37c 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbProvider.cs @@ -205,13 +205,9 @@ namespace MediaBrowser.Providers.Movies if (!string.IsNullOrEmpty(path)) { - var imagesFilePath = GetImagesDataFilePath(item); - var fileInfo = new FileInfo(path); - var imagesFileInfo = new FileInfo(imagesFilePath); - return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed || - !imagesFileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(imagesFileInfo) > providerInfo.LastRefreshed; + return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed; } return base.NeedsRefreshBasedOnCompareDate(item, providerInfo); @@ -263,7 +259,8 @@ namespace MediaBrowser.Providers.Movies id = item.GetProviderId(MetadataProviders.Imdb); } - if (string.IsNullOrEmpty(id)) + // Don't search for music video id's because it is very easy to misidentify. + if (string.IsNullOrEmpty(id) && !(item is MusicVideo)) { id = await FindId(item, cancellationToken).ConfigureAwait(false); } @@ -317,7 +314,7 @@ namespace MediaBrowser.Providers.Movies var year = item.ProductionYear ?? yearInName; Logger.Info("MovieDbProvider: Finding id for item: " + name); - string language = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower(); + var language = item.GetPreferredMetadataLanguage().ToLower(); //if we are a boxset - look at our first child var boxset = item as BoxSet; @@ -501,43 +498,35 @@ namespace MediaBrowser.Providers.Movies { // Id could be ImdbId or TmdbId - var language = ConfigurationManager.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); + var country = item.GetPreferredMetadataCountryCode(); var dataFilePath = GetDataFilePath(item); var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - if (string.IsNullOrEmpty(dataFilePath) || !File.Exists(dataFilePath) || !File.Exists(GetImagesDataFilePath(item))) - { - var isBoxSet = item is BoxSet; + var isBoxSet = item is BoxSet; + if (string.IsNullOrEmpty(dataFilePath) || !File.Exists(dataFilePath)) + { var mainResult = await FetchMainResult(id, isBoxSet, language, cancellationToken).ConfigureAwait(false); if (mainResult == null) return; tmdbId = mainResult.id.ToString(_usCulture); - var movieDataPath = GetMovieDataPath(ConfigurationManager.ApplicationPaths, isBoxSet, tmdbId); - - dataFilePath = Path.Combine(movieDataPath, language + ".json"); + dataFilePath = GetDataFilePath(isBoxSet, tmdbId, language); var directory = Path.GetDirectoryName(dataFilePath); Directory.CreateDirectory(directory); JsonSerializer.SerializeToFile(mainResult, dataFilePath); - - // Now get the language-less version - mainResult = await FetchMainResult(id, isBoxSet, null, cancellationToken).ConfigureAwait(false); - - dataFilePath = Path.Combine(movieDataPath, "default.json"); - - JsonSerializer.SerializeToFile(mainResult, dataFilePath); } if (isForcedRefresh || ConfigurationManager.Configuration.EnableTmdbUpdates || !HasAltMeta(item)) { - dataFilePath = GetDataFilePath(item, tmdbId); + dataFilePath = GetDataFilePath(isBoxSet, tmdbId, language); if (!string.IsNullOrEmpty(dataFilePath)) { @@ -553,27 +542,18 @@ namespace MediaBrowser.Providers.Movies /// </summary> /// <param name="id">The id.</param> /// <param name="isBoxSet">if set to <c>true</c> [is box set].</param> - /// <param name="dataPath">The data path.</param> + /// <param name="preferredMetadataLanguage">The preferred metadata language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - internal async Task DownloadMovieInfo(string id, bool isBoxSet, string dataPath, CancellationToken cancellationToken) + internal async Task DownloadMovieInfo(string id, bool isBoxSet, string preferredMetadataLanguage, CancellationToken cancellationToken) { - var language = ConfigurationManager.Configuration.PreferredMetadataLanguage; - - var mainResult = await FetchMainResult(id, isBoxSet, language, cancellationToken).ConfigureAwait(false); + var mainResult = await FetchMainResult(id, isBoxSet, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); if (mainResult == null) return; - var dataFilePath = Path.Combine(dataPath, language + ".json"); - - Directory.CreateDirectory(dataPath); - - JsonSerializer.SerializeToFile(mainResult, dataFilePath); - - // Now get the language-less version - mainResult = await FetchMainResult(id, isBoxSet, null, cancellationToken).ConfigureAwait(false); + var dataFilePath = GetDataFilePath(isBoxSet, id, preferredMetadataLanguage); - dataFilePath = Path.Combine(dataPath, "default.json"); + Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); JsonSerializer.SerializeToFile(mainResult, dataFilePath); } @@ -592,30 +572,17 @@ namespace MediaBrowser.Providers.Movies return null; } - return GetDataFilePath(item, id); - } - - internal string GetDataFilePath(BaseItem item, string tmdbId) - { - var language = ConfigurationManager.Configuration.PreferredMetadataLanguage; - - var path = GetMovieDataPath(ConfigurationManager.ApplicationPaths, item is BoxSet, tmdbId); - - path = Path.Combine(path, language + ".json"); - - return path; + return GetDataFilePath(item is BoxSet, id, item.GetPreferredMetadataLanguage()); } - internal string GetImagesDataFilePath(BaseItem item) + internal string GetDataFilePath(bool isBoxset, string tmdbId, string preferredLanguage) { - var path = GetDataFilePath(item); + var path = GetMovieDataPath(ConfigurationManager.ApplicationPaths, isBoxset, tmdbId); - if (!string.IsNullOrEmpty(path)) - { - path = Path.Combine(Path.GetDirectoryName(path), "default.json"); - } + var filename = string.Format("all-{0}.json", + preferredLanguage ?? string.Empty); - return path; + return Path.Combine(path, filename); } /// <summary> @@ -632,9 +599,18 @@ namespace MediaBrowser.Providers.Movies var url = string.Format(baseUrl, id, ApiKey); + // Get images in english and with no language + url += "&include_image_language=en,null"; + if (!string.IsNullOrEmpty(language)) { - url += "&language=" + language; + // If preferred language isn't english, get those images too + if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + { + url += string.Format(",{0}", language); + } + + url += string.Format("&language={0}", language); } CompleteMovieData mainResult; @@ -744,27 +720,29 @@ namespace MediaBrowser.Providers.Movies // tmdb appears to have unified their numbers to always report "7.3" regardless of country // so I removed the culture-specific processing here because it was not working for other countries -ebr // Movies get this from imdb - if (movie is BoxSet && float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out rating)) + if (!(movie is Movie) && float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out rating)) { movie.CommunityRating = rating; } // Movies get this from imdb - if (movie is BoxSet) + if (!(movie is Movie)) { movie.VoteCount = movieData.vote_count; } + var preferredCountryCode = movie.GetPreferredMetadataCountryCode(); + //release date and certification are retrieved based on configured country and we fall back on US if not there and to minimun release date if still no match if (movieData.releases != null && movieData.releases.countries != null) { - var ourRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals(ConfigurationManager.Configuration.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)) ?? new Country(); + var ourRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals(preferredCountryCode, StringComparison.OrdinalIgnoreCase)) ?? new Country(); var usRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase)) ?? new Country(); var minimunRelease = movieData.releases.countries.OrderBy(c => c.release_date).FirstOrDefault() ?? new Country(); if (!movie.LockedFields.Contains(MetadataFields.OfficialRating)) { - var ratingPrefix = ConfigurationManager.Configuration.MetadataCountryCode.Equals("us", StringComparison.OrdinalIgnoreCase) ? "" : ConfigurationManager.Configuration.MetadataCountryCode + "-"; + var ratingPrefix = string.Equals(preferredCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? "" : preferredCountryCode + "-"; movie.OfficialRating = !string.IsNullOrEmpty(ourRelease.certification) ? ratingPrefix + ourRelease.certification : !string.IsNullOrEmpty(usRelease.certification) @@ -773,44 +751,16 @@ namespace MediaBrowser.Providers.Movies ? minimunRelease.iso_3166_1 + "-" + minimunRelease.certification : null; } - - if (ourRelease.release_date != default(DateTime)) - { - if (ourRelease.release_date.Year != 1) - { - movie.PremiereDate = ourRelease.release_date.ToUniversalTime(); - movie.ProductionYear = ourRelease.release_date.Year; - } - } - else if (usRelease.release_date != default(DateTime)) - { - if (usRelease.release_date.Year != 1) - { - movie.PremiereDate = usRelease.release_date.ToUniversalTime(); - movie.ProductionYear = usRelease.release_date.Year; - } - } - else if (minimunRelease.release_date != default(DateTime)) - { - if (minimunRelease.release_date.Year != 1) - { - - movie.PremiereDate = minimunRelease.release_date.ToUniversalTime(); - movie.ProductionYear = minimunRelease.release_date.Year; - } - } } - else + + if (movieData.release_date.Year != 1) { - if (movieData.release_date.Year != 1) - { - //no specific country release info at all - movie.PremiereDate = movieData.release_date.ToUniversalTime(); - movie.ProductionYear = movieData.release_date.Year; - } + //no specific country release info at all + movie.PremiereDate = movieData.release_date.ToUniversalTime(); + movie.ProductionYear = movieData.release_date.Year; } - //if that didn't find a rating and we are a boxset, use the one from our first child + // If that didn't find a rating and we are a boxset, use the one from our first child if (movie.OfficialRating == null && movie is BoxSet && !movie.LockedFields.Contains(MetadataFields.OfficialRating)) { var boxset = movie as BoxSet; @@ -837,15 +787,17 @@ namespace MediaBrowser.Providers.Movies // genres // Movies get this from imdb - if (movieData.genres != null && !movie.LockedFields.Contains(MetadataFields.Genres)) + var genres = movieData.genres ?? new List<GenreItem>(); + if (!movie.LockedFields.Contains(MetadataFields.Genres)) { // Only grab them if a boxset or there are no genres. // For movies and trailers we'll use imdb via omdb - if (movie is BoxSet || movie.Genres.Count == 0) + // But omdb data is for english users only so fetch if language is not english + if (!(movie is Movie) || movie.Genres.Count == 0 || !string.Equals(movie.GetPreferredMetadataLanguage(), "en", StringComparison.OrdinalIgnoreCase)) { movie.Genres.Clear(); - foreach (var genre in movieData.genres.Select(g => g.name)) + foreach (var genre in genres.Select(g => g.name)) { movie.AddGenre(genre); } diff --git a/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs b/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs index bb1299f67..3ba777b37 100644 --- a/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs +++ b/MediaBrowser.Providers/Movies/MovieProviderFromXml.cs @@ -1,7 +1,7 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -48,7 +48,8 @@ namespace MediaBrowser.Providers.Movies return !trailer.IsLocalTrailer; } - return item is Movie || item is MusicVideo || item is AdultVideo; + // Check parent for null to avoid running this against things like video backdrops + return item is Video && !(item is Episode) && item.Parent != null; } /// <summary> diff --git a/MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs b/MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs index f8fb133c6..291d2ff4d 100644 --- a/MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs +++ b/MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs @@ -2,7 +2,10 @@ using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; @@ -16,7 +19,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Providers.Movies { - public class MovieUpdatesPreScanTask : ILibraryPrescanTask + public class MovieUpdatesPreScanTask : ILibraryPostScanTask { /// <summary> /// The updates URL @@ -37,6 +40,7 @@ namespace MediaBrowser.Providers.Movies private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _json; private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; /// <summary> /// Initializes a new instance of the <see cref="MovieUpdatesPreScanTask"/> class. @@ -45,13 +49,14 @@ namespace MediaBrowser.Providers.Movies /// <param name="httpClient">The HTTP client.</param> /// <param name="config">The config.</param> /// <param name="json">The json.</param> - public MovieUpdatesPreScanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem) + public MovieUpdatesPreScanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem, ILibraryManager libraryManager) { _logger = logger; _httpClient = httpClient; _config = config; _json = json; _fileSystem = fileSystem; + _libraryManager = libraryManager; } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); @@ -101,7 +106,7 @@ namespace MediaBrowser.Providers.Movies var timestampFileInfo = new FileInfo(timestampFile); // Don't check for updates every single time - if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 3) + if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 7) { return; } @@ -196,15 +201,30 @@ namespace MediaBrowser.Providers.Movies var list = ids.ToList(); var numComplete = 0; + // Gather all movies into a lookup by tmdb id + var allMovies = _libraryManager.RootFolder.RecursiveChildren + .Where(i => i is Movie || i is Trailer) + .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tmdb))) + .ToLookup(i => i.GetProviderId(MetadataProviders.Tmdb)); + foreach (var id in list) { - try - { - await UpdateMovie(id, isBoxSet, moviesDataPath, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) + // Find the preferred language(s) for the movie in the library + var languages = allMovies[id] + .Select(i => i.GetPreferredMetadataLanguage()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var language in languages) { - _logger.ErrorException("Error updating tmdb movie id {0}", ex, id); + try + { + await UpdateMovie(id, isBoxSet, language, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error updating tmdb movie id {0}, language {1}", ex, id, language); + } } numComplete++; @@ -221,18 +241,14 @@ namespace MediaBrowser.Providers.Movies /// </summary> /// <param name="id">The id.</param> /// <param name="isBoxSet">if set to <c>true</c> [is box set].</param> - /// <param name="dataPath">The data path.</param> + /// <param name="preferredMetadataLanguage">The preferred metadata language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private Task UpdateMovie(string id, bool isBoxSet, string dataPath, CancellationToken cancellationToken) + private Task UpdateMovie(string id, bool isBoxSet, string preferredMetadataLanguage, CancellationToken cancellationToken) { - _logger.Info("Updating movie from tmdb " + id); - - var itemDataPath = Path.Combine(dataPath, id); - - Directory.CreateDirectory(dataPath); + _logger.Info("Updating movie from tmdb " + id + ", language " + preferredMetadataLanguage); - return MovieDbProvider.Current.DownloadMovieInfo(id, isBoxSet, itemDataPath, cancellationToken); + return MovieDbProvider.Current.DownloadMovieInfo(id, isBoxSet, preferredMetadataLanguage, cancellationToken); } class Result diff --git a/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs b/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs index 8370eecbb..8940f1d49 100644 --- a/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs +++ b/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs @@ -202,13 +202,21 @@ namespace MediaBrowser.Providers.Movies private bool ShouldFetchGenres(BaseItem item) { + var lang = item.GetPreferredMetadataLanguage(); + + // The data isn't localized and so can only be used for english users + if (!string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + // Only fetch if other providers didn't get anything if (item is Trailer) { return item.Genres.Count == 0; } - return item is Series; + return item is Series || item is Movie; } protected class RootObject diff --git a/MediaBrowser.Providers/Movies/PersonUpdatesPreScanTask.cs b/MediaBrowser.Providers/Movies/PersonUpdatesPreScanTask.cs deleted file mode 100644 index 489b0ad09..000000000 --- a/MediaBrowser.Providers/Movies/PersonUpdatesPreScanTask.cs +++ /dev/null @@ -1,236 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -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; - -namespace MediaBrowser.Providers.Movies -{ - public class PersonUpdatesPreScanTask : IPeoplePrescanTask - { - /// <summary> - /// The updates URL - /// </summary> - private const string UpdatesUrl = "http://api.themoviedb.org/3/person/changes?start_date={0}&api_key={1}&page={2}"; - - /// <summary> - /// The _HTTP client - /// </summary> - private readonly IHttpClient _httpClient; - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - /// <summary> - /// The _config - /// </summary> - private readonly IServerConfigurationManager _config; - private readonly IJsonSerializer _json; - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="PersonUpdatesPreScanTask"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="httpClient">The HTTP client.</param> - /// <param name="config">The config.</param> - public PersonUpdatesPreScanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem) - { - _logger = logger; - _httpClient = httpClient; - _config = config; - _json = json; - _fileSystem = fileSystem; - } - - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - if (!_config.Configuration.EnableInternetProviders || !_config.Configuration.EnableTmdbUpdates) - { - progress.Report(100); - return; - } - - var path = MovieDbPersonProvider.GetPersonsDataPath(_config.CommonApplicationPaths); - - Directory.CreateDirectory(path); - - var timestampFile = Path.Combine(path, "time.txt"); - - var timestampFileInfo = new FileInfo(timestampFile); - - // Don't check for updates every single time - if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 3) - { - return; - } - - // Find out the last time we queried tvdb for updates - var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; - - var existingDirectories = GetExistingIds(path).ToList(); - - if (!string.IsNullOrEmpty(lastUpdateTime)) - { - long lastUpdateTicks; - - if (long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out lastUpdateTicks)) - { - var lastUpdateDate = new DateTime(lastUpdateTicks, DateTimeKind.Utc); - - // They only allow up to 14 days of updates - if ((DateTime.UtcNow - lastUpdateDate).TotalDays > 13) - { - lastUpdateDate = DateTime.UtcNow.AddDays(-13); - } - - var updatedIds = await GetIdsToUpdate(lastUpdateDate, 1, cancellationToken).ConfigureAwait(false); - - var existingDictionary = existingDirectories.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - var idsToUpdate = updatedIds.Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); - - await UpdatePeople(idsToUpdate, progress, cancellationToken).ConfigureAwait(false); - } - } - - File.WriteAllText(timestampFile, DateTime.UtcNow.Ticks.ToString(UsCulture), Encoding.UTF8); - progress.Report(100); - } - - /// <summary> - /// Gets the existing ids. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>IEnumerable{System.String}.</returns> - private IEnumerable<string> GetExistingIds(string path) - { - return Directory.EnumerateDirectories(path) - .SelectMany(Directory.EnumerateDirectories) - .Select(Path.GetFileNameWithoutExtension); - } - - /// <summary> - /// Gets the ids to update. - /// </summary> - /// <param name="lastUpdateTime">The last update time.</param> - /// <param name="page">The page.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{System.String}}.</returns> - private async Task<IEnumerable<string>> GetIdsToUpdate(DateTime lastUpdateTime, int page, CancellationToken cancellationToken) - { - var hasMorePages = false; - var list = new List<string>(); - - // First get last time - using (var stream = await _httpClient.Get(new HttpRequestOptions - { - Url = string.Format(UpdatesUrl, lastUpdateTime.ToString("yyyy-MM-dd"), MovieDbProvider.ApiKey, page), - CancellationToken = cancellationToken, - EnableHttpCompression = true, - ResourcePool = MovieDbProvider.Current.MovieDbResourcePool, - AcceptHeader = MovieDbProvider.AcceptHeader - - }).ConfigureAwait(false)) - { - var obj = _json.DeserializeFromStream<RootObject>(stream); - - var data = obj.results.Select(i => i.id.ToString(UsCulture)); - - list.AddRange(data); - - hasMorePages = page < obj.total_pages; - } - - if (hasMorePages) - { - var more = await GetIdsToUpdate(lastUpdateTime, page + 1, cancellationToken).ConfigureAwait(false); - - list.AddRange(more); - } - - return list; - } - - /// <summary> - /// Updates the people. - /// </summary> - /// <param name="ids">The ids.</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task UpdatePeople(IEnumerable<string> ids, IProgress<double> progress, CancellationToken cancellationToken) - { - var list = ids.ToList(); - var numComplete = 0; - - foreach (var id in list) - { - try - { - await UpdatePerson(id, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error updating tmdb person id {0}", ex, id); - } - - numComplete++; - double percent = numComplete; - percent /= list.Count; - percent *= 100; - - progress.Report(percent); - } - } - - /// <summary> - /// Updates the person. - /// </summary> - /// <param name="id">The id.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private Task UpdatePerson(string id, CancellationToken cancellationToken) - { - _logger.Info("Updating person from tmdb " + id); - - return MovieDbPersonProvider.Current.DownloadPersonInfo(id, cancellationToken); - } - - class Result - { - public int id { get; set; } - public bool? adult { get; set; } - } - - class RootObject - { - public List<Result> results { get; set; } - public int page { get; set; } - public int total_pages { get; set; } - public int total_results { get; set; } - - public RootObject() - { - results = new List<Result>(); - } - } - } -} diff --git a/MediaBrowser.Providers/Music/AlbumInfoFromSongProvider.cs b/MediaBrowser.Providers/Music/AlbumInfoFromSongProvider.cs index c4b4af97f..47799b8f3 100644 --- a/MediaBrowser.Providers/Music/AlbumInfoFromSongProvider.cs +++ b/MediaBrowser.Providers/Music/AlbumInfoFromSongProvider.cs @@ -140,7 +140,6 @@ namespace MediaBrowser.Providers.Music } } - providerInfo.FileStamp = GetComparisonData(songs); SetLastRefreshed(item, DateTime.UtcNow, providerInfo); diff --git a/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs b/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs index 161c96a5d..b6e0d61f7 100644 --- a/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs @@ -154,9 +154,11 @@ namespace MediaBrowser.Providers.Music /// <returns>Task{System.Boolean}.</returns> public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) { - var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartAlbumProvider.ProviderName).ConfigureAwait(false); - - await FetchFromXml(item, images.ToList(), cancellationToken).ConfigureAwait(false); + if (!item.LockedFields.Contains(MetadataFields.Images)) + { + var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartAlbumProvider.ProviderName).ConfigureAwait(false); + await FetchFromXml(item, images.ToList(), cancellationToken).ConfigureAwait(false); + } SetLastRefreshed(item, DateTime.UtcNow, providerInfo); @@ -203,6 +205,7 @@ namespace MediaBrowser.Providers.Music { continue; } + break; } } } diff --git a/MediaBrowser.Providers/Music/FanArtArtistProvider.cs b/MediaBrowser.Providers/Music/FanArtArtistProvider.cs index 4830d15b1..b248fcb40 100644 --- a/MediaBrowser.Providers/Music/FanArtArtistProvider.cs +++ b/MediaBrowser.Providers/Music/FanArtArtistProvider.cs @@ -213,13 +213,12 @@ namespace MediaBrowser.Providers.Music } if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art || - ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops || - ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner || - ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo || - ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary) + ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops || + ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner || + ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo || + ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary) { var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartArtistProvider.ProviderName).ConfigureAwait(false); - await FetchFromXml(item, images.ToList(), cancellationToken).ConfigureAwait(false); } @@ -268,46 +267,52 @@ namespace MediaBrowser.Providers.Music /// <returns>Task.</returns> private async Task FetchFromXml(BaseItem item, List<RemoteImageInfo> images , CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary && !item.HasImage(ImageType.Primary)) + if (!item.LockedFields.Contains(MetadataFields.Images)) { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary && !item.HasImage(ImageType.Primary)) + { + await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo && !item.HasImage(ImageType.Logo)) - { - await SaveImage(item, images, ImageType.Logo, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo && !item.HasImage(ImageType.Logo)) + { + await SaveImage(item, images, ImageType.Logo, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art && !item.HasImage(ImageType.Art)) - { - await SaveImage(item, images, ImageType.Art, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art && !item.HasImage(ImageType.Art)) + { + await SaveImage(item, images, ImageType.Art, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner && !item.HasImage(ImageType.Banner)) - { - await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner && !item.HasImage(ImageType.Banner)) + { + await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false); + } + } - var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops; - if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops && - item.BackdropImagePaths.Count < backdropLimit) + if (!item.LockedFields.Contains(MetadataFields.Backdrops)) { - foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) + cancellationToken.ThrowIfCancellationRequested(); + + var backdropLimit = ConfigurationManager.Configuration.MusicOptions.MaxBackdrops; + if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops && + item.BackdropImagePaths.Count < backdropLimit) { - await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Backdrop, null, cancellationToken) - .ConfigureAwait(false); + foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) + { + await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Backdrop, null, cancellationToken) + .ConfigureAwait(false); - if (item.BackdropImagePaths.Count >= backdropLimit) break; + if (item.BackdropImagePaths.Count >= backdropLimit) break; + } } } } @@ -328,6 +333,7 @@ namespace MediaBrowser.Providers.Music { continue; } + break; } } } diff --git a/MediaBrowser.Providers/Music/FanArtUpdatesPrescanTask.cs b/MediaBrowser.Providers/Music/FanArtUpdatesPrescanTask.cs index ddf212179..a3d0deb0e 100644 --- a/MediaBrowser.Providers/Music/FanArtUpdatesPrescanTask.cs +++ b/MediaBrowser.Providers/Music/FanArtUpdatesPrescanTask.cs @@ -15,7 +15,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Providers.Music { - class FanArtUpdatesPrescanTask : ILibraryPrescanTask + class FanArtUpdatesPrescanTask : ILibraryPostScanTask { private const string UpdatesUrl = "http://api.fanart.tv/webservice/newmusic/{0}/{1}/"; diff --git a/MediaBrowser.Providers/Music/LastFmImageProvider.cs b/MediaBrowser.Providers/Music/LastFmImageProvider.cs index 2a30a3a2e..98ba58fa8 100644 --- a/MediaBrowser.Providers/Music/LastFmImageProvider.cs +++ b/MediaBrowser.Providers/Music/LastFmImageProvider.cs @@ -90,7 +90,7 @@ namespace MediaBrowser.Providers.Music ? ConfigurationManager.Configuration.DownloadMusicAlbumImages : ConfigurationManager.Configuration.DownloadMusicArtistImages; - if (configSetting.Primary && !item.HasImage(ImageType.Primary)) + if (configSetting.Primary && !item.HasImage(ImageType.Primary) && !item.LockedFields.Contains(MetadataFields.Images)) { var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); diff --git a/MediaBrowser.Providers/Music/LastfmHelper.cs b/MediaBrowser.Providers/Music/LastfmHelper.cs index 3301d5584..df02cee5b 100644 --- a/MediaBrowser.Providers/Music/LastfmHelper.cs +++ b/MediaBrowser.Providers/Music/LastfmHelper.cs @@ -81,16 +81,20 @@ namespace MediaBrowser.Providers.Music } // Only grab the date here if the album doesn't already have one, since id3 tags are preferred - if (!item.PremiereDate.HasValue) - { - DateTime release; + DateTime release; - if (DateTime.TryParse(data.releasedate, out release)) + if (DateTime.TryParse(data.releasedate, out release)) + { + // Lastfm sends back null as sometimes 1901, other times 0 + if (release.Year > 1901) { - // Lastfm sends back null as sometimes 1901, other times 0 - if (release.Year > 1901) + if (!item.PremiereDate.HasValue) { item.PremiereDate = release; + } + + if (!item.ProductionYear.HasValue) + { item.ProductionYear = release.Year; } } diff --git a/MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs b/MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs index d95365b02..5c923869f 100644 --- a/MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs @@ -37,32 +37,34 @@ namespace MediaBrowser.Providers.Music get { return "FanArt"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is MusicAlbum; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { + var album = (MusicAlbum)item; + var list = new List<RemoteImageInfo>(); - var artistMusicBrainzId = item.Parent.GetProviderId(MetadataProviders.Musicbrainz); + var artistMusicBrainzId = album.Parent.GetProviderId(MetadataProviders.Musicbrainz); if (!string.IsNullOrEmpty(artistMusicBrainzId)) { var artistXmlPath = FanArtArtistProvider.GetArtistDataPath(_config.CommonApplicationPaths, artistMusicBrainzId); artistXmlPath = Path.Combine(artistXmlPath, "fanart.xml"); - var musicBrainzReleaseGroupId = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + var musicBrainzReleaseGroupId = album.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); - var musicBrainzId = item.GetProviderId(MetadataProviders.Musicbrainz); + var musicBrainzId = album.GetProviderId(MetadataProviders.Musicbrainz); try { @@ -74,7 +76,7 @@ namespace MediaBrowser.Providers.Music } } - var language = _config.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs b/MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs index cdb07d3d7..ddf5064aa 100644 --- a/MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs +++ b/MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs @@ -37,23 +37,25 @@ namespace MediaBrowser.Providers.Music get { return "FanArt"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is MusicArtist; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { + var artist = (MusicArtist)item; + var list = new List<RemoteImageInfo>(); - var artistMusicBrainzId = item.GetProviderId(MetadataProviders.Musicbrainz); + var artistMusicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz); if (!string.IsNullOrEmpty(artistMusicBrainzId)) { @@ -70,7 +72,7 @@ namespace MediaBrowser.Providers.Music } } - var language = _config.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs b/MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs index 72e8c6f6b..6d6f1ec7b 100644 --- a/MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs +++ b/MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs @@ -23,19 +23,19 @@ namespace MediaBrowser.Providers.Music get { return "last.fm"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is MusicAlbum || item is MusicArtist; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var list = new List<RemoteImageInfo>(); diff --git a/MediaBrowser.Providers/Savers/ChannelXmlSaver.cs b/MediaBrowser.Providers/Savers/ChannelXmlSaver.cs index 6880c9948..ad7f1287f 100644 --- a/MediaBrowser.Providers/Savers/ChannelXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/ChannelXmlSaver.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Providers.Savers // If new metadata has been downloaded or metadata was manually edited, proceed if ((wasMetadataEdited || wasMetadataDownloaded)) { - return item is Channel; + return item is LiveTvChannel; } return false; diff --git a/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs b/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs index 35dd551f1..91e769994 100644 --- a/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs @@ -92,6 +92,21 @@ namespace MediaBrowser.Providers.Savers builder.Append("<SeasonNumber>" + SecurityElement.Escape(episode.ParentIndexNumber.Value.ToString(_usCulture)) + "</SeasonNumber>"); } + if (episode.AbsoluteEpisodeNumber.HasValue) + { + builder.Append("<absolute_number>" + SecurityElement.Escape(episode.AbsoluteEpisodeNumber.Value.ToString(_usCulture)) + "</absolute_number>"); + } + + if (episode.DvdEpisodeNumber.HasValue) + { + builder.Append("<DVD_episodenumber>" + SecurityElement.Escape(episode.DvdEpisodeNumber.Value.ToString(_usCulture)) + "</DVD_episodenumber>"); + } + + if (episode.DvdSeasonNumber.HasValue) + { + builder.Append("<DVD_season>" + SecurityElement.Escape(episode.DvdSeasonNumber.Value.ToString(_usCulture)) + "</DVD_season>"); + } + if (episode.PremiereDate.HasValue) { builder.Append("<FirstAired>" + SecurityElement.Escape(episode.PremiereDate.Value.ToString("yyyy-MM-dd")) + "</FirstAired>"); @@ -113,7 +128,10 @@ namespace MediaBrowser.Providers.Savers "EpisodeNumberEnd", "airsafter_season", "airsbefore_episode", - "airsbefore_season" + "airsbefore_season", + "DVD_episodenumber", + "DVD_season", + "absolute_number" }); } diff --git a/MediaBrowser.Providers/Savers/MovieXmlSaver.cs b/MediaBrowser.Providers/Savers/MovieXmlSaver.cs index 17dca6008..f10e24dc1 100644 --- a/MediaBrowser.Providers/Savers/MovieXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/MovieXmlSaver.cs @@ -1,10 +1,9 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; -using MediaBrowser.Providers.Movies; -using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -50,7 +49,8 @@ namespace MediaBrowser.Providers.Savers return !trailer.IsLocalTrailer; } - return item is Movie || item is MusicVideo || item is AdultVideo; + // Check parent for null to avoid running this against things like video backdrops + return item is Video && !(item is Episode) && item.Parent != null; } return false; diff --git a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs index 522b2c90b..dc2d5eddd 100644 --- a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Savers "LocalTitle", "LockData", "LockedFields", - "MediaInfo", + "Format3D", "MPAARating", "MusicbrainzId", "MusicBrainzReleaseGroupId", @@ -326,12 +326,12 @@ namespace MediaBrowser.Providers.Savers } } - var hasLanguage = item as IHasLanguage; + var hasLanguage = item as IHasPreferredMetadataLanguage; if (hasLanguage != null) { - if (!string.IsNullOrEmpty(hasLanguage.Language)) + if (!string.IsNullOrEmpty(hasLanguage.PreferredMetadataLanguage)) { - builder.Append("<Language>" + SecurityElement.Escape(hasLanguage.Language) + "</Language>"); + builder.Append("<Language>" + SecurityElement.Escape(hasLanguage.PreferredMetadataLanguage) + "</Language>"); } } @@ -426,8 +426,6 @@ namespace MediaBrowser.Providers.Savers { if (hasTagline.Taglines.Count > 0) { - builder.Append("<TagLine>" + SecurityElement.Escape(hasTagline.Taglines[0]) + "</TagLine>"); - builder.Append("<Taglines>"); foreach (var tagline in hasTagline.Taglines) @@ -449,8 +447,6 @@ namespace MediaBrowser.Providers.Savers } builder.Append("</Genres>"); - - builder.Append("<Genre>" + SecurityElement.Escape(string.Join("|", item.Genres.ToArray())) + "</Genre>"); } if (item.Studios.Count > 0) @@ -534,18 +530,6 @@ namespace MediaBrowser.Providers.Savers { var video = item as Video; - builder.Append("<MediaInfo>"); - - builder.Append("<Video>"); - - if (item.RunTimeTicks.HasValue) - { - var timespan = TimeSpan.FromTicks(item.RunTimeTicks.Value); - - builder.Append("<Duration>" + Convert.ToInt64(timespan.TotalMinutes).ToString(UsCulture) + "</Duration>"); - builder.Append("<DurationSeconds>" + Convert.ToInt64(timespan.TotalSeconds).ToString(UsCulture) + "</DurationSeconds>"); - } - if (video != null && video.Video3DFormat.HasValue) { switch (video.Video3DFormat.Value) @@ -564,10 +548,6 @@ namespace MediaBrowser.Providers.Savers break; } } - - builder.Append("</Video>"); - - builder.Append("</MediaInfo>"); } } } diff --git a/MediaBrowser.Providers/Studios/StudioImageProvider.cs b/MediaBrowser.Providers/Studios/StudioImageProvider.cs new file mode 100644 index 000000000..6d8d023db --- /dev/null +++ b/MediaBrowser.Providers/Studios/StudioImageProvider.cs @@ -0,0 +1,154 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Studios +{ + public class StudioImageProvider : BaseMetadataProvider + { + private readonly IProviderManager _providerManager; + private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5); + + public StudioImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) + : base(logManager, configurationManager) + { + _providerManager = providerManager; + } + + public override bool Supports(BaseItem item) + { + return item is Studio; + } + + public override bool RequiresInternet + { + get + { + return true; + } + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate; + } + } + + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) + { + return false; + } + + return base.NeedsRefreshInternal(item, providerInfo); + } + + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + protected override string ProviderVersion + { + get + { + return "5"; + } + } + + public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) + { + if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb)) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + return true; + } + + var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, StudiosManualImageProvider.ProviderName).ConfigureAwait(false); + + await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false); + + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + return true; + } + + private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken) + { + if (!item.LockedFields.Contains(MetadataFields.Images)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!item.HasImage(ImageType.Primary)) + { + await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); + } + cancellationToken.ThrowIfCancellationRequested(); + + if (!item.HasImage(ImageType.Thumb)) + { + await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); + } + } + + if (!item.LockedFields.Contains(MetadataFields.Backdrops)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.BackdropImagePaths.Count == 0) + { + foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) + { + await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken) + .ConfigureAwait(false); + + break; + } + } + } + } + + + private async Task SaveImage(BaseItem item, IEnumerable<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken) + { + foreach (var image in images.Where(i => i.Type == type)) + { + try + { + await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false); + break; + } + catch (HttpException ex) + { + // Sometimes fanart has bad url's in their xml + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Third; } + } + } +} diff --git a/MediaBrowser.Providers/Studios/StudiosManualImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosManualImageProvider.cs new file mode 100644 index 000000000..49f552093 --- /dev/null +++ b/MediaBrowser.Providers/Studios/StudiosManualImageProvider.cs @@ -0,0 +1,135 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Studios +{ + public class StudiosManualImageProvider : IImageProvider + { + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "Media Browser"; } + } + + public bool Supports(IHasImages item) + { + return item is Studio; + } + + public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) + { + return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Backdrop, cancellationToken); + } + + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) + { + return GetImages(item, true, true, cancellationToken); + } + + private Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, bool posters, bool backdrops, CancellationToken cancellationToken) + { + var list = new List<RemoteImageInfo>(); + + if (posters) + { + list.Add(GetImage(item, "posters.txt", ImageType.Primary, "folder")); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (backdrops) + { + list.Add(GetImage(item, "thumbs.txt", ImageType.Thumb, "thumb")); + } + + return Task.FromResult(list.Where(i => i != null)); + } + + private RemoteImageInfo GetImage(IHasImages item, string filename, ImageType type, string remoteFilename) + { + var url = GetUrl(item, filename, remoteFilename); + + if (url != null) + { + return new RemoteImageInfo + { + ProviderName = Name, + Type = type, + Url = url + }; + } + + return null; + } + + private string GetUrl(IHasImages item, string listingFilename, string remoteFilename) + { + var list = GetAvailableImages(listingFilename); + + var match = FindMatch(item, list); + + if (!string.IsNullOrEmpty(match)) + { + return GetUrl(match, remoteFilename); + } + + return null; + } + + private string FindMatch(IHasImages item, IEnumerable<string> images) + { + var name = GetComparableName(item.Name); + + return images.FirstOrDefault(i => string.Equals(name, GetComparableName(i), StringComparison.OrdinalIgnoreCase)); + } + + private string GetComparableName(string name) + { + return name.Replace(" ", string.Empty).Replace(".", string.Empty).Replace("&", string.Empty).Replace("!", string.Empty); + } + + private string GetUrl(string image, string filename) + { + return string.Format("https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/studios/{0}/{1}.jpg", image, filename); + } + + private IEnumerable<string> GetAvailableImages(string filename) + { + var path = GetType().Namespace + "." + filename; + + using (var stream = GetType().Assembly.GetManifestResourceStream(path)) + { + using (var reader = new StreamReader(stream)) + { + var lines = new List<string>(); + + while (!reader.EndOfStream) + { + var text = reader.ReadLine(); + + lines.Add(text); + } + + return lines; + } + } + } + + public int Priority + { + get { return 0; } + } + } +} diff --git a/MediaBrowser.Providers/Studios/posters.txt b/MediaBrowser.Providers/Studios/posters.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/MediaBrowser.Providers/Studios/posters.txt diff --git a/MediaBrowser.Providers/Studios/thumbs.txt b/MediaBrowser.Providers/Studios/thumbs.txt new file mode 100644 index 000000000..95fed18f1 --- /dev/null +++ b/MediaBrowser.Providers/Studios/thumbs.txt @@ -0,0 +1,493 @@ +15 Gigs +19 Entertainment +20th Television +321 Productions +4K Media +4Kids Entertainment +A La Carte Communications +A&E +Aardman +ABC +ABC Family +ABC News +ABC Studios +Above Average +Acacia Fitness +Action Television +Advertising Age +All Channel Films +All3Media +Alli +Alliance Entertainment +Alloy +AllWarriorNetwork +American Pop Classics +Ananey +Anchor Bay Entertainment +Anderson Digital +Animal Planet +Animation Domination High-Def +Anime Network +Aniplex +Artists Den Entertainment +Asian Crush +Atlantic Records +Attention Span +Austin City Limits Music Festival +Australian Broadcasting Corporation +Australian Food TV +Avalon UK +Azteca America +Bandai +Base 79 +BBC Worldwide +Beliefnet +Believe +BET +Beta Film +Big Air Studios +BIGFlix +bio +Blame Society +Blastro Networks +Bloody Disgusting Selects +Bloomberg +Bonnier TV Group +Border Entertainment +Brain Farm +Brainstorm Media +Brave New Films +Bravo +Broadway Video +Brushfire Records +Butaca +BVTV +C3 Entertainment +Canal 13 de Chile +Candlelight Media +Candor TV +Caracol Television +Carsey Werner +CBS +CelebTV +Charlie Rose +Cheflive +CHIC.TV +Chiller +China Lion +Cine Real +Cinedigm +CINELAN +Cinema Guild +Cinema Libre Studio +Cinema Purgatorio +CineSport +Cirque du Soleil +Citizens United Productions No. 3 +CJ Entertainment +Classic Media +Clinton Global Initiative +Cloo +ClubWPT +CNBC +CODA BOOKS +CollegeHumor +Comedy Central +Comedy Time +Conde Nast Digital +Constantin Film +Content and Co +Content Family +Content Media Corporation +Contentino +Cooking Channel +Crackle +Crime & Investigation Network +Criterion Collection +CRM +Cuppa Coffee +Dark Sky Films +Dave Matthews Band +Davis Panzer +Debutante Inc +Digital Artists +Digital Rights Group +Digital Studios +Discovery Channel +Discovery +Distribber +Diva +DIY Network +DocComTV +DramaFever +Duopoly +E! Entertainment +EA Sports +Eagle Media +Eagle Rock +Echo Bridge Entertainment +Echo Pictures +EchoBoom Sports +Edmunds +ElecPlay +Electric Entertainment +Electric Sky +ELLE +EMI +Enchanted Tales +Endemol +Entertainment Rights +eOne Entertainment Distribution +Epicurious.com +Eqal +Esquire Network +Estrella TV +Everyday Edisons +Evil Global +Exclusive Media +ExerciseTV +Fanclub +Fangoria +FEARnet +Fever Dreams +Fight TV +Film Ideas on Demand +Film Movement +Film Sales Company +FilmBuff +Finley-Holiday Films +First Look Studios +First Run Features +Focus Features +Food Network +FORA.tv +Ford +FOX +Fox College Sports +Fox Movie Channel +Fox News +Fox Reality +Fox Sports +Fox Sports Net +Fox Television Classics +Frantic Films +FremantleMedia +FUEL TV +FUNimation +FX +FXM +FXX +G4 +Gaiam +Galavision +GameTrailers +Generate +George Dickel +Giant Ape Media +Glamour Films +GoDigital +Golf TV +Gong +Gorilla Pictures +Gravitas +Gravitas Horror +GreenLight Media +GT Media +H2 +Handmade TV +Hat Trick +HD Films, Inc +Health Science Channel +HealthiNation +HereTV +HGTV +Historic Films +History +History en Español +HitFix +Hollywood Pictures +How it Works +Howcast +Howdini +Hudsun Media +Hulu Original Series +Hype +Iconix +iCue.com +IFC +IFC Films +IGN +Image Entertainment +Imagina US +Independent Comedy Network +Independent International Pictures Corp +Indie Crush +IndieFlix +itsallinyourhands.tv +ITV +ITV1 +Janson Media +Jim Henson Family TV +K2 +KCET +Kidz Bop +Kino Lorber +KinoNation +Klown +Koan +L Studio +Lagardere +Laguna Productions +Latin Crush +Legend Fighting Championship +Legend Films +Lifetime +Link TV +Lionsgate +Liquid Comics +Litton Entertainment +LMN +Local Food Sustainable Network +Logo +lolflix +Long Way Round +Look +Lou Reda Productions +Lucha Libre USA +LXTV +MAN +Manga Entertainment +Manolin Studios +Mar Vista +Martha Stewart Living +Marvel +Maverick Entertainment +Maya +MBC America +Media Blasters +Mentorn +MGM +MHz Networks +Midnight Pulp +Military History +Millennium Media Services +Modelinia +Mojo +MoMedia +Monterey Media +Moonscoop +Moshcam +Movieola +Movies by OHM +Moving Art +MPI +MSNBC +MTV +MulticomTV +MVD Entertainment Group +My Vortexx +My Yoga +MyNetworkTV +NASA +Nat Geo Wild +National Geographic Channel +NBC +NBC News +NBC Sports +NBC Universal +NBCU TV +NCircle +Netflix +New Renaissance +NHL +Nickelodeon +NickMom +Nikki Sixx +Nirvana Films +NIS America +Novel Ruby Productions +NowThisNews +nuvoTV +O2 Media +OhmTV +Oops Doughnuts +Ora TV +Orange Lounge +ORF Universum +Oscilloscope Laboratories +Oxygen +Paley Media +Panna +Paranormal TV +Passion River +PBS Kids +Phase 4 Films +Players Network +Plum TV +PopSugar TV +Power Rangers +PPI Releasing +PRO +Pure Adrenaline +Pure History +Pure Nature +Pure Science +Questar +Quintus Media +Quiver +Rajshri Media +Raphael Saadiq +Razor & Tie +RCTV +Real Magic TV +Red Bull +Red Hour Digital +ReelAfrican +ReelzChannel +Revolver +Rick Steves' Network +RiffTrax +Right Network +Riverhorse +Roadside Attractions +Ron Hazelton Productions +RooftopComedy +Rovio +RSA +RT +RTE +S and S Entertainment +Saavn +Sachs Judah +Salient Media +Satelight +Saturday Morning TV +SBS +SBS Australia +Scholastic +Science Channel +Scott Entertainment +Screen Media +Sesame Street +Shaftesbury +Shemaroo +Shochiku +Shout! Factory +Showtime +Shree International +Sky Studios +SnagFilms +SOFA +SOMA +Sonar Entertainment +Sony Pictures Television +SoPeachi +Source Interlink Media +SpaceRip +SPEED +Speed Racer Enterprises +Spike +Spike TV +Stand Up To Cancer +Starz +Strand Releasing +Strike.TV +Sundance Channel +SunWorld Pictures +Sweet Irony +Syfy +Syndicado +Synergetic +Talking Baseball with Ed Randall +Tantao Entertainment +TasteTV +Telepictures +TenduTV +The Cannell Studios +The CW +The Democratic National Convention +The Denis Leary Podcasts +The Global Film Initiative +The Jim Henson Company +The Kitchen Diva +The LXD +The Military Network +The Morning After +The National Film Board of Canada +The New York Times +The Onion +The OnLine Network +The Orchard +The Rebound +The Situation Workout +The Sundance Institute +The Three Stooges +The Weinstein Company +The White House +The Wine Library +The Zalman King Company +This Week In Studios +Thunderbird +Tiny Island Productions +TLA Releasing +TLC +TMS Entertainment +Toei Animation +Tokyopop +Total College Sports +Total Content Digital +Touchstone Pictures +Tr3s +Transworld +Travel Channel +Troma +TV Globo +TV Land +TVF International +TVG Interactive Horseracing +TVGN +Twentieth Century Fox +Uncork'd Entertainment +UniMas +Universal Pictures +Universal Sports +Universal Television +Univision +unwrapped.tv +USA +USA Network +uStudio +Vanguard Cinema +Venevision +Venus +VH1 +Vibrant Media +Videofashion +viewster +ViKi +Virgil Films +Vision Films +Vivendi Entertainment +VIZ Media +Vogue.TV +Wall Street Journal +Warner Bros. Records +WatchMojo.com +Water.org +WCG +WE tv +Web Therapy +Well Go +WEP +Westchester Films +Wolfe Video +WWE +Yan Can Cook +Young Hollywood +YourTango +ZDF Enterprises +ZED +Zeitgeist Films +Zodiak Kids +Zodiak Rights +ZoomTV
\ No newline at end of file diff --git a/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs b/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs index 4427e60e4..3e7597e0d 100644 --- a/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs +++ b/MediaBrowser.Providers/TV/EpisodeIndexNumberProvider.cs @@ -50,7 +50,12 @@ namespace MediaBrowser.Providers.TV /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> public override bool Supports(BaseItem item) { - return item is Episode && item.LocationType != LocationType.Virtual && item.LocationType != LocationType.Remote; + if (item is Episode) + { + var locationType = item.LocationType; + return locationType != LocationType.Virtual && locationType != LocationType.Remote; + } + return false; } /// <summary> diff --git a/MediaBrowser.Providers/TV/EpisodeXmlParser.cs b/MediaBrowser.Providers/TV/EpisodeXmlParser.cs index 5d970107e..446c34f74 100644 --- a/MediaBrowser.Providers/TV/EpisodeXmlParser.cs +++ b/MediaBrowser.Providers/TV/EpisodeXmlParser.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities.TV; +using System; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Logging; @@ -40,7 +41,7 @@ namespace MediaBrowser.Providers.TV } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + /// <summary> /// Fetches the data from XML node. /// </summary> @@ -142,6 +143,55 @@ namespace MediaBrowser.Providers.TV break; } + case "absolute_number": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AbsoluteEpisodeNumber = rval; + } + } + + break; + } + case "DVD_episodenumber": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + float num; + + if (float.TryParse(number, NumberStyles.Any, UsCulture, out num)) + { + item.DvdEpisodeNumber = num; + } + } + break; + } + + case "DVD_season": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + float num; + + if (float.TryParse(number, NumberStyles.Any, UsCulture, out num)) + { + item.DvdSeasonNumber = Convert.ToInt32(num); + } + } + break; + } + case "airsbefore_episode": { var val = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs b/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs index ed5145bc2..50ce72a89 100644 --- a/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs @@ -101,11 +101,10 @@ namespace MediaBrowser.Providers.TV { cancellationToken.ThrowIfCancellationRequested(); - var season = (Season)item; + var season = (Season) item; // Process images var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartSeasonImageProvider.ProviderName).ConfigureAwait(false); - await FetchImages(season, images.ToList(), cancellationToken).ConfigureAwait(false); SetLastRefreshed(item, DateTime.UtcNow, providerInfo); @@ -121,7 +120,7 @@ namespace MediaBrowser.Providers.TV /// <returns>Task.</returns> private async Task FetchImages(Season season, List<RemoteImageInfo> images, CancellationToken cancellationToken) { - if (ConfigurationManager.Configuration.DownloadSeasonImages.Thumb && !season.HasImage(ImageType.Thumb)) + if (ConfigurationManager.Configuration.DownloadSeasonImages.Thumb && !season.HasImage(ImageType.Thumb) && !season.LockedFields.Contains(MetadataFields.Images)) { await SaveImage(season, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); } @@ -143,6 +142,7 @@ namespace MediaBrowser.Providers.TV { continue; } + break; } } } diff --git a/MediaBrowser.Providers/TV/FanArtTVProvider.cs b/MediaBrowser.Providers/TV/FanArtTVProvider.cs index 251420261..286702b8c 100644 --- a/MediaBrowser.Providers/TV/FanArtTVProvider.cs +++ b/MediaBrowser.Providers/TV/FanArtTVProvider.cs @@ -196,56 +196,61 @@ namespace MediaBrowser.Providers.TV /// <returns>Task.</returns> private async Task FetchFromXml(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - if (ConfigurationManager.Configuration.DownloadSeriesImages.Primary && !item.HasImage(ImageType.Primary)) + if (!item.LockedFields.Contains(MetadataFields.Images)) { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadSeriesImages.Primary && !item.HasImage(ImageType.Primary)) + { + await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadSeriesImages.Logo && !item.HasImage(ImageType.Logo)) - { - await SaveImage(item, images, ImageType.Logo, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadSeriesImages.Logo && !item.HasImage(ImageType.Logo)) + { + await SaveImage(item, images, ImageType.Logo, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadSeriesImages.Art && !item.HasImage(ImageType.Art)) - { - await SaveImage(item, images, ImageType.Art, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadSeriesImages.Art && !item.HasImage(ImageType.Art)) + { + await SaveImage(item, images, ImageType.Art, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadSeriesImages.Thumb && !item.HasImage(ImageType.Thumb)) - { - await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadSeriesImages.Thumb && !item.HasImage(ImageType.Thumb)) + { + await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner)) - { - await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner)) + { + await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false); + } + } - var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops; - if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && - item.BackdropImagePaths.Count < backdropLimit) + if (!item.LockedFields.Contains(MetadataFields.Backdrops)) { - foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) + cancellationToken.ThrowIfCancellationRequested(); + + var backdropLimit = ConfigurationManager.Configuration.TvOptions.MaxBackdrops; + if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && + item.BackdropImagePaths.Count < backdropLimit) { - await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Backdrop, null, cancellationToken) - .ConfigureAwait(false); + foreach (var image in images.Where(i => i.Type == ImageType.Backdrop)) + { + await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Backdrop, null, cancellationToken) + .ConfigureAwait(false); - if (item.BackdropImagePaths.Count >= backdropLimit) break; + if (item.BackdropImagePaths.Count >= backdropLimit) break; + } } } - } private async Task SaveImage(BaseItem item, List<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken) @@ -264,6 +269,7 @@ namespace MediaBrowser.Providers.TV { continue; } + break; } } } diff --git a/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs b/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs index 8ef04ed11..46137a211 100644 --- a/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs +++ b/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Providers.TV { - class FanArtTvUpdatesPrescanTask : ILibraryPrescanTask + class FanArtTvUpdatesPrescanTask : ILibraryPostScanTask { private const string UpdatesUrl = "http://api.fanart.tv/webservice/newtv/{0}/{1}/"; diff --git a/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs b/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs index f9b779011..503c56d0d 100644 --- a/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs @@ -37,35 +37,36 @@ namespace MediaBrowser.Providers.TV get { return "FanArt"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Season; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var list = new List<RemoteImageInfo>(); - var series = ((Season)item).Series; + var season = (Season)item; + var series = season.Series; if (series != null) { var id = series.GetProviderId(MetadataProviders.Tvdb); - if (!string.IsNullOrEmpty(id) && item.IndexNumber.HasValue) + if (!string.IsNullOrEmpty(id) && season.IndexNumber.HasValue) { var xmlPath = FanArtTvProvider.Current.GetFanartXmlPath(id); try { - AddImages(list, item.IndexNumber.Value, xmlPath, cancellationToken); + AddImages(list, season.IndexNumber.Value, xmlPath, cancellationToken); } catch (FileNotFoundException) { @@ -74,7 +75,7 @@ namespace MediaBrowser.Providers.TV } } - var language = _config.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs b/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs index cb7a4efd1..6a80f720a 100644 --- a/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs @@ -37,19 +37,19 @@ namespace MediaBrowser.Providers.TV get { return "FanArt"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Series; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var list = new List<RemoteImageInfo>(); @@ -71,7 +71,7 @@ namespace MediaBrowser.Providers.TV } } - var language = _config.Configuration.PreferredMetadataLanguage; + var language = item.GetPreferredMetadataLanguage(); var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs index d63fb5091..6d38dee2e 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs @@ -31,19 +31,19 @@ namespace MediaBrowser.Providers.TV get { return "TheTVDB"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Episode; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var episode = (Episode)item; diff --git a/MediaBrowser.Providers/TV/ManualTvdbPersonImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbPersonImageProvider.cs index adeca12f2..456db1048 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbPersonImageProvider.cs @@ -37,19 +37,19 @@ namespace MediaBrowser.Providers.TV get { return "TheTVDB"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Person; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var seriesWithPerson = _library.RootFolder .RecursiveChildren diff --git a/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs index 3efd0a3e3..d9a6f6507 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs @@ -38,19 +38,19 @@ namespace MediaBrowser.Providers.TV get { return "TheTVDB"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Season; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { var season = (Season)item; @@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.TV try { - var result = GetImages(path, season.IndexNumber.Value, cancellationToken); + var result = GetImages(path, item.GetPreferredMetadataLanguage(), season.IndexNumber.Value, cancellationToken); return Task.FromResult(result); } @@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.TV return Task.FromResult<IEnumerable<RemoteImageInfo>>(new RemoteImageInfo[] { }); } - private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, int seasonNumber, CancellationToken cancellationToken) + private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, int seasonNumber, CancellationToken cancellationToken) { var settings = new XmlReaderSettings { @@ -123,13 +123,11 @@ namespace MediaBrowser.Providers.TV } } - var language = _config.Configuration.PreferredMetadataLanguage; - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); return list.OrderByDescending(i => { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) { return 3; } diff --git a/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs index 5987215d1..644cad93b 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs @@ -38,21 +38,22 @@ namespace MediaBrowser.Providers.TV get { return "TheTVDB"; } } - public bool Supports(BaseItem item) + public bool Supports(IHasImages item) { return item is Series; } - public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken) { var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); return images.Where(i => i.Type == imageType); } - public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken) + public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken) { - var seriesId = item.GetProviderId(MetadataProviders.Tvdb); + var series = (Series)item; + var seriesId = series.GetProviderId(MetadataProviders.Tvdb); if (!string.IsNullOrEmpty(seriesId)) { @@ -63,7 +64,7 @@ namespace MediaBrowser.Providers.TV try { - var result = GetImages(path, cancellationToken); + var result = GetImages(path, item.GetPreferredMetadataLanguage(), cancellationToken); return Task.FromResult(result); } @@ -76,7 +77,7 @@ namespace MediaBrowser.Providers.TV return Task.FromResult<IEnumerable<RemoteImageInfo>>(new RemoteImageInfo[] { }); } - private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, CancellationToken cancellationToken) + private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken) { var settings = new XmlReaderSettings { @@ -121,13 +122,11 @@ namespace MediaBrowser.Providers.TV } } - var language = _config.Configuration.PreferredMetadataLanguage; - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); return list.OrderByDescending(i => { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) { return 3; } diff --git a/MediaBrowser.Providers/TV/SeasonIndexNumberProvider.cs b/MediaBrowser.Providers/TV/SeasonIndexNumberProvider.cs new file mode 100644 index 000000000..593784201 --- /dev/null +++ b/MediaBrowser.Providers/TV/SeasonIndexNumberProvider.cs @@ -0,0 +1,83 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + class SeasonIndexNumberProvider : BaseMetadataProvider + { + /// <summary> + /// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class. + /// </summary> + /// <param name="logManager">The log manager.</param> + /// <param name="configurationManager">The configuration manager.</param> + public SeasonIndexNumberProvider(ILogManager logManager, IServerConfigurationManager configurationManager) + : base(logManager, configurationManager) + { + } + + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + protected override string ProviderVersion + { + get + { + return "2"; + } + } + + /// <summary> + /// Supportses the specified item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + public override bool Supports(BaseItem item) + { + if (item is Season) + { + var locationType = item.LocationType; + return locationType != LocationType.Virtual && locationType != LocationType.Remote; + } + return false; + } + + /// <summary> + /// Fetches metadata and returns true or false indicating if any work that requires persistence was done + /// </summary> + /// <param name="item">The item.</param> + /// <param name="force">if set to <c>true</c> [force].</param> + /// <param name="providerInfo">The provider information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{System.Boolean}.</returns> + public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) + { + item.IndexNumber = TVUtils.GetSeasonNumberFromPath(item.Path); + + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + + return TrueTaskResult; + } + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.First; } + } + } +} diff --git a/MediaBrowser.Providers/TV/SeasonProviderFromXml.cs b/MediaBrowser.Providers/TV/SeasonProviderFromXml.cs index 2127234dc..9fbcad7c0 100644 --- a/MediaBrowser.Providers/TV/SeasonProviderFromXml.cs +++ b/MediaBrowser.Providers/TV/SeasonProviderFromXml.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.TV /// <value>The priority.</value> public override MetadataProviderPriority Priority { - get { return MetadataProviderPriority.First; } + get { return MetadataProviderPriority.Second; } } private const string XmlFileName = "season.xml"; diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index b1df4e553..cc24cad5d 100644 --- a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs +++ b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs @@ -39,8 +39,7 @@ namespace MediaBrowser.Providers.TV private async Task RunInternal(IProgress<double> progress, CancellationToken cancellationToken) { - if (!_config.Configuration.EnableInternetProviders || - _config.Configuration.InternetProviderExcludeTypes.Contains(typeof(Series).Name, StringComparer.OrdinalIgnoreCase)) + if (!_config.Configuration.EnableInternetProviders) { progress.Report(100); return; @@ -456,7 +455,7 @@ namespace MediaBrowser.Providers.TV { _logger.Info("Creating Season {0} entry for {1}", seasonNumber, series.Name); - var name = string.Format("Season {0}", seasonNumber.ToString(UsCulture)); + var name = seasonNumber == 0 ? _config.Configuration.SeasonZeroDisplayName : string.Format("Season {0}", seasonNumber.ToString(UsCulture)); var season = new Season { diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs index 5d9f387fe..f5e21bf69 100644 --- a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs @@ -404,6 +404,58 @@ namespace MediaBrowser.Providers.TV break; } + case "DVD_episodenumber": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + float num; + + if (float.TryParse(val, NumberStyles.Any, _usCulture, out num)) + { + item.DvdEpisodeNumber = num; + } + } + + break; + } + + case "DVD_season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + float num; + + if (float.TryParse(val, NumberStyles.Any, _usCulture, out num)) + { + item.DvdSeasonNumber = Convert.ToInt32(num); + } + } + + break; + } + + case "absolute_number": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval)) + { + item.AbsoluteEpisodeNumber = rval; + } + } + + break; + } + case "airsbefore_episode": { var val = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs index 3a503ea20..f2ce92efd 100644 --- a/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs @@ -75,9 +75,10 @@ namespace MediaBrowser.Providers.TV SetLastRefreshed(item, DateTime.UtcNow, providerInfo); return true; } + private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken) { - if (!item.HasImage(ImageType.Primary)) + if (!item.HasImage(ImageType.Primary) && !item.LockedFields.Contains(MetadataFields.Images)) { var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); diff --git a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs index df5c39b65..24d231192 100644 --- a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs +++ b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs @@ -3,6 +3,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; @@ -45,6 +46,7 @@ namespace MediaBrowser.Providers.TV /// </summary> private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; /// <summary> /// Initializes a new instance of the <see cref="TvdbPrescanTask"/> class. @@ -52,12 +54,13 @@ namespace MediaBrowser.Providers.TV /// <param name="logger">The logger.</param> /// <param name="httpClient">The HTTP client.</param> /// <param name="config">The config.</param> - public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem) + public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem, ILibraryManager libraryManager) { _logger = logger; _httpClient = httpClient; _config = config; _fileSystem = fileSystem; + _libraryManager = libraryManager; } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); @@ -70,8 +73,7 @@ namespace MediaBrowser.Providers.TV /// <returns>Task.</returns> public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - if (!_config.Configuration.EnableInternetProviders || - _config.Configuration.InternetProviderExcludeTypes.Contains(typeof(Series).Name, StringComparer.OrdinalIgnoreCase)) + if (!_config.Configuration.EnableInternetProviders) { progress.Report(100); return; @@ -274,19 +276,36 @@ namespace MediaBrowser.Providers.TV var list = seriesIds.ToList(); var numComplete = 0; + // Gather all series into a lookup by tvdb id + var allSeries = _libraryManager.RootFolder.RecursiveChildren + .OfType<Series>() + .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) + .ToLookup(i => i.GetProviderId(MetadataProviders.Tvdb)); + foreach (var seriesId in list) { - try - { - await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) + // Find the preferred language(s) for the movie in the library + var languages = allSeries[seriesId] + .Select(i => i.GetPreferredMetadataLanguage()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var language in languages) { - // Already logged at lower levels, but don't fail the whole operation, unless timed out - // We have to fail this to make it run again otherwise new episode data could potentially be missing - if (ex.IsTimedOut) + try { - throw; + await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, language, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + _logger.ErrorException("Error updating tvdb series id {0}, language {1}", ex, seriesId, language); + + // Already logged at lower levels, but don't fail the whole operation, unless timed out + // We have to fail this to make it run again otherwise new episode data could potentially be missing + if (ex.IsTimedOut) + { + throw; + } } } @@ -305,17 +324,18 @@ namespace MediaBrowser.Providers.TV /// <param name="id">The id.</param> /// <param name="seriesDataPath">The series data path.</param> /// <param name="lastTvDbUpdateTime">The last tv db update time.</param> + /// <param name="preferredMetadataLanguage">The preferred metadata language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, CancellationToken cancellationToken) + private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) { - _logger.Info("Updating series " + id); + _logger.Info("Updating movie from tmdb " + id + ", language " + preferredMetadataLanguage); seriesDataPath = Path.Combine(seriesDataPath, id); Directory.CreateDirectory(seriesDataPath); - return TvdbSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, lastTvDbUpdateTime, cancellationToken); + return TvdbSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); } } } diff --git a/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs b/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs index cdfe86dac..17ed6b5a2 100644 --- a/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.IO; +using System.Net; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -6,6 +7,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using System; using System.Collections.Generic; @@ -157,29 +159,20 @@ namespace MediaBrowser.Providers.TV private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, int backdropLimit, CancellationToken cancellationToken) { - if (!item.HasImage(ImageType.Primary)) + if (!item.LockedFields.Contains(MetadataFields.Images)) { - var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); - - if (image != null) + if (!item.HasImage(ImageType.Primary)) { - await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) - .ConfigureAwait(false); + await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); } - } - - if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) - { - var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); - if (image != null) + if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) { - await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) - .ConfigureAwait(false); + await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false); } } - if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit) + if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit && !item.LockedFields.Contains(MetadataFields.Backdrops)) { foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop)) { @@ -196,5 +189,26 @@ namespace MediaBrowser.Providers.TV } } } + + private async Task SaveImage(BaseItem item, List<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken) + { + foreach (var image in images.Where(i => i.Type == type)) + { + try + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, type, null, cancellationToken).ConfigureAwait(false); + break; + } + catch (HttpException ex) + { + // Sometimes fanart has bad url's in their xml + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } } } diff --git a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs index c1ac0e386..21a72dd57 100644 --- a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs @@ -137,13 +137,13 @@ namespace MediaBrowser.Providers.TV protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) + if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.TvOptions.MaxBackdrops) { return false; } return base.NeedsRefreshInternal(item, providerInfo); } - + /// <summary> /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// </summary> @@ -167,33 +167,36 @@ namespace MediaBrowser.Providers.TV private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, int backdropLimit, CancellationToken cancellationToken) { - if (!item.HasImage(ImageType.Primary)) + if (!item.LockedFields.Contains(MetadataFields.Images)) { - var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); - - if (image != null) + if (!item.HasImage(ImageType.Primary)) { - await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) - .ConfigureAwait(false); - } - } + var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); - if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner)) - { - var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) + .ConfigureAwait(false); + } + } - if (image != null) + if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner)) { - await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) - .ConfigureAwait(false); + var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); + + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) + .ConfigureAwait(false); + } } } - if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit) + if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit && !item.LockedFields.Contains(MetadataFields.Backdrops)) { foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop && (!i.Width.HasValue || - i.Width.Value >= ConfigurationManager.Configuration.MinSeriesBackdropDownloadWidth))) + i.Width.Value >= ConfigurationManager.Configuration.TvOptions.MinBackdropWidth))) { var url = backdrop.Url; diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs index 5691f885e..4df391e2a 100644 --- a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs @@ -248,13 +248,13 @@ namespace MediaBrowser.Providers.TV .Select(Path.GetFileName) .ToList(); - var seriesXmlFilename = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"; + var seriesXmlFilename = series.GetPreferredMetadataLanguage().ToLower() + ".xml"; // Only download if not already there // The prescan task will take care of updates so we don't need to re-download here if (!files.Contains("banners.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains("actors.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains(seriesXmlFilename, StringComparer.OrdinalIgnoreCase)) { - await DownloadSeriesZip(seriesId, seriesDataPath, null, cancellationToken).ConfigureAwait(false); + await DownloadSeriesZip(seriesId, seriesDataPath, null, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); } // Have to check this here since we prevent the normal enforcement through ProviderManager @@ -285,11 +285,13 @@ namespace MediaBrowser.Providers.TV /// </summary> /// <param name="seriesId">The series id.</param> /// <param name="seriesDataPath">The series data path.</param> + /// <param name="lastTvDbUpdateTime">The last tv database update time.</param> + /// <param name="preferredMetadataLanguage">The preferred metadata language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, CancellationToken cancellationToken) + internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) { - var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); + var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, preferredMetadataLanguage); using (var zipStream = await HttpClient.Get(new HttpRequestOptions { @@ -319,7 +321,7 @@ namespace MediaBrowser.Providers.TV await SanitizeXmlFile(file).ConfigureAwait(false); } - await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, ConfigurationManager.Configuration.PreferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false); + await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, preferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false); } private void DeleteXmlFiles(string path) @@ -852,7 +854,7 @@ namespace MediaBrowser.Providers.TV if (!string.IsNullOrWhiteSpace(val)) { // Only fill this in if there's no existing genres, because Imdb data from Omdb is preferred - if (!item.LockedFields.Contains(MetadataFields.Genres) && (item.Genres.Count == 0 || !string.Equals(ConfigurationManager.Configuration.PreferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))) + if (!item.LockedFields.Contains(MetadataFields.Genres) && (item.Genres.Count == 0 || !string.Equals(item.GetPreferredMetadataLanguage(), "en", StringComparison.OrdinalIgnoreCase))) { var vals = val .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) diff --git a/MediaBrowser.Providers/VirtualItemImageValidator.cs b/MediaBrowser.Providers/VirtualItemImageValidator.cs new file mode 100644 index 000000000..d4bbaf713 --- /dev/null +++ b/MediaBrowser.Providers/VirtualItemImageValidator.cs @@ -0,0 +1,48 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers +{ + public class VirtualItemImageValidator : BaseMetadataProvider + { + public VirtualItemImageValidator(ILogManager logManager, IServerConfigurationManager configurationManager) + : base(logManager, configurationManager) + { + } + + public override bool Supports(BaseItem item) + { + var locationType = item.LocationType; + + return locationType == LocationType.Virtual || + locationType == LocationType.Remote; + } + + public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) + { + item.ValidateImages(); + item.ValidateBackdrops(); + + var hasScreenshots = item as IHasScreenshots; + + if (hasScreenshots != null) + { + hasScreenshots.ValidateScreenshots(); + } + + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + return TrueTaskResult; + } + + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.First; } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs index 8165e11eb..94438e3e0 100644 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -69,10 +69,9 @@ namespace MediaBrowser.Server.Implementations.Configuration /// </summary> private void UpdateItemsByNamePath() { - if (!string.IsNullOrEmpty(Configuration.ItemsByNamePath)) - { - ApplicationPaths.ItemsByNamePath = Configuration.ItemsByNamePath; - } + ((ServerApplicationPaths) ApplicationPaths).ItemsByNamePath = string.IsNullOrEmpty(Configuration.ItemsByNamePath) ? + null : + Configuration.ItemsByNamePath; } /// <summary> @@ -84,19 +83,29 @@ namespace MediaBrowser.Server.Implementations.Configuration { var newConfig = (ServerConfiguration) newConfiguration; - var newIbnPath = newConfig.ItemsByNamePath; + ValidateItemByNamePath(newConfig); + + base.ReplaceConfiguration(newConfiguration); + } + + /// <summary> + /// Replaces the item by name path. + /// </summary> + /// <param name="newConfig">The new configuration.</param> + /// <exception cref="System.IO.DirectoryNotFoundException"></exception> + private void ValidateItemByNamePath(ServerConfiguration newConfig) + { + var newPath = newConfig.ItemsByNamePath; - if (!string.IsNullOrWhiteSpace(newIbnPath) - && !string.Equals(Configuration.ItemsByNamePath ?? string.Empty, newIbnPath)) + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(Configuration.ItemsByNamePath ?? string.Empty, newPath)) { // Validate - if (!Directory.Exists(newIbnPath)) + if (!Directory.Exists(newPath)) { - throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newConfig.ItemsByNamePath)); + throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); } } - - base.ReplaceConfiguration(newConfiguration); } } } diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs index f9cf90787..3d53d2b86 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; @@ -69,6 +68,8 @@ namespace MediaBrowser.Server.Implementations.Drawing { fs.CopyTo(memoryStream); + memoryStream.Position = 0; + // Co it the old fashioned way using (var b = Image.FromStream(memoryStream, true, false)) { diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs index 27ce90787..7ddf63cf8 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs @@ -53,10 +53,6 @@ namespace MediaBrowser.Server.Implementations.Drawing private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationPaths _appPaths; - private readonly string _croppedWhitespaceImageCachePath; - private readonly string _enhancedImageCachePath; - private readonly string _resizedImageCachePath; - public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer) { _logger = logger; @@ -64,10 +60,6 @@ namespace MediaBrowser.Server.Implementations.Drawing _jsonSerializer = jsonSerializer; _appPaths = appPaths; - _croppedWhitespaceImageCachePath = Path.Combine(appPaths.ImageCachePath, "cropped-images"); - _enhancedImageCachePath = Path.Combine(appPaths.ImageCachePath, "enhanced-images"); - _resizedImageCachePath = Path.Combine(appPaths.ImageCachePath, "resized-images"); - _saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite); Dictionary<Guid, ImageSize> sizeDictionary; @@ -92,6 +84,30 @@ namespace MediaBrowser.Server.Implementations.Drawing _cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary); } + private string ResizedImageCachePath + { + get + { + return Path.Combine(_appPaths.ImageCachePath, "resized-images"); + } + } + + private string EnhancedImageCachePath + { + get + { + return Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); + } + } + + private string CroppedWhitespaceImageCachePath + { + get + { + return Path.Combine(_appPaths.ImageCachePath, "cropped-images"); + } + } + public void AddParts(IEnumerable<IImageEnhancer> enhancers) { ImageEnhancers = enhancers.ToArray(); @@ -212,8 +228,12 @@ namespace MediaBrowser.Server.Implementations.Drawing // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here using (var thumbnail = new Bitmap(newWidth, newHeight, PixelFormat.Format32bppPArgb)) { - // Preserve the original resolution - thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution); + // Mono throw an exeception if assign 0 to SetResolution + if (originalImage.HorizontalResolution >= 0 && originalImage.VerticalResolution >= 0) + { + // Preserve the original resolution + thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution); + } using (var thumbnailGraph = Graphics.FromImage(thumbnail)) { @@ -391,7 +411,7 @@ namespace MediaBrowser.Server.Implementations.Drawing var name = originalImagePath; name += "datemodified=" + dateModified.Ticks; - var croppedImagePath = GetCachePath(_croppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath)); + var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath)); var semaphore = GetLock(croppedImagePath); @@ -480,7 +500,7 @@ namespace MediaBrowser.Server.Implementations.Drawing filename += "b=" + backgroundColor; } - return GetCachePath(_resizedImageCachePath, filename, Path.GetExtension(originalPath)); + return GetCachePath(ResizedImageCachePath, filename, Path.GetExtension(originalPath)); } /// <summary> @@ -578,7 +598,7 @@ namespace MediaBrowser.Server.Implementations.Drawing /// <param name="imagePath">The image path.</param> /// <returns>Guid.</returns> /// <exception cref="System.ArgumentNullException">item</exception> - public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath) + public Guid GetImageCacheTag(IHasImages item, ImageType imageType, string imagePath) { if (item == null) { @@ -607,7 +627,7 @@ namespace MediaBrowser.Server.Implementations.Drawing /// <param name="imageEnhancers">The image enhancers.</param> /// <returns>Guid.</returns> /// <exception cref="System.ArgumentNullException">item</exception> - public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified, List<IImageEnhancer> imageEnhancers) + public Guid GetImageCacheTag(IHasImages item, ImageType imageType, string originalImagePath, DateTime dateModified, List<IImageEnhancer> imageEnhancers) { if (item == null) { @@ -644,7 +664,7 @@ namespace MediaBrowser.Server.Implementations.Drawing /// <param name="imageType">Type of the image.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns>Task{System.String}.</returns> - public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex) + public async Task<string> GetEnhancedImage(IHasImages item, ImageType imageType, int imageIndex) { var enhancers = GetSupportedEnhancers(item, imageType).ToList(); @@ -657,7 +677,7 @@ namespace MediaBrowser.Server.Implementations.Drawing return result.Item1; } - private async Task<Tuple<string, DateTime>> GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, + private async Task<Tuple<string, DateTime>> GetEnhancedImage(string originalImagePath, DateTime dateModified, IHasImages item, ImageType imageType, int imageIndex, List<IImageEnhancer> enhancers) { @@ -693,7 +713,7 @@ namespace MediaBrowser.Server.Implementations.Drawing /// <param name="supportedEnhancers">The supported enhancers.</param> /// <returns>System.String.</returns> /// <exception cref="System.ArgumentNullException">originalImagePath</exception> - private async Task<string> GetEnhancedImageInternal(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex, List<IImageEnhancer> supportedEnhancers) + private async Task<string> GetEnhancedImageInternal(string originalImagePath, DateTime dateModified, IHasImages item, ImageType imageType, int imageIndex, List<IImageEnhancer> supportedEnhancers) { if (string.IsNullOrEmpty(originalImagePath)) { @@ -708,7 +728,7 @@ namespace MediaBrowser.Server.Implementations.Drawing var cacheGuid = GetImageCacheTag(item, imageType, originalImagePath, dateModified, supportedEnhancers); // All enhanced images are saved as png to allow transparency - var enhancedImagePath = GetCachePath(_enhancedImageCachePath, cacheGuid + ".png"); + var enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + ".png"); var semaphore = GetLock(enhancedImagePath); @@ -766,7 +786,7 @@ namespace MediaBrowser.Server.Implementations.Drawing /// <param name="imageType">Type of the image.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns>Task{EnhancedImage}.</returns> - private async Task<Image> ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex) + private async Task<Image> ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, Image originalImage, IHasImages item, ImageType imageType, int imageIndex) { var result = originalImage; @@ -884,7 +904,7 @@ namespace MediaBrowser.Server.Implementations.Drawing return Path.Combine(path, filename); } - public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType) + public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType) { return ImageEnhancers.Where(i => { diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index ba9dd170d..932c7ba3f 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -163,8 +163,7 @@ namespace MediaBrowser.Server.Implementations.Dto { var folder = (Folder)item; - dto.ChildCount = folder.GetChildren(user, true) - .Count(); + dto.ChildCount = GetChildCount(folder, user); if (!(folder is UserRootFolder)) { @@ -182,6 +181,12 @@ namespace MediaBrowser.Server.Implementations.Dto } } + private int GetChildCount(Folder folder, User user) + { + return folder.GetChildren(user, true) + .Count(); + } + public UserDto GetUserDto(User user) { if (user == null) @@ -238,7 +243,8 @@ namespace MediaBrowser.Server.Implementations.Dto NowViewingItemType = session.NowViewingItemType, ApplicationVersion = session.ApplicationVersion, CanSeek = session.CanSeek, - QueueableMediaTypes = session.QueueableMediaTypes + QueueableMediaTypes = session.QueueableMediaTypes, + RemoteEndPoint = session.RemoteEndPoint }; if (session.NowPlayingItem != null) @@ -287,8 +293,6 @@ namespace MediaBrowser.Server.Implementations.Dto return info; } - const string IndexFolderDelimeter = "-index-"; - /// <summary> /// Gets client-side Id of a server-side BaseItem /// </summary> @@ -302,13 +306,6 @@ namespace MediaBrowser.Server.Implementations.Dto throw new ArgumentNullException("item"); } - var indexFolder = item as IndexFolder; - - if (indexFolder != null) - { - return GetDtoId(indexFolder.Parent) + IndexFolderDelimeter + (indexFolder.IndexName ?? string.Empty) + IndexFolderDelimeter + indexFolder.Id; - } - return item.Id.ToString("N"); } @@ -613,26 +610,15 @@ namespace MediaBrowser.Server.Implementations.Dto throw new ArgumentNullException("id"); } - // If the item is an indexed folder we have to do a special routine to get it - var isIndexFolder = id.IndexOf(IndexFolderDelimeter, StringComparison.OrdinalIgnoreCase) != -1; - - if (isIndexFolder) - { - if (userId.HasValue) - { - return GetIndexFolder(id, userId.Value); - } - } - BaseItem item = null; - if (userId.HasValue || !isIndexFolder) + if (userId.HasValue) { item = _libraryManager.GetItemById(new Guid(id)); } // If we still don't find it, look within individual user views - if (item == null && !userId.HasValue && isIndexFolder) + if (item == null && !userId.HasValue) { foreach (var user in _userManager.Users) { @@ -649,60 +635,6 @@ namespace MediaBrowser.Server.Implementations.Dto } /// <summary> - /// Finds an index folder based on an Id and userId - /// </summary> - /// <param name="id">The id.</param> - /// <param name="userId">The user id.</param> - /// <returns>BaseItem.</returns> - private BaseItem GetIndexFolder(string id, Guid userId) - { - var user = _userManager.GetUserById(userId); - - var stringSeparators = new[] { IndexFolderDelimeter }; - - // Split using the delimeter - var values = id.Split(stringSeparators, StringSplitOptions.None).ToList(); - - // Get the top folder normally using the first id - var folder = GetItemByDtoId(values[0], userId) as Folder; - - values.RemoveAt(0); - - // Get indexed folders using the remaining values in the id string - return GetIndexFolder(values, folder, user); - } - - /// <summary> - /// Gets indexed folders based on a list of index names and folder id's - /// </summary> - /// <param name="values">The values.</param> - /// <param name="parentFolder">The parent folder.</param> - /// <param name="user">The user.</param> - /// <returns>BaseItem.</returns> - private BaseItem GetIndexFolder(List<string> values, Folder parentFolder, User user) - { - // The index name is first - var indexBy = values[0]; - - // The index folder id is next - var indexFolderId = new Guid(values[1]); - - // Remove them from the lst - values.RemoveRange(0, 2); - - // Get the IndexFolder - var indexFolder = parentFolder.GetChildren(user, false, indexBy).FirstOrDefault(i => i.Id == indexFolderId) as Folder; - - // Nested index folder - if (values.Count > 0) - { - return GetIndexFolder(values, indexFolder, user); - } - - return indexFolder; - } - - /// <summary> /// Sets simple property values on a DTOBaseItem /// </summary> /// <param name="dto">The dto.</param> @@ -723,7 +655,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.DisplayMediaType = item.DisplayMediaType; - if (fields.Contains(ItemFields.MetadataSettings)) + if (fields.Contains(ItemFields.Settings)) { dto.LockedFields = item.LockedFields; dto.EnableInternetProviders = !item.DontFetchMeta; @@ -807,10 +739,12 @@ namespace MediaBrowser.Server.Implementations.Dto dto.MediaType = item.MediaType; dto.LocationType = item.LocationType; - var hasLanguage = item as IHasLanguage; - if (hasLanguage != null) + var hasLang = item as IHasPreferredMetadataLanguage; + + if (hasLang != null) { - dto.Language = hasLanguage.Language; + dto.PreferredMetadataCountryCode = hasLang.PreferredMetadataCountryCode; + dto.PreferredMetadataLanguage = hasLang.PreferredMetadataLanguage; } var hasCriticRating = item as IHasCriticRating; @@ -887,7 +821,7 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.ParentLogoItemId = GetDtoId(parentWithLogo); - dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo)); + dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImagePath(ImageType.Logo)); } } @@ -900,7 +834,7 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.ParentArtItemId = GetDtoId(parentWithImage); - dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art, parentWithImage.GetImage(ImageType.Art)); + dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art, parentWithImage.GetImagePath(ImageType.Art)); } } @@ -913,7 +847,7 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.ParentThumbItemId = GetDtoId(parentWithImage); - dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb, parentWithImage.GetImage(ImageType.Thumb)); + dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb, parentWithImage.GetImagePath(ImageType.Thumb)); } } @@ -1061,7 +995,13 @@ namespace MediaBrowser.Server.Implementations.Dto if (episode != null) { dto.IndexNumberEnd = episode.IndexNumberEnd; - dto.SpecialSeasonNumber = episode.AirsAfterSeasonNumber ?? episode.AirsBeforeSeasonNumber; + + dto.DvdSeasonNumber = episode.DvdSeasonNumber; + dto.DvdEpisodeNumber = episode.DvdEpisodeNumber; + dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber; + dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber; + dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber; + dto.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber; var seasonId = episode.SeasonId; if (seasonId.HasValue) @@ -1082,6 +1022,11 @@ namespace MediaBrowser.Server.Implementations.Dto dto.SpecialFeatureCount = series.SpecialFeatureIds.Count; dto.SeasonCount = series.SeasonCount; + + if (fields.Contains(ItemFields.Settings)) + { + dto.DisplaySpecialsWithSeasons = series.DisplaySpecialsWithSeasons; + } } if (episode != null) @@ -1095,7 +1040,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (series.HasImage(ImageType.Thumb)) { - dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImage(ImageType.Thumb)); + dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImagePath(ImageType.Thumb)); } var imagePath = series.PrimaryImagePath; @@ -1197,8 +1142,21 @@ namespace MediaBrowser.Server.Implementations.Dto double totalPercentPlayed = 0; + IEnumerable<BaseItem> children; + + var season = folder as Season; + + if (season != null) + { + children = season.GetEpisodes(user).Where(i => i.LocationType != LocationType.Virtual); + } + else + { + children = folder.GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual); + } + // Loop through each recursive child - foreach (var child in folder.GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)) + foreach (var child in children) { var userdata = _userDataRepository.GetUserData(user.Id, child.GetUserDataKey()); diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 29dce6747..34d705bfb 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -5,6 +5,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; using ServiceStack; +using ServiceStack.Api.Swagger; using ServiceStack.Host; using ServiceStack.Host.Handlers; using ServiceStack.Host.HttpListener; @@ -63,8 +64,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer _logger = logManager.GetLogger("HttpServer"); - LogManager.LogFactory = new ServerLogFactory(logManager); - _containerAdapter = new ContainerAdapter(applicationHost); for (var i = 0; i < 2; i++) @@ -95,7 +94,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer container.Adapter = _containerAdapter; - //Plugins.Add(new SwaggerFeature()); + Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature()); HostContext.GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse); } @@ -477,7 +476,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer ServiceController = CreateServiceController(); _logger.Info("Calling ServiceStack AppHost.Init"); - Init(); + + base.Init(); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs index 57acddc43..c403c09b4 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs @@ -1,6 +1,7 @@ using MediaBrowser.Common; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; +using ServiceStack.Logging; namespace MediaBrowser.Server.Implementations.HttpServer { @@ -20,6 +21,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// <returns>IHttpServer.</returns> public static IHttpServer CreateServer(IApplicationHost applicationHost, ILogManager logManager, string serverName, string handlerPath, string defaultRedirectpath) { + LogManager.LogFactory = new ServerLogFactory(logManager); + return new HttpListenerHost(applicationHost, logManager, serverName, handlerPath, defaultRedirectpath); } } diff --git a/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs b/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs index a2240f52d..1efc3bc70 100644 --- a/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs +++ b/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs @@ -529,27 +529,26 @@ namespace MediaBrowser.Server.Implementations.IO return; } - await Task.WhenAll(itemsToRefresh.Select(i => Task.Run(async () => + foreach (var item in itemsToRefresh) { - Logger.Info(i.Name + " (" + i.Path + ") will be refreshed."); + Logger.Info(item.Name + " (" + item.Path + ") will be refreshed."); try { - await i.ChangedExternally().ConfigureAwait(false); + await item.ChangedExternally().ConfigureAwait(false); } catch (IOException ex) { // For now swallow and log. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) // Should we remove it from it's parent? - Logger.ErrorException("Error refreshing {0}", ex, i.Name); + Logger.ErrorException("Error refreshing {0}", ex, item.Name); } catch (Exception ex) { - Logger.ErrorException("Error refreshing {0}", ex, i.Name); + Logger.ErrorException("Error refreshing {0}", ex, item.Name); } - - }))).ConfigureAwait(false); + } } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 95ec416b6..5268faa4f 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -114,6 +114,12 @@ namespace MediaBrowser.Server.Implementations.Library { return true; } + + // Don't misidentify xbmc trailers as a movie + if (filename.IndexOf(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } } } diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 41694765d..11c99a32c 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -899,6 +899,15 @@ namespace MediaBrowser.Server.Implementations.Library } /// <summary> + /// Queues the library scan. + /// </summary> + public void QueueLibraryScan() + { + // Just run the scheduled task so that the user can see it + _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>(); + } + + /// <summary> /// Validates the media library internal. /// </summary> /// <param name="progress">The progress.</param> @@ -1311,11 +1320,6 @@ namespace MediaBrowser.Server.Implementations.Library { var list = items.ToList(); - foreach (var item in list) - { - item.DateLastSaved = DateTime.UtcNow; - } - await ItemRepository.SaveItems(list, cancellationToken).ConfigureAwait(false); foreach (var item in list) diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 03e29dd38..3d6f7e66a 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -91,31 +91,43 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies if (args.Path.IndexOf("[trailers]", StringComparison.OrdinalIgnoreCase) != -1 || string.Equals(collectionType, CollectionType.Trailers, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Trailer>(args.Path, args.FileSystemChildren); + return FindMovie<Trailer>(args.Path, args.Parent, args.FileSystemChildren); } if (args.Path.IndexOf("[musicvideos]", StringComparison.OrdinalIgnoreCase) != -1 || string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<MusicVideo>(args.Path, args.FileSystemChildren); + return FindMovie<MusicVideo>(args.Path, args.Parent, args.FileSystemChildren); } if (args.Path.IndexOf("[adultvideos]", StringComparison.OrdinalIgnoreCase) != -1 || string.Equals(collectionType, CollectionType.AdultVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<AdultVideo>(args.Path, args.FileSystemChildren); + return FindMovie<AdultVideo>(args.Path, args.Parent, args.FileSystemChildren); } + if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) + { + return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren); + } + if (string.IsNullOrEmpty(collectionType) || string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase) || string.Equals(collectionType, CollectionType.BoxSets, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Movie>(args.Path, args.FileSystemChildren); + return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren); } return null; } + var filename = Path.GetFileName(args.Path); + // Don't misidentify xbmc trailers as a movie + if (filename.IndexOf(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) != -1) + { + return null; + } + // Find movies that are mixed in the same folder if (args.Path.IndexOf("[trailers]", StringComparison.OrdinalIgnoreCase) != -1 || string.Equals(collectionType, CollectionType.Trailers, StringComparison.OrdinalIgnoreCase)) @@ -172,7 +184,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies private void SetProviderIdFromPath(Video item) { //we need to only look at the name of this actual item (not parents) - var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(Path.GetDirectoryName(item.Path)); + var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.MetaLocation); var id = justName.GetAttributeValue("tmdbid"); @@ -187,9 +199,10 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies /// </summary> /// <typeparam name="T"></typeparam> /// <param name="path">The path.</param> + /// <param name="parent">The parent.</param> /// <param name="fileSystemEntries">The file system entries.</param> /// <returns>Movie.</returns> - private T FindMovie<T>(string path, IEnumerable<FileSystemInfo> fileSystemEntries) + private T FindMovie<T>(string path, Folder parent, IEnumerable<FileSystemInfo> fileSystemEntries) where T : Video, new() { var movies = new List<T>(); @@ -237,7 +250,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies var childArgs = new ItemResolveArgs(_applicationPaths, _libraryManager) { FileInfo = child, - Path = child.FullName + Path = child.FullName, + Parent = parent }; var item = ResolveVideo<T>(childArgs); @@ -261,7 +275,9 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies if (multiDiscFolders.Count > 0) { - return GetMultiDiscMovie<T>(multiDiscFolders); + var folders = fileSystemEntries.Where(child => (child.Attributes & FileAttributes.Directory) == FileAttributes.Directory); + + return GetMultiDiscMovie<T>(multiDiscFolders, folders); } return null; @@ -271,25 +287,26 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies /// Gets the multi disc movie. /// </summary> /// <typeparam name="T"></typeparam> - /// <param name="folders">The folders.</param> + /// <param name="multiDiscFolders">The folders.</param> + /// <param name="allFolders">All folders.</param> /// <returns>``0.</returns> - private T GetMultiDiscMovie<T>(List<FileSystemInfo> folders) + private T GetMultiDiscMovie<T>(List<FileSystemInfo> multiDiscFolders, IEnumerable<FileSystemInfo> allFolders) where T : Video, new() { - var videoType = VideoType.BluRay; + var videoTypes = new List<VideoType>(); - var folderPaths = folders.Select(i => i.FullName).Where(i => + var folderPaths = multiDiscFolders.Select(i => i.FullName).Where(i => { var subfolders = Directory.GetDirectories(i).Select(Path.GetFileName).ToList(); if (subfolders.Any(IsDvdDirectory)) { - videoType = VideoType.Dvd; + videoTypes.Add(VideoType.Dvd); return true; } if (subfolders.Any(IsBluRayDirectory)) { - videoType = VideoType.BluRay; + videoTypes.Add(VideoType.BluRay); return true; } @@ -297,18 +314,46 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies }).OrderBy(i => i).ToList(); + // If different video types were found, don't allow this + if (videoTypes.Count > 0 && videoTypes.Any(i => i != videoTypes[0])) + { + return null; + } + if (folderPaths.Count == 0) { return null; } + // If there are other folders side by side that are folder rips, don't allow it + // TODO: Improve this to return null if any folder is present aside from our regularly ignored folders + if (allFolders.Except(multiDiscFolders).Any(i => + { + var subfolders = Directory.GetDirectories(i.FullName).Select(Path.GetFileName).ToList(); + + if (subfolders.Any(IsDvdDirectory)) + { + return true; + } + if (subfolders.Any(IsBluRayDirectory)) + { + return true; + } + + return false; + + })) + { + return null; + } + return new T { Path = folderPaths[0], IsMultiPart = true, - VideoType = videoType + VideoType = videoTypes[0] }; } diff --git a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs index 8d010aecc..79f126511 100644 --- a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs @@ -49,7 +49,7 @@ namespace MediaBrowser.Server.Implementations.Library /// userId /// or /// key</exception> - public async Task SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) + public async Task SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) { if (userData == null) { diff --git a/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs index 4243aecfe..d4a74f2b6 100644 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserManager.cs @@ -22,39 +22,10 @@ namespace MediaBrowser.Server.Implementations.Library public class UserManager : IUserManager { /// <summary> - /// The _users - /// </summary> - private IEnumerable<User> _users; - /// <summary> - /// The _user lock - /// </summary> - private object _usersSyncLock = new object(); - /// <summary> - /// The _users initialized - /// </summary> - private bool _usersInitialized; - /// <summary> /// Gets the users. /// </summary> /// <value>The users.</value> - public IEnumerable<User> Users - { - get - { - // Call ToList to exhaust the stream because we'll be iterating over this multiple times - LazyInitializer.EnsureInitialized(ref _users, ref _usersInitialized, ref _usersSyncLock, LoadUsers); - return _users; - } - internal set - { - _users = value; - - if (value == null) - { - _usersInitialized = false; - } - } - } + public IEnumerable<User> Users { get; private set; } /// <summary> /// The _logger @@ -78,11 +49,13 @@ namespace MediaBrowser.Server.Implementations.Library /// </summary> /// <param name="logger">The logger.</param> /// <param name="configurationManager">The configuration manager.</param> + /// <param name="userRepository">The user repository.</param> public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository) { _logger = logger; UserRepository = userRepository; ConfigurationManager = configurationManager; + Users = new List<User>(); } #region UserUpdated Event @@ -132,6 +105,11 @@ namespace MediaBrowser.Server.Implementations.Library return Users.FirstOrDefault(u => u.Id == id); } + public async Task Initialize() + { + Users = await LoadUsers().ConfigureAwait(false); + } + /// <summary> /// Authenticates a User and returns a result indicating whether or not it succeeded /// </summary> @@ -185,7 +163,7 @@ namespace MediaBrowser.Server.Implementations.Library /// Loads the users from the repository /// </summary> /// <returns>IEnumerable{User}.</returns> - private IEnumerable<User> LoadUsers() + private async Task<IEnumerable<User>> LoadUsers() { var users = UserRepository.RetrieveAllUsers().ToList(); @@ -198,10 +176,7 @@ namespace MediaBrowser.Server.Implementations.Library user.DateLastSaved = DateTime.UtcNow; - var task = UserRepository.SaveUser(user, CancellationToken.None); - - // Hate having to block threads - Task.WaitAll(task); + await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); users.Add(user); } @@ -284,7 +259,7 @@ namespace MediaBrowser.Server.Implementations.Library } public event EventHandler<GenericEventArgs<User>> UserCreated; - + /// <summary> /// Creates the user. /// </summary> @@ -311,11 +286,11 @@ namespace MediaBrowser.Server.Implementations.Library Users = list; user.DateLastSaved = DateTime.UtcNow; - + await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); - + return user; } @@ -377,10 +352,10 @@ namespace MediaBrowser.Server.Implementations.Library } } - OnUserDeleted(user); - // Force this to be lazy loaded again - Users = null; + Users = await LoadUsers().ConfigureAwait(false); + + OnUserDeleted(user); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs index e16430e69..d04ebe32d 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -7,6 +9,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; +using System.IO; using System.Linq; using System.Net; using System.Threading; @@ -18,17 +21,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv { private readonly ILiveTvManager _liveTvManager; private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; - public ChannelImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager) + public ChannelImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient) : base(logManager, configurationManager) { _liveTvManager = liveTvManager; _providerManager = providerManager; + _fileSystem = fileSystem; + _httpClient = httpClient; } public override bool Supports(BaseItem item) { - return item is Channel; + return item is LiveTvChannel; } protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) @@ -44,9 +51,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv return true; } + var changed = true; + try { - await DownloadImage(item, cancellationToken).ConfigureAwait(false); + changed = await DownloadImage((LiveTvChannel)item, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -57,26 +66,78 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } + if (changed) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + } - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; + return changed; } - private async Task DownloadImage(BaseItem item, CancellationToken cancellationToken) + private async Task<bool> DownloadImage(LiveTvChannel item, CancellationToken cancellationToken) { - var channel = (Channel)item; + var channelInfo = item.ChannelInfo; + + Stream imageStream = null; + string contentType = null; + + if (!string.IsNullOrEmpty(channelInfo.ImagePath)) + { + contentType = "image/" + Path.GetExtension(channelInfo.ImagePath).ToLower(); + imageStream = _fileSystem.GetFileStream(channelInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); + } + else if (!string.IsNullOrEmpty(channelInfo.ImageUrl)) + { + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = channelInfo.ImageUrl + }; + + var response = await _httpClient.GetResponse(options).ConfigureAwait(false); - var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, channel.ServiceName, StringComparison.OrdinalIgnoreCase)); + if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + Logger.Error("Provider did not return an image content type."); + return false; + } - if (service != null) + imageStream = response.Content; + contentType = response.ContentType; + } + else if (channelInfo.HasImage ?? true) { - var response = await service.GetChannelImageAsync(channel.ChannelId, cancellationToken).ConfigureAwait(false); + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase)); + if (service != null) + { + try + { + var response = await service.GetChannelImageAsync(channelInfo.Id, cancellationToken).ConfigureAwait(false); + + if (response != null) + { + imageStream = response.Stream; + contentType = response.MimeType; + } + } + catch (NotImplementedException) + { + return false; + } + } + } + + if (imageStream != null) + { // Dummy up the original url - var url = channel.ServiceName + channel.ChannelId; + var url = item.ServiceName + channelInfo.Id; - await _providerManager.SaveImage(channel, response.Stream, response.MimeType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); + await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); + return true; } + + return false; } public override MetadataProviderPriority Priority diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs new file mode 100644 index 000000000..0b2d0c5e9 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -0,0 +1,503 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.LiveTv +{ + public class LiveTvDtoService + { + private readonly ILogger _logger; + private readonly IImageProcessor _imageProcessor; + + private readonly IUserDataManager _userDataManager; + private readonly IDtoService _dtoService; + + public LiveTvDtoService(IDtoService dtoService, IUserDataManager userDataManager, IImageProcessor imageProcessor, ILogger logger) + { + _dtoService = dtoService; + _userDataManager = userDataManager; + _imageProcessor = imageProcessor; + _logger = logger; + } + + public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel) + { + var dto = new TimerInfoDto + { + Id = GetInternalTimerId(service.Name, info.Id).ToString("N"), + Overview = info.Overview, + EndDate = info.EndDate, + Name = info.Name, + StartDate = info.StartDate, + ExternalId = info.Id, + ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"), + Status = info.Status, + SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"), + PrePaddingSeconds = info.PrePaddingSeconds, + PostPaddingSeconds = info.PostPaddingSeconds, + IsPostPaddingRequired = info.IsPostPaddingRequired, + IsPrePaddingRequired = info.IsPrePaddingRequired, + ExternalChannelId = info.ChannelId, + ExternalSeriesTimerId = info.SeriesTimerId, + ServiceName = service.Name, + ExternalProgramId = info.ProgramId, + Priority = info.Priority, + RunTimeTicks = (info.EndDate - info.StartDate).Ticks + }; + + if (!string.IsNullOrEmpty(info.ProgramId)) + { + dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N"); + } + + if (program != null) + { + dto.ProgramInfo = GetProgramInfoDto(program, channel.ChannelInfo.Name); + + dto.ProgramInfo.TimerId = dto.Id; + dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId; + } + + if (channel != null) + { + dto.ChannelName = channel.ChannelInfo.Name; + } + + return dto; + } + + public SeriesTimerInfoDto GetSeriesTimerInfoDto(SeriesTimerInfo info, ILiveTvService service, string channelName) + { + var dto = new SeriesTimerInfoDto + { + Id = GetInternalSeriesTimerId(service.Name, info.Id).ToString("N"), + Overview = info.Overview, + EndDate = info.EndDate, + Name = info.Name, + StartDate = info.StartDate, + ExternalId = info.Id, + PrePaddingSeconds = info.PrePaddingSeconds, + PostPaddingSeconds = info.PostPaddingSeconds, + IsPostPaddingRequired = info.IsPostPaddingRequired, + IsPrePaddingRequired = info.IsPrePaddingRequired, + Days = info.Days, + Priority = info.Priority, + RecordAnyChannel = info.RecordAnyChannel, + RecordAnyTime = info.RecordAnyTime, + RecordNewOnly = info.RecordNewOnly, + ExternalChannelId = info.ChannelId, + ExternalProgramId = info.ProgramId, + ServiceName = service.Name, + ChannelName = channelName + }; + + if (!string.IsNullOrEmpty(info.ChannelId)) + { + dto.ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"); + } + + if (!string.IsNullOrEmpty(info.ProgramId)) + { + dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N"); + } + + dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days); + + return dto; + } + + public DayPattern? GetDayPattern(List<DayOfWeek> days) + { + DayPattern? pattern = null; + + if (days.Count > 0) + { + if (days.Count == 7) + { + pattern = DayPattern.Daily; + } + else if (days.Count == 2) + { + if (days.Contains(DayOfWeek.Saturday) && days.Contains(DayOfWeek.Sunday)) + { + pattern = DayPattern.Weekends; + } + } + else if (days.Count == 5) + { + if (days.Contains(DayOfWeek.Monday) && days.Contains(DayOfWeek.Tuesday) && days.Contains(DayOfWeek.Wednesday) && days.Contains(DayOfWeek.Thursday) && days.Contains(DayOfWeek.Friday)) + { + pattern = DayPattern.Weekdays; + } + } + } + + return pattern; + } + + /// <summary> + /// Convert the provider 0-5 scale to our 0-10 scale + /// </summary> + /// <param name="val"></param> + /// <returns></returns> + private float? GetClientCommunityRating(float? val) + { + if (!val.HasValue) + { + return null; + } + + return val.Value * 2; + } + + public string GetStatusName(RecordingStatus status) + { + if (status == RecordingStatus.InProgress) + { + return "In Progress"; + } + + if (status == RecordingStatus.ConflictedNotOk) + { + return "Conflicted"; + } + + if (status == RecordingStatus.ConflictedOk) + { + return "Scheduled"; + } + + return status.ToString(); + } + + public RecordingInfoDto GetRecordingInfoDto(LiveTvRecording recording, LiveTvChannel channel, ILiveTvService service, User user = null) + { + var info = recording.RecordingInfo; + + var dto = new RecordingInfoDto + { + Id = GetInternalRecordingId(service.Name, info.Id).ToString("N"), + SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"), + Type = recording.GetClientTypeName(), + Overview = info.Overview, + EndDate = info.EndDate, + Name = info.Name, + StartDate = info.StartDate, + ExternalId = info.Id, + ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"), + Status = info.Status, + StatusName = GetStatusName(info.Status), + Path = info.Path, + Genres = info.Genres, + IsRepeat = info.IsRepeat, + EpisodeTitle = info.EpisodeTitle, + ChannelType = info.ChannelType, + MediaType = info.ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video, + CommunityRating = GetClientCommunityRating(info.CommunityRating), + OfficialRating = info.OfficialRating, + Audio = info.Audio, + IsHD = info.IsHD, + ServiceName = service.Name, + Url = info.Url, + IsMovie = info.IsMovie, + IsSeries = info.IsSeries, + IsSports = info.IsSports, + IsLive = info.IsLive, + IsNews = info.IsNews, + IsKids = info.IsKids, + IsPremiere = info.IsPremiere, + RunTimeTicks = (info.EndDate - info.StartDate).Ticks + }; + + var imageTag = GetImageTag(recording); + + if (imageTag.HasValue) + { + dto.ImageTags[ImageType.Primary] = imageTag.Value; + } + + if (user != null) + { + dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, recording.GetUserDataKey())); + } + + if (!string.IsNullOrEmpty(info.ProgramId)) + { + dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N"); + } + + if (channel != null) + { + dto.ChannelName = channel.ChannelInfo.Name; + } + + return dto; + } + + /// <summary> + /// Gets the channel info dto. + /// </summary> + /// <param name="info">The info.</param> + /// <param name="user">The user.</param> + /// <returns>ChannelInfoDto.</returns> + public ChannelInfoDto GetChannelInfoDto(LiveTvChannel info, User user = null) + { + var channelInfo = info.ChannelInfo; + + var dto = new ChannelInfoDto + { + Name = info.Name, + ServiceName = info.ServiceName, + ChannelType = channelInfo.ChannelType, + Number = channelInfo.Number, + Type = info.GetClientTypeName(), + Id = info.Id.ToString("N"), + MediaType = info.MediaType, + ExternalId = channelInfo.Id + }; + + if (user != null) + { + dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey())); + } + + var imageTag = GetImageTag(info); + + if (imageTag.HasValue) + { + dto.ImageTags[ImageType.Primary] = imageTag.Value; + } + + return dto; + } + + public ProgramInfoDto GetProgramInfoDto(LiveTvProgram item, string channelName, User user = null) + { + var program = item.ProgramInfo; + + var dto = new ProgramInfoDto + { + Id = GetInternalProgramId(item.ServiceName, program.Id).ToString("N"), + ChannelId = GetInternalChannelId(item.ServiceName, program.ChannelId).ToString("N"), + Overview = program.Overview, + EndDate = program.EndDate, + Genres = program.Genres, + ExternalId = program.Id, + Name = program.Name, + ServiceName = item.ServiceName, + StartDate = program.StartDate, + OfficialRating = program.OfficialRating, + IsHD = program.IsHD, + OriginalAirDate = program.OriginalAirDate, + Audio = program.Audio, + CommunityRating = GetClientCommunityRating(program.CommunityRating), + IsRepeat = program.IsRepeat, + EpisodeTitle = program.EpisodeTitle, + ChannelName = channelName, + IsMovie = program.IsMovie, + IsSeries = program.IsSeries, + IsSports = program.IsSports, + IsLive = program.IsLive, + IsNews = program.IsNews, + IsKids = program.IsKids, + IsPremiere = program.IsPremiere, + RunTimeTicks = (program.EndDate - program.StartDate).Ticks, + Type = "Program" + }; + + var imageTag = GetImageTag(item); + + if (imageTag.HasValue) + { + dto.ImageTags[ImageType.Primary] = imageTag.Value; + } + + if (user != null) + { + dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, item.GetUserDataKey())); + } + + return dto; + } + + private Guid? GetImageTag(BaseItem info) + { + var path = info.PrimaryImagePath; + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + try + { + return _imageProcessor.GetImageCacheTag(info, ImageType.Primary, path); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting image info for {0}", ex, info.Name); + } + + return null; + } + + public Guid GetInternalChannelId(string serviceName, string externalId) + { + var name = serviceName + externalId; + + return name.ToLower().GetMBId(typeof(LiveTvChannel)); + } + + public Guid GetInternalTimerId(string serviceName, string externalId) + { + var name = serviceName + externalId; + + return name.ToLower().GetMD5(); + } + + public Guid GetInternalSeriesTimerId(string serviceName, string externalId) + { + var name = serviceName + externalId; + + return name.ToLower().GetMD5(); + } + + public Guid GetInternalProgramId(string serviceName, string externalId) + { + var name = serviceName + externalId; + + return name.ToLower().GetMD5(); + } + + public Guid GetInternalRecordingId(string serviceName, string externalId) + { + var name = serviceName + externalId; + + return name.ToLower().GetMD5(); + } + + public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, ILiveTvManager liveTv, CancellationToken cancellationToken) + { + var info = new TimerInfo + { + Overview = dto.Overview, + EndDate = dto.EndDate, + Name = dto.Name, + StartDate = dto.StartDate, + Status = dto.Status, + PrePaddingSeconds = dto.PrePaddingSeconds, + PostPaddingSeconds = dto.PostPaddingSeconds, + IsPostPaddingRequired = dto.IsPostPaddingRequired, + IsPrePaddingRequired = dto.IsPrePaddingRequired, + Priority = dto.Priority, + SeriesTimerId = dto.ExternalSeriesTimerId, + ProgramId = dto.ExternalProgramId, + ChannelId = dto.ExternalChannelId, + Id = dto.ExternalId + }; + + // Convert internal server id's to external tv provider id's + if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id)) + { + var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false); + + info.Id = timer.ExternalId; + } + + if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId)) + { + var channel = await liveTv.GetChannel(dto.ChannelId, cancellationToken).ConfigureAwait(false); + + if (channel != null) + { + info.ChannelId = channel.ExternalId; + } + } + + if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId)) + { + var program = await liveTv.GetProgram(dto.ProgramId, cancellationToken).ConfigureAwait(false); + + if (program != null) + { + info.ProgramId = program.ExternalId; + } + } + + if (!string.IsNullOrEmpty(dto.SeriesTimerId) && string.IsNullOrEmpty(info.SeriesTimerId)) + { + var timer = await liveTv.GetSeriesTimer(dto.SeriesTimerId, cancellationToken).ConfigureAwait(false); + + if (timer != null) + { + info.SeriesTimerId = timer.ExternalId; + } + } + + return info; + } + + public async Task<SeriesTimerInfo> GetSeriesTimerInfo(SeriesTimerInfoDto dto, bool isNew, ILiveTvManager liveTv, CancellationToken cancellationToken) + { + var info = new SeriesTimerInfo + { + Overview = dto.Overview, + EndDate = dto.EndDate, + Name = dto.Name, + StartDate = dto.StartDate, + PrePaddingSeconds = dto.PrePaddingSeconds, + PostPaddingSeconds = dto.PostPaddingSeconds, + IsPostPaddingRequired = dto.IsPostPaddingRequired, + IsPrePaddingRequired = dto.IsPrePaddingRequired, + Days = dto.Days, + Priority = dto.Priority, + RecordAnyChannel = dto.RecordAnyChannel, + RecordAnyTime = dto.RecordAnyTime, + RecordNewOnly = dto.RecordNewOnly, + ProgramId = dto.ExternalProgramId, + ChannelId = dto.ExternalChannelId, + Id = dto.ExternalId + }; + + // Convert internal server id's to external tv provider id's + if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id)) + { + var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false); + + info.Id = timer.ExternalId; + } + + if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId)) + { + var channel = await liveTv.GetChannel(dto.ChannelId, cancellationToken).ConfigureAwait(false); + + if (channel != null) + { + info.ChannelId = channel.ExternalId; + } + } + + if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId)) + { + var program = await liveTv.GetProgram(dto.ProgramId, cancellationToken).ConfigureAwait(false); + + if (program != null) + { + info.ProgramId = program.ExternalId; + } + } + + return info; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 185a01663..218c930df 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -8,7 +8,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; @@ -30,29 +29,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly IItemRepository _itemRepo; - private readonly IImageProcessor _imageProcessor; - private readonly IUserManager _userManager; + private readonly ILocalizationManager _localization; - private readonly IUserDataManager _userDataManager; - private readonly IDtoService _dtoService; + private readonly LiveTvDtoService _tvDtoService; private readonly List<ILiveTvService> _services = new List<ILiveTvService>(); - private List<Channel> _channels = new List<Channel>(); - private List<ProgramInfoDto> _programs = new List<ProgramInfoDto>(); + private Dictionary<Guid, LiveTvChannel> _channels = new Dictionary<Guid, LiveTvChannel>(); + private Dictionary<Guid, LiveTvProgram> _programs = new Dictionary<Guid, LiveTvProgram>(); - public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserManager userManager, ILocalizationManager localization, IUserDataManager userDataManager, IDtoService dtoService) + public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, ILocalizationManager localization, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager) { _appPaths = appPaths; _fileSystem = fileSystem; _logger = logger; _itemRepo = itemRepo; - _imageProcessor = imageProcessor; - _userManager = userManager; _localization = localization; - _userDataManager = userDataManager; - _dtoService = dtoService; + _userManager = userManager; + + _tvDtoService = new LiveTvDtoService(dtoService, userDataManager, imageProcessor, logger); } /// <summary> @@ -77,77 +73,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv ActiveService = _services.FirstOrDefault(); } - /// <summary> - /// Gets the channel info dto. - /// </summary> - /// <param name="info">The info.</param> - /// <param name="user">The user.</param> - /// <returns>ChannelInfoDto.</returns> - public ChannelInfoDto GetChannelInfoDto(Channel info, User user) - { - var dto = new ChannelInfoDto - { - Name = info.Name, - ServiceName = info.ServiceName, - ChannelType = info.ChannelType, - Number = info.ChannelNumber, - Type = info.GetType().Name, - Id = info.Id.ToString("N"), - MediaType = info.MediaType - }; - - if (user != null) - { - dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey())); - } - - var imageTag = GetLogoImageTag(info); - - if (imageTag.HasValue) - { - dto.ImageTags[ImageType.Primary] = imageTag.Value; - } - - return dto; - } - - private Guid? GetLogoImageTag(Channel info) - { - var path = info.PrimaryImagePath; - - if (string.IsNullOrEmpty(path)) - { - return null; - } - - try - { - return _imageProcessor.GetImageCacheTag(info, ImageType.Primary, path); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting channel image info for {0}", ex, info.Name); - } - - return null; - } - - public QueryResult<ChannelInfoDto> GetChannels(ChannelQuery query) + public Task<QueryResult<ChannelInfoDto>> GetChannels(ChannelQuery query, CancellationToken cancellationToken) { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId)); - IEnumerable<Channel> channels = _channels; + IEnumerable<LiveTvChannel> channels = _channels.Values; if (user != null) { - channels = channels.Where(i => i.IsParentalAllowed(user, _localization)) + channels = channels + .Where(i => i.IsParentalAllowed(user, _localization)) .OrderBy(i => { double number = 0; - if (!string.IsNullOrEmpty(i.ChannelNumber)) + if (!string.IsNullOrEmpty(i.ChannelInfo.Number)) { - double.TryParse(i.ChannelNumber, out number); + double.TryParse(i.ChannelInfo.Number, out number); } return number; @@ -159,81 +101,81 @@ namespace MediaBrowser.Server.Implementations.LiveTv { double number = 0; - if (!string.IsNullOrEmpty(i.ChannelNumber)) + if (!string.IsNullOrEmpty(i.ChannelInfo.Number)) { - double.TryParse(i.ChannelNumber, out number); + double.TryParse(i.ChannelInfo.Number, out number); } return number; }).ThenBy(i => i.Name) - .Select(i => GetChannelInfoDto(i, user)) + .Select(i => _tvDtoService.GetChannelInfoDto(i, user)) .ToArray(); - return new QueryResult<ChannelInfoDto> + var result = new QueryResult<ChannelInfoDto> { Items = returnChannels, TotalRecordCount = returnChannels.Length }; + + return Task.FromResult(result); } - public Channel GetChannel(string id) + public LiveTvChannel GetInternalChannel(string id) { - var guid = new Guid(id); + return GetInternalChannel(new Guid(id)); + } + + private LiveTvChannel GetInternalChannel(Guid id) + { + LiveTvChannel channel = null; - return _channels.FirstOrDefault(i => i.Id == guid); + _channels.TryGetValue(id, out channel); + return channel; } - public ChannelInfoDto GetChannelInfoDto(string id, string userId) + public LiveTvProgram GetInternalProgram(string id) { - var channel = GetChannel(id); + var guid = new Guid(id); - var user = string.IsNullOrEmpty(userId) ? null : _userManager.GetUserById(new Guid(userId)); + LiveTvProgram obj = null; - return channel == null ? null : GetChannelInfoDto(channel, user); + _programs.TryGetValue(guid, out obj); + return obj; } - private ProgramInfoDto GetProgramInfoDto(ProgramInfo program, Channel channel) + public async Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken) { - var id = GetInternalProgramIdId(channel.ServiceName, program.Id).ToString("N"); + var service = ActiveService; - return new ProgramInfoDto - { - ChannelId = channel.Id.ToString("N"), - Overview = program.Overview, - EndDate = program.EndDate, - Genres = program.Genres, - ExternalId = program.Id, - Id = id, - Name = program.Name, - ServiceName = channel.ServiceName, - StartDate = program.StartDate, - OfficialRating = program.OfficialRating, - IsHD = program.IsHD, - OriginalAirDate = program.OriginalAirDate, - Audio = program.Audio, - CommunityRating = program.CommunityRating, - AspectRatio = program.AspectRatio, - IsRepeat = program.IsRepeat, - EpisodeTitle = program.EpisodeTitle - }; + var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); + + var recording = recordings.FirstOrDefault(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(id)); + + return await GetRecording(recording, service.Name, cancellationToken).ConfigureAwait(false); } - private Guid GetInternalChannelId(string serviceName, string externalChannelId, string channelName) + public async Task<StreamResponseInfo> GetRecordingStream(string id, CancellationToken cancellationToken) { - var name = serviceName + externalChannelId + channelName; + var service = ActiveService; + + var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); - return name.ToLower().GetMBId(typeof(Channel)); + var recording = recordings.FirstOrDefault(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(id)); + + return await service.GetRecordingStream(recording.Id, cancellationToken).ConfigureAwait(false); } - private Guid GetInternalProgramIdId(string serviceName, string externalProgramId) + public async Task<StreamResponseInfo> GetChannelStream(string id, CancellationToken cancellationToken) { - var name = serviceName + externalProgramId; + var service = ActiveService; + + var channel = GetInternalChannel(id); - return name.ToLower().GetMD5(); + return await service.GetChannelStream(channel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false); } - private async Task<Channel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken) + private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken) { var path = Path.Combine(_appPaths.ItemsByNamePath, "channels", _fileSystem.GetValidFilename(serviceName), _fileSystem.GetValidFilename(channelInfo.Name)); @@ -254,27 +196,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv isNew = true; } - var id = GetInternalChannelId(serviceName, channelInfo.Id, channelInfo.Name); + var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id); - var item = _itemRepo.RetrieveItem(id) as Channel; + var item = _itemRepo.RetrieveItem(id) as LiveTvChannel; if (item == null) { - item = new Channel + item = new LiveTvChannel { Name = channelInfo.Name, Id = id, DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo), DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo), - Path = path, - ChannelId = channelInfo.Id, - ChannelNumber = channelInfo.Number, - ServiceName = serviceName + Path = path }; isNew = true; } + item.ChannelInfo = channelInfo; + item.ServiceName = serviceName; + // Set this now so we don't cause additional file system access during provider executions item.ResetResolveArgs(fileInfo); @@ -283,26 +225,160 @@ namespace MediaBrowser.Server.Implementations.LiveTv return item; } + private async Task<LiveTvProgram> GetProgram(ProgramInfo info, ChannelType channelType, string serviceName, CancellationToken cancellationToken) + { + var isNew = false; + + var id = _tvDtoService.GetInternalProgramId(serviceName, info.Id); + + var item = _itemRepo.RetrieveItem(id) as LiveTvProgram; + + if (item == null) + { + item = new LiveTvProgram + { + Name = info.Name, + Id = id, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow + }; + + isNew = true; + } + + item.ChannelType = channelType; + item.ProgramInfo = info; + item.ServiceName = serviceName; + + await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false); + + return item; + } + + private async Task<LiveTvRecording> GetRecording(RecordingInfo info, string serviceName, CancellationToken cancellationToken) + { + var isNew = false; + + var id = _tvDtoService.GetInternalRecordingId(serviceName, info.Id); + + var item = _itemRepo.RetrieveItem(id) as LiveTvRecording; + + if (item == null) + { + item = new LiveTvRecording + { + Name = info.Name, + Id = id, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow + }; + + isNew = true; + } + + item.RecordingInfo = info; + item.ServiceName = serviceName; + + await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false); + + return item; + } + + private LiveTvChannel GetChannel(LiveTvProgram program) + { + var programChannelId = program.ProgramInfo.ChannelId; + + var internalProgramChannelId = _tvDtoService.GetInternalChannelId(program.ServiceName, programChannelId); + + return GetInternalChannel(internalProgramChannelId); + } + + public async Task<ProgramInfoDto> GetProgram(string id, CancellationToken cancellationToken, User user = null) + { + var program = GetInternalProgram(id); + + var channel = GetChannel(program); + + var channelName = channel == null ? null : channel.ChannelInfo.Name; + + var dto = _tvDtoService.GetProgramInfoDto(program, channelName, user); + + await AddRecordingInfo(new[] { dto }, cancellationToken).ConfigureAwait(false); + + return dto; + } + public async Task<QueryResult<ProgramInfoDto>> GetPrograms(ProgramQuery query, CancellationToken cancellationToken) { - IEnumerable<ProgramInfoDto> programs = _programs - .OrderBy(i => i.StartDate) - .ThenBy(i => i.EndDate); + IEnumerable<LiveTvProgram> programs = _programs.Values; if (query.ChannelIdList.Length > 0) { var guids = query.ChannelIdList.Select(i => new Guid(i)).ToList(); + var serviceName = ActiveService.Name; + + programs = programs.Where(i => + { + var programChannelId = i.ProgramInfo.ChannelId; + + var internalProgramChannelId = _tvDtoService.GetInternalChannelId(serviceName, programChannelId); + + return guids.Contains(internalProgramChannelId); + }); + } + + var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId)); - programs = programs.Where(i => guids.Contains(new Guid(i.ChannelId))); + if (user != null) + { + programs = programs.Where(i => i.IsParentalAllowed(user, _localization)); } - var returnArray = programs.ToArray(); + var returnArray = programs + .OrderBy(i => i.ProgramInfo.StartDate) + .Select(i => + { + var channel = GetChannel(i); + + var channelName = channel == null ? null : channel.ChannelInfo.Name; + + return _tvDtoService.GetProgramInfoDto(i, channelName, user); + }) + .ToArray(); + + await AddRecordingInfo(returnArray, cancellationToken).ConfigureAwait(false); - return new QueryResult<ProgramInfoDto> + var result = new QueryResult<ProgramInfoDto> { Items = returnArray, TotalRecordCount = returnArray.Length }; + + return result; + } + + private async Task AddRecordingInfo(IEnumerable<ProgramInfoDto> programs, CancellationToken cancellationToken) + { + var timers = await ActiveService.GetTimersAsync(cancellationToken).ConfigureAwait(false); + var timerList = timers.ToList(); + + foreach (var program in programs) + { + var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, program.ExternalId, StringComparison.OrdinalIgnoreCase)); + + if (timer != null) + { + program.TimerId = _tvDtoService.GetInternalTimerId(program.ServiceName, timer.Id) + .ToString("N"); + + if (!string.IsNullOrEmpty(timer.SeriesTimerId)) + { + program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(program.ServiceName, timer.SeriesTimerId) + .ToString("N"); + } + } + } + } internal async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken) @@ -321,8 +397,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv var allChannels = await GetChannels(service, cancellationToken).ConfigureAwait(false); var allChannelsList = allChannels.ToList(); - var list = new List<Channel>(); - var programs = new List<ProgramInfoDto>(); + var list = new List<LiveTvChannel>(); + var programs = new List<LiveTvProgram>(); var numComplete = 0; @@ -334,7 +410,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channelPrograms = await service.GetProgramsAsync(channelInfo.Item2.Id, cancellationToken).ConfigureAwait(false); - programs.AddRange(channelPrograms.Select(program => GetProgramInfoDto(program, item))); + var programTasks = channelPrograms.Select(program => GetProgram(program, item.ChannelInfo.ChannelType, service.Name, cancellationToken)); + var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false); + + programs.AddRange(programEntities); list.Add(item); } @@ -354,8 +433,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv progress.Report(90 * percent + 10); } - _programs = programs; - _channels = list; + _programs = programs.ToDictionary(i => i.Id); + _channels = list.ToDictionary(i => i.Id); } private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken) @@ -365,70 +444,73 @@ namespace MediaBrowser.Server.Implementations.LiveTv return channels.Select(i => new Tuple<string, ChannelInfo>(service.Name, i)); } - private async Task<IEnumerable<RecordingInfoDto>> GetRecordings(ILiveTvService service, CancellationToken cancellationToken) + public async Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken) { + var service = ActiveService; + + var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId)); + + var list = new List<RecordingInfo>(); + var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); + list.AddRange(recordings); - return recordings.Select(i => GetRecordingInfoDto(i, service)); - } - - private RecordingInfoDto GetRecordingInfoDto(RecordingInfo info, ILiveTvService service) - { - var id = service.Name + info.ChannelId + info.Id; - id = id.GetMD5().ToString("N"); - - var dto = new RecordingInfoDto - { - ChannelName = info.ChannelName, - Overview = info.Overview, - EndDate = info.EndDate, - Name = info.Name, - StartDate = info.StartDate, - Id = id, - ExternalId = info.Id, - ChannelId = GetInternalChannelId(service.Name, info.ChannelId, info.ChannelName).ToString("N"), - Status = info.Status, - Path = info.Path, - Genres = info.Genres, - IsRepeat = info.IsRepeat, - EpisodeTitle = info.EpisodeTitle, - ChannelType = info.ChannelType, - MediaType = info.ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video, - CommunityRating = info.CommunityRating, - OfficialRating = info.OfficialRating, - Audio = info.Audio, - IsHD = info.IsHD - }; + if (!string.IsNullOrEmpty(query.ChannelId)) + { + var guid = new Guid(query.ChannelId); - var duration = info.EndDate - info.StartDate; - dto.DurationMs = Convert.ToInt32(duration.TotalMilliseconds); + var currentServiceName = service.Name; - if (!string.IsNullOrEmpty(info.ProgramId)) + list = list + .Where(i => _tvDtoService.GetInternalChannelId(currentServiceName, i.ChannelId) == guid) + .ToList(); + } + + if (!string.IsNullOrEmpty(query.Id)) { - dto.ProgramId = GetInternalProgramIdId(service.Name, info.ProgramId).ToString("N"); + var guid = new Guid(query.Id); + + var currentServiceName = service.Name; + + list = list + .Where(i => _tvDtoService.GetInternalRecordingId(currentServiceName, i.Id) == guid) + .ToList(); } - return dto; - } + if (!string.IsNullOrEmpty(query.GroupId)) + { + var guid = new Guid(query.GroupId); - public async Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken) - { - var list = new List<RecordingInfoDto>(); + list = list.Where(i => GetRecordingGroupIds(i).Contains(guid)) + .ToList(); + } + + IEnumerable<LiveTvRecording> entities = await GetEntities(list, service.Name, cancellationToken).ConfigureAwait(false); + + entities = entities.OrderByDescending(i => i.RecordingInfo.StartDate); - if (ActiveService != null) + if (user != null) { - var recordings = await GetRecordings(ActiveService, cancellationToken).ConfigureAwait(false); + var currentUser = user; + entities = entities.Where(i => i.IsParentalAllowed(currentUser, _localization)); + } - list.AddRange(recordings); + if (query.StartIndex.HasValue) + { + entities = entities.Skip(query.StartIndex.Value); } - if (!string.IsNullOrEmpty(query.ChannelId)) + if (query.Limit.HasValue) { - list = list.Where(i => string.Equals(i.ChannelId, query.ChannelId)) - .ToList(); + entities = entities.Take(query.Limit.Value); } - var returnArray = list.OrderByDescending(i => i.StartDate) + var returnArray = entities + .Select(i => + { + var channel = string.IsNullOrEmpty(i.RecordingInfo.ChannelId) ? null : GetInternalChannel(_tvDtoService.GetInternalChannelId(service.Name, i.RecordingInfo.ChannelId)); + return _tvDtoService.GetRecordingInfoDto(i, channel, service, user); + }) .ToArray(); return new QueryResult<RecordingInfoDto> @@ -438,17 +520,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } + private Task<LiveTvRecording[]> GetEntities(IEnumerable<RecordingInfo> recordings, string serviceName, CancellationToken cancellationToken) + { + var tasks = recordings.Select(i => GetRecording(i, serviceName, cancellationToken)); + + return Task.WhenAll(tasks); + } + private IEnumerable<ILiveTvService> GetServices(string serviceName, string channelId) { IEnumerable<ILiveTvService> services = _services; if (string.IsNullOrEmpty(serviceName) && !string.IsNullOrEmpty(channelId)) { - var channelIdGuid = new Guid(channelId); + var channel = GetInternalChannel(channelId); - serviceName = _channels.Where(i => i.Id == channelIdGuid) - .Select(i => i.ServiceName) - .FirstOrDefault(); + if (channel != null) + { + serviceName = channel.ServiceName; + } } if (!string.IsNullOrEmpty(serviceName)) @@ -459,137 +549,340 @@ namespace MediaBrowser.Server.Implementations.LiveTv return services; } - public Task ScheduleRecording(string programId) + public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var service = ActiveService; + var timers = await service.GetTimersAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(query.ChannelId)) + { + var guid = new Guid(query.ChannelId); + timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId)); + } + + var returnArray = timers + .Select(i => + { + var program = string.IsNullOrEmpty(i.ProgramId) ? null : GetInternalProgram(_tvDtoService.GetInternalProgramId(service.Name, i.ProgramId).ToString("N")); + var channel = string.IsNullOrEmpty(i.ChannelId) ? null : GetInternalChannel(_tvDtoService.GetInternalChannelId(service.Name, i.ChannelId)); + + return _tvDtoService.GetTimerInfoDto(i, service, program, channel); + }) + .OrderBy(i => i.StartDate) + .ToArray(); + + return new QueryResult<TimerInfoDto> + { + Items = returnArray, + TotalRecordCount = returnArray.Length + }; } - public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) + public async Task DeleteRecording(string recordingId) { - var list = new List<TimerInfoDto>(); + var recording = await GetRecording(recordingId, CancellationToken.None).ConfigureAwait(false); - if (ActiveService != null) + if (recording == null) { - var timers = await GetTimers(ActiveService, cancellationToken).ConfigureAwait(false); + throw new ResourceNotFoundException(string.Format("Recording with Id {0} not found", recordingId)); + } + + var service = GetServices(recording.ServiceName, null) + .First(); + + await service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None).ConfigureAwait(false); + } + + public async Task CancelTimer(string id) + { + var timer = await GetTimer(id, CancellationToken.None).ConfigureAwait(false); - list.AddRange(timers); + if (timer == null) + { + throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); } - if (!string.IsNullOrEmpty(query.ChannelId)) + var service = GetServices(timer.ServiceName, null) + .First(); + + await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); + } + + public async Task CancelSeriesTimer(string id) + { + var timer = await GetSeriesTimer(id, CancellationToken.None).ConfigureAwait(false); + + if (timer == null) { - list = list.Where(i => string.Equals(i.ChannelId, query.ChannelId)) - .ToList(); + throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); } - var returnArray = list.OrderByDescending(i => i.StartDate) + var service = GetServices(timer.ServiceName, null) + .First(); + + await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); + } + + public async Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken, User user = null) + { + var results = await GetRecordings(new RecordingQuery + { + UserId = user == null ? null : user.Id.ToString("N"), + Id = id + + }, cancellationToken).ConfigureAwait(false); + + return results.Items.FirstOrDefault(); + } + + public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) + { + var results = await GetTimers(new TimerQuery(), cancellationToken).ConfigureAwait(false); + + return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); + } + + public async Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken) + { + var results = await GetSeriesTimers(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); + + return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); + } + + public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) + { + var service = ActiveService; + + var timers = await service.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); + + var returnArray = timers + .Select(i => + { + string channelName = null; + + if (!string.IsNullOrEmpty(i.ChannelId)) + { + var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId); + var channel = GetInternalChannel(internalChannelId); + channelName = channel == null ? null : channel.ChannelInfo.Name; + } + + return _tvDtoService.GetSeriesTimerInfoDto(i, service, channelName); + + }) + .OrderByDescending(i => i.StartDate) .ToArray(); - return new QueryResult<TimerInfoDto> + return new QueryResult<SeriesTimerInfoDto> { Items = returnArray, TotalRecordCount = returnArray.Length }; } - private async Task<IEnumerable<TimerInfoDto>> GetTimers(ILiveTvService service, CancellationToken cancellationToken) + public Task<ChannelInfoDto> GetChannel(string id, CancellationToken cancellationToken, User user = null) { - var timers = await service.GetTimersAsync(cancellationToken).ConfigureAwait(false); + var channel = GetInternalChannel(id); + + var dto = _tvDtoService.GetChannelInfoDto(channel, user); - return timers.Select(i => GetTimerInfoDto(i, service)); + return Task.FromResult(dto); } - private TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service) + public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken) { - var id = service.Name + info.ChannelId + info.Id; - id = id.GetMD5().ToString("N"); + var service = ActiveService; + + var info = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); + + var obj = _tvDtoService.GetSeriesTimerInfoDto(info, service, null); + + obj.Id = obj.ExternalId = string.Empty; + + return obj; + } - var dto = new TimerInfoDto + public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) + { + var info = await GetNewTimerDefaults(cancellationToken).ConfigureAwait(false); + + var program = await GetProgram(programId, cancellationToken).ConfigureAwait(false); + + info.Days = new List<DayOfWeek> { - ChannelName = info.ChannelName, - Description = info.Description, - EndDate = info.EndDate, - Name = info.Name, - StartDate = info.StartDate, - Id = id, - ExternalId = info.Id, - ChannelId = GetInternalChannelId(service.Name, info.ChannelId, info.ChannelName).ToString("N"), - Status = info.Status, - SeriesTimerId = info.SeriesTimerId, - RequestedPostPaddingSeconds = info.RequestedPostPaddingSeconds, - RequestedPrePaddingSeconds = info.RequestedPrePaddingSeconds, - RequiredPostPaddingSeconds = info.RequiredPostPaddingSeconds, - RequiredPrePaddingSeconds = info.RequiredPrePaddingSeconds + program.StartDate.ToLocalTime().DayOfWeek }; - var duration = info.EndDate - info.StartDate; - dto.DurationMs = Convert.ToInt32(duration.TotalMilliseconds); + info.DayPattern = _tvDtoService.GetDayPattern(info.Days); - if (!string.IsNullOrEmpty(info.ProgramId)) - { - dto.ProgramId = GetInternalProgramIdId(service.Name, info.ProgramId).ToString("N"); - } + info.Name = program.Name; + info.ChannelId = program.ChannelId; + info.ChannelName = program.ChannelName; + info.EndDate = program.EndDate; + info.StartDate = program.StartDate; + info.Name = program.Name; + info.Overview = program.Overview; + info.ProgramId = program.Id; + info.ExternalProgramId = program.ExternalId; - return dto; + return info; } - public async Task DeleteRecording(string recordingId) + public async Task CreateTimer(TimerInfoDto timer, CancellationToken cancellationToken) { - var recordings = await GetRecordings(new RecordingQuery - { + var service = string.IsNullOrEmpty(timer.ServiceName) ? ActiveService : GetServices(timer.ServiceName, null).First(); - }, CancellationToken.None).ConfigureAwait(false); + var info = await _tvDtoService.GetTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - var recording = recordings.Items - .FirstOrDefault(i => string.Equals(recordingId, i.Id, StringComparison.OrdinalIgnoreCase)); + // Set priority from default values + var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); + info.Priority = defaultValues.Priority; - if (recording == null) - { - throw new ResourceNotFoundException(string.Format("Recording with Id {0} not found", recordingId)); - } + await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); + } - var channel = GetChannel(recording.ChannelId); + public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) + { + var service = string.IsNullOrEmpty(timer.ServiceName) ? ActiveService : GetServices(timer.ServiceName, null).First(); - var service = GetServices(channel.ServiceName, null) - .First(); + var info = await _tvDtoService.GetSeriesTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - await service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None).ConfigureAwait(false); + // Set priority from default values + var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); + info.Priority = defaultValues.Priority; + + await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); } - public async Task CancelTimer(string id) + public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) + { + var info = await _tvDtoService.GetTimerInfo(timer, false, this, cancellationToken).ConfigureAwait(false); + + var service = string.IsNullOrEmpty(timer.ServiceName) ? ActiveService : GetServices(timer.ServiceName, null).First(); + + await service.UpdateTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) + { + var info = await _tvDtoService.GetSeriesTimerInfo(timer, false, this, cancellationToken).ConfigureAwait(false); + + var service = string.IsNullOrEmpty(timer.ServiceName) ? ActiveService : GetServices(timer.ServiceName, null).First(); + + await service.UpdateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + + private List<string> GetRecordingGroupNames(RecordingInfo recording) { - var timers = await GetTimers(new TimerQuery + var list = new List<string>(); + + if (recording.IsSeries) { + list.Add(recording.Name); + } - }, CancellationToken.None).ConfigureAwait(false); + if (recording.IsKids) + { + list.Add("Kids"); + } - var timer = timers.Items - .FirstOrDefault(i => string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)); + if (recording.IsMovie) + { + list.Add("Movies"); + } - if (timer == null) + if (recording.IsNews) { - throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); + list.Add("News"); } - var channel = GetChannel(timer.ChannelId); + if (recording.IsPremiere) + { + list.Add("Sports"); + } - var service = GetServices(channel.ServiceName, null) - .First(); + if (!recording.IsSports && !recording.IsNews && !recording.IsMovie && !recording.IsKids && !recording.IsSeries) + { + list.Add("Others"); + } - await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); + return list; } - public async Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken) + private List<Guid> GetRecordingGroupIds(RecordingInfo recording) { - var results = await GetRecordings(new RecordingQuery(), cancellationToken).ConfigureAwait(false); - - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); + return GetRecordingGroupNames(recording).Select(i => i.ToLower() + .GetMD5()) + .ToList(); } - public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) + public async Task<QueryResult<RecordingGroupDto>> GetRecordingGroups(RecordingGroupQuery query, CancellationToken cancellationToken) { - var results = await GetTimers(new TimerQuery(), cancellationToken).ConfigureAwait(false); + var recordingResult = await GetRecordings(new RecordingQuery + { + UserId = query.UserId - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); + }, cancellationToken).ConfigureAwait(false); + + var recordings = recordingResult.Items; + + var groups = new List<RecordingGroupDto>(); + + var series = recordings + .Where(i => i.IsSeries) + .ToLookup(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + groups.AddRange(series.OrderBy(i => i.Key).Select(i => new RecordingGroupDto + { + Name = i.Key, + RecordingCount = i.Count() + })); + + groups.Add(new RecordingGroupDto + { + Name = "Kids", + RecordingCount = recordings.Count(i => i.IsKids) + }); + + groups.Add(new RecordingGroupDto + { + Name = "Movies", + RecordingCount = recordings.Count(i => i.IsMovie) + }); + + groups.Add(new RecordingGroupDto + { + Name = "News", + RecordingCount = recordings.Count(i => i.IsNews) + }); + + groups.Add(new RecordingGroupDto + { + Name = "Sports", + RecordingCount = recordings.Count(i => i.IsSports) + }); + + groups.Add(new RecordingGroupDto + { + Name = "Others", + RecordingCount = recordings.Count(i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries) + }); + + groups = groups + .Where(i => i.RecordingCount > 0) + .ToList(); + + foreach (var group in groups) + { + group.Id = group.Name.ToLower().GetMD5().ToString("N"); + } + + return new QueryResult<RecordingGroupDto> + { + Items = groups.ToArray(), + TotalRecordCount = groups.Count + }; } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs new file mode 100644 index 000000000..7c343f77c --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs @@ -0,0 +1,156 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.LiveTv +{ + public class ProgramImageProvider : BaseMetadataProvider + { + private readonly ILiveTvManager _liveTvManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; + + public ProgramImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient) + : base(logManager, configurationManager) + { + _liveTvManager = liveTvManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + _httpClient = httpClient; + } + + public override bool Supports(BaseItem item) + { + return item is LiveTvProgram; + } + + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + return !item.HasImage(ImageType.Primary); + } + + public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) + { + if (item.HasImage(ImageType.Primary)) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + return true; + } + + var changed = true; + + try + { + changed = await DownloadImage((LiveTvProgram)item, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + // Don't fail the provider on a 404 + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + if (changed) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + } + + return changed; + } + + private async Task<bool> DownloadImage(LiveTvProgram item, CancellationToken cancellationToken) + { + var programInfo = item.ProgramInfo; + + Stream imageStream = null; + string contentType = null; + + if (!string.IsNullOrEmpty(programInfo.ImagePath)) + { + contentType = "image/" + Path.GetExtension(programInfo.ImagePath).ToLower(); + imageStream = _fileSystem.GetFileStream(programInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); + } + else if (!string.IsNullOrEmpty(programInfo.ImageUrl)) + { + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = programInfo.ImageUrl + }; + + var response = await _httpClient.GetResponse(options).ConfigureAwait(false); + + if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + Logger.Error("Provider did not return an image content type."); + return false; + } + + imageStream = response.Content; + contentType = response.ContentType; + } + else if (programInfo.HasImage ?? true) + { + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase)); + + if (service != null) + { + try + { + var response = await service.GetProgramImageAsync(programInfo.Id, programInfo.ChannelId, cancellationToken).ConfigureAwait(false); + + if (response != null) + { + imageStream = response.Stream; + contentType = response.MimeType; + } + } + catch (NotImplementedException) + { + return false; + } + } + } + + if (imageStream != null) + { + // Dummy up the original url + var url = item.ServiceName + programInfo.Id; + + await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); + return true; + } + + return false; + } + + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Second; } + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate; + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs new file mode 100644 index 000000000..0b5ec285e --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs @@ -0,0 +1,156 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; + +namespace MediaBrowser.Server.Implementations.LiveTv +{ + public class RecordingImageProvider : BaseMetadataProvider + { + private readonly ILiveTvManager _liveTvManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; + + public RecordingImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient) + : base(logManager, configurationManager) + { + _liveTvManager = liveTvManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + _httpClient = httpClient; + } + + public override bool Supports(BaseItem item) + { + return item is LiveTvRecording; + } + + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + return !item.HasImage(ImageType.Primary); + } + + public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) + { + if (item.HasImage(ImageType.Primary)) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + return true; + } + + var changed = true; + + try + { + changed = await DownloadImage((LiveTvRecording)item, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + // Don't fail the provider on a 404 + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + if (changed) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + } + + return changed; + } + + private async Task<bool> DownloadImage(LiveTvRecording item, CancellationToken cancellationToken) + { + var recordingInfo = item.RecordingInfo; + + Stream imageStream = null; + string contentType = null; + + if (!string.IsNullOrEmpty(recordingInfo.ImagePath)) + { + contentType = "image/" + Path.GetExtension(recordingInfo.ImagePath).ToLower(); + imageStream = _fileSystem.GetFileStream(recordingInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); + } + else if (!string.IsNullOrEmpty(recordingInfo.ImageUrl)) + { + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = recordingInfo.ImageUrl + }; + + var response = await _httpClient.GetResponse(options).ConfigureAwait(false); + + if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + Logger.Error("Provider did not return an image content type."); + return false; + } + + imageStream = response.Content; + contentType = response.ContentType; + } + else if (recordingInfo.HasImage ?? true) + { + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase)); + + if (service != null) + { + try + { + var response = await service.GetRecordingImageAsync(recordingInfo.Id, cancellationToken).ConfigureAwait(false); + + if (response != null) + { + imageStream = response.Stream; + contentType = response.MimeType; + } + } + catch (NotImplementedException) + { + return false; + } + } + } + + if (imageStream != null) + { + // Dummy up the original url + var url = item.ServiceName + recordingInfo.Id; + + await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); + return true; + } + + return false; + } + + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Second; } + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate; + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Localization/Ratings/au.txt b/MediaBrowser.Server.Implementations/Localization/Ratings/au.txt index a68a3f5f3..fa60f5305 100644 --- a/MediaBrowser.Server.Implementations/Localization/Ratings/au.txt +++ b/MediaBrowser.Server.Implementations/Localization/Ratings/au.txt @@ -1,6 +1,8 @@ AU-G,1 AU-PG,5 AU-M,6 +AU-MA15+,7 AU-M15+,8 AU-R18+,9 -AU-X18+,10
\ No newline at end of file +AU-X18+,10 +AU-RC,11 diff --git a/MediaBrowser.Server.Implementations/Localization/Ratings/be.txt b/MediaBrowser.Server.Implementations/Localization/Ratings/be.txt new file mode 100644 index 000000000..99a53f664 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Ratings/be.txt @@ -0,0 +1,6 @@ +BE-AL,1 +BE-MG6,2 +BE-6,3 +BE-9,5 +BE-12,6 +BE-16,8
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt b/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt index 571b6eba9..e1c3639cb 100644 --- a/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt +++ b/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt @@ -2,4 +2,5 @@ DE-FSK0,1 DE-FSK6+,5 DE-FSK12+,7 DE-FSK16+,8 +DE-16,8 DE-FSK18+,9 diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 39966f0d7..86dd0bc75 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>MediaBrowser.Server.Implementations</RootNamespace> <AssemblyName>MediaBrowser.Server.Implementations</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>10.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -24,6 +24,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -32,33 +33,33 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release Mono\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <ItemGroup> <Reference Include="Alchemy"> <HintPath>..\packages\Alchemy.2.2.1\lib\net40\Alchemy.dll</HintPath> </Reference> - <Reference Include="ServiceStack, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.dll</HintPath> - </Reference> - <Reference Include="ServiceStack.Client, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Client.dll</HintPath> - </Reference> - <Reference Include="ServiceStack.Common, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Common.dll</HintPath> + <Reference Include="ServiceStack.Api.Swagger"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll</HintPath> </Reference> - <Reference Include="ServiceStack.Interfaces, Version=4.0.0.0, Culture=neutral, PublicKeyToken=e06fbc6124f57c43, processorArchitecture=MSIL"> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Data.SQLite, Version=1.0.90.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=x86"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> + <HintPath>..\packages\System.Data.SQLite.x86.1.0.90.0\lib\net45\System.Data.SQLite.dll</HintPath> </Reference> - <Reference Include="ServiceStack.Text, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Text.dll</HintPath> + <Reference Include="System.Data.SQLite.Linq"> + <HintPath>..\packages\System.Data.SQLite.x86.1.0.90.0\lib\net45\System.Data.SQLite.Linq.dll</HintPath> </Reference> - <Reference Include="System" /> - <Reference Include="System.Core" /> <Reference Include="System.Drawing" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> @@ -70,9 +71,26 @@ <Reference Include="BDInfo"> <HintPath>..\packages\MediaBrowser.BdInfo.1.0.0.5\lib\net20\BDInfo.dll</HintPath> </Reference> - <Reference Include="System.Data.SQLite"> - <HintPath>..\packages\System.Data.SQLite.x86.1.0.89.0\lib\net45\System.Data.SQLite.dll</HintPath> + <Reference Include="System.Data.SQLite" Condition=" '$(ConfigurationName)' == 'Release Mono' "> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\ThirdParty\System.Data.SQLite.ManagedOnly\x86\1.0.90.0\net40\System.Data.SQLite.dll</HintPath> + </Reference> + <Reference Include="ServiceStack"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Client"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Client.dll</HintPath> </Reference> + <Reference Include="ServiceStack.Common"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Common.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Interfaces"> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Text"> + <HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath> + </Reference> + <Reference Include="Mono.Posix" Condition=" '$(ConfigurationName)' == 'Release Mono' " /> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -139,7 +157,10 @@ <Compile Include="Library\Validators\StudiosValidator.cs" /> <Compile Include="Library\Validators\YearsPostScanTask.cs" /> <Compile Include="LiveTv\ChannelImageProvider.cs" /> + <Compile Include="LiveTv\LiveTvDtoService.cs" /> <Compile Include="LiveTv\LiveTvManager.cs" /> + <Compile Include="LiveTv\ProgramImageProvider.cs" /> + <Compile Include="LiveTv\RecordingImageProvider.cs" /> <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> <Compile Include="Localization\LocalizationManager.cs" /> <Compile Include="MediaEncoder\MediaEncoder.cs" /> @@ -153,12 +174,14 @@ <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Providers\ImageSaver.cs" /> <Compile Include="Providers\ProviderManager.cs" /> + <Compile Include="Roku\RokuControllerFactory.cs" /> <Compile Include="ScheduledTasks\PeopleValidationTask.cs" /> <Compile Include="ScheduledTasks\ChapterImagesTask.cs" /> <Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" /> <Compile Include="ServerApplicationPaths.cs" /> <Compile Include="ServerManager\ServerManager.cs" /> <Compile Include="ServerManager\WebSocketConnection.cs" /> + <Compile Include="Roku\RokuSessionController.cs" /> <Compile Include="Session\SessionManager.cs"> <SubType>Code</SubType> </Compile> @@ -169,6 +192,7 @@ <Compile Include="Sorting\AlbumArtistComparer.cs" /> <Compile Include="Sorting\AlbumComparer.cs" /> <Compile Include="Sorting\AlbumCountComparer.cs" /> + <Compile Include="Sorting\AlphanumComparator.cs" /> <Compile Include="Sorting\ArtistComparer.cs" /> <Compile Include="Sorting\BudgetComparer.cs" /> <Compile Include="Sorting\CommunityRatingComparer.cs" /> @@ -244,8 +268,101 @@ <ItemGroup> <EmbeddedResource Include="Localization\Ratings\ca.txt" /> </ItemGroup> + <ItemGroup> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\css\highlight.default.css"> + <Link>swagger-ui\css\highlight.default.css</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\css\screen.css"> + <Link>swagger-ui\css\screen.css</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\images\logo_small.png"> + <Link>swagger-ui\images\logo_small.png</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\images\pet_store_api.png"> + <Link>swagger-ui\images\pet_store_api.png</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\images\throbber.gif"> + <Link>swagger-ui\images\throbber.gif</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\images\wordnik_api.png"> + <Link>swagger-ui\images\wordnik_api.png</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\index.html"> + <Link>swagger-ui\index.html</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\backbone-min.js"> + <Link>swagger-ui\lib\backbone-min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\handlebars-1.0.0.js"> + <Link>swagger-ui\lib\handlebars-1.0.0.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\highlight.7.3.pack.js"> + <Link>swagger-ui\lib\highlight.7.3.pack.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery-1.8.0.min.js"> + <Link>swagger-ui\lib\jquery-1.8.0.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.ba-bbq.min.js"> + <Link>swagger-ui\lib\jquery.ba-bbq.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.slideto.min.js"> + <Link>swagger-ui\lib\jquery.slideto.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.wiggle.min.js"> + <Link>swagger-ui\lib\jquery.wiggle.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\shred.bundle.js"> + <Link>swagger-ui\lib\shred.bundle.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\shred\content.js"> + <Link>swagger-ui\lib\shred\content.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\swagger.js"> + <Link>swagger-ui\lib\swagger.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\underscore-min.js"> + <Link>swagger-ui\lib\underscore-min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\swagger-ui.js"> + <Link>swagger-ui\swagger-ui.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\swagger-ui.min.js"> + <Link>swagger-ui\swagger-ui.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <EmbeddedResource Include="Localization\Ratings\be.txt" /> + </ItemGroup> + <ItemGroup> + <Content Include="..\ThirdParty\System.Data.SQLite.ManagedOnly\x86\1.0.90.0\net40\System.Data.SQLite.dll" Condition=" '$(ConfigurationName)' == 'Release Mono'"> + <Link>System.Data.SQLite.dll</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\System.Data.SQLite.ManagedOnly\x86\1.0.90.0\net40\System.Data.SQLite.Linq.dll" Condition=" '$(ConfigurationName)' == 'Release Mono'"> + <Link>System.Data.SQLite.Linq.dll</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" /> + <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs index 2224c657f..187dc19bf 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs @@ -427,8 +427,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder throw new ArgumentNullException("outputPath"); } - var slowSeekParam = GetSlowSeekCommandLineParameter(offset); - var fastSeekParam = GetFastSeekCommandLineParameter(offset); + + var slowSeekParam = offset.TotalSeconds > 0 ? " -ss " + offset.TotalSeconds.ToString(UsCulture) : string.Empty; var encodingParam = string.IsNullOrEmpty(language) ? string.Empty : GetSubtitleLanguageEncodingParam(language) + " "; @@ -444,12 +444,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder UseShellExecute = false, FileName = FFMpegPath, Arguments = - string.Format("{0}{1}-i \"{2}\"{3} \"{4}\"", - fastSeekParam, - encodingParam, - inputPath, - slowSeekParam, - outputPath), + string.Format("{0} -i \"{1}\" {2} -c:s ass \"{3}\"", encodingParam, inputPath, slowSeekParam, outputPath), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -459,6 +454,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-convert-" + Guid.NewGuid() + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); @@ -665,7 +661,9 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder throw new ArgumentNullException("outputPath"); } - var slowSeekParam = offset.TotalSeconds > 0 ? " -ss " + offset.TotalSeconds.ToString(UsCulture) : string.Empty; + + var slowSeekParam = GetSlowSeekCommandLineParameter(offset); + var fastSeekParam = GetFastSeekCommandLineParameter(offset); var process = new Process { @@ -678,7 +676,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder RedirectStandardError = true, FileName = FFMpegPath, - Arguments = string.Format("-i {0}{1} -map 0:{2} -an -vn -c:s ass \"{3}\"", inputPath, slowSeekParam, subtitleStreamIndex, outputPath), + Arguments = string.Format(" {0} -i {1} {2} -map 0:{3} -an -vn -c:s ass \"{4}\"", fastSeekParam, inputPath, slowSeekParam, subtitleStreamIndex, outputPath), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false } @@ -687,6 +685,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs index 075ef4239..77b993205 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs @@ -32,6 +32,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _logger = logManager.GetLogger(GetType().Name); } + private SqliteShrinkMemoryTimer _shrinkMemoryTimer; + /// <summary> /// Opens the connection to the database /// </summary> @@ -52,6 +54,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.RunQueries(queries, _logger); PrepareStatements(); + + _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger); } /// <summary> @@ -282,6 +286,12 @@ namespace MediaBrowser.Server.Implementations.Persistence { lock (_disposeLock) { + if (_shrinkMemoryTimer != null) + { + _shrinkMemoryTimer.Dispose(); + _shrinkMemoryTimer = null; + } + if (_connection != null) { if (_connection.IsOpen()) diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs index 9836de735..0b3d5f784 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs @@ -1,11 +1,7 @@ using MediaBrowser.Model.Logging; using System; using System.Data; -#if __MonoCS__ -using Mono.Data.Sqlite; -#else using System.Data.SQLite; -#endif using System.IO; using System.Threading.Tasks; @@ -140,18 +136,6 @@ namespace MediaBrowser.Server.Implementations.Persistence logger.Info("Opening {0}", dbPath); - #if __MonoCS__ - var connectionstr = new SqliteConnectionStringBuilder - { - PageSize = 4096, - CacheSize = 4096, - SyncMode = SynchronizationModes.Normal, - DataSource = dbPath, - JournalMode = SQLiteJournalModeEnum.Off - }; - - var connection = new SqliteConnection(connectionstr.ConnectionString); -#else var connectionstr = new SQLiteConnectionStringBuilder { PageSize = 4096, @@ -162,7 +146,7 @@ namespace MediaBrowser.Server.Implementations.Persistence }; var connection = new SQLiteConnection(connectionstr.ConnectionString); -#endif + await connection.OpenAsync().ConfigureAwait(false); return connection; diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs index 5d836b090..9971c7460 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs @@ -25,6 +25,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _logger = logManager.GetLogger(GetType().Name); } + private SqliteShrinkMemoryTimer _shrinkMemoryTimer; + /// <summary> /// Opens the connection to the database /// </summary> @@ -50,6 +52,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.RunQueries(queries, _logger); PrepareStatements(); + + _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger); } private static readonly string[] SaveColumns = @@ -240,6 +244,12 @@ namespace MediaBrowser.Server.Implementations.Persistence { lock (_disposeLock) { + if (_shrinkMemoryTimer != null) + { + _shrinkMemoryTimer.Dispose(); + _shrinkMemoryTimer = null; + } + if (_connection != null) { if (_connection.IsOpen()) diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs index d3f9100b1..4e388b4b0 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs @@ -55,6 +55,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _logger = logManager.GetLogger(GetType().Name); } + private SqliteShrinkMemoryTimer _shrinkMemoryTimer; + /// <summary> /// Opens the connection to the database /// </summary> @@ -78,6 +80,8 @@ namespace MediaBrowser.Server.Implementations.Persistence }; _connection.RunQueries(queries, _logger); + + _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger); } /// <summary> @@ -267,6 +271,12 @@ namespace MediaBrowser.Server.Implementations.Persistence { lock (_disposeLock) { + if (_shrinkMemoryTimer != null) + { + _shrinkMemoryTimer.Dispose(); + _shrinkMemoryTimer = null; + } + if (_connection != null) { if (_connection.IsOpen()) diff --git a/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs b/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs index 8d88b66a0..0346aba97 100644 --- a/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs +++ b/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs @@ -118,6 +118,8 @@ namespace MediaBrowser.Server.Implementations.Providers imageIndex = hasScreenshots.ScreenshotImagePaths.Count; } + var index = imageIndex ?? 0; + var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally); // If there are more than one output paths, the stream will need to be seekable @@ -132,7 +134,7 @@ namespace MediaBrowser.Server.Implementations.Providers source = memoryStream; } - var currentPath = GetCurrentImagePath(item, type, imageIndex); + var currentPath = GetCurrentImagePath(item, type, index); using (source) { @@ -152,7 +154,7 @@ namespace MediaBrowser.Server.Implementations.Providers } } - // Set the path into the BaseItem + // Set the path into the item SetImagePath(item, type, imageIndex, paths[0], sourceUrl); // Delete the current path @@ -257,27 +259,9 @@ namespace MediaBrowser.Server.Implementations.Providers /// or /// imageIndex /// </exception> - private string GetCurrentImagePath(BaseItem item, ImageType type, int? imageIndex) + private string GetCurrentImagePath(IHasImages item, ImageType type, int imageIndex) { - switch (type) - { - case ImageType.Screenshot: - - var hasScreenshots = (IHasScreenshots)item; - if (!imageIndex.HasValue) - { - throw new ArgumentNullException("imageIndex"); - } - return hasScreenshots.ScreenshotImagePaths.Count > imageIndex.Value ? hasScreenshots.ScreenshotImagePaths[imageIndex.Value] : null; - case ImageType.Backdrop: - if (!imageIndex.HasValue) - { - throw new ArgumentNullException("imageIndex"); - } - return item.BackdropImagePaths.Count > imageIndex.Value ? item.BackdropImagePaths[imageIndex.Value] : null; - default: - return item.GetImage(type); - } + return item.GetImagePath(type, imageIndex); } /// <summary> @@ -336,7 +320,7 @@ namespace MediaBrowser.Server.Implementations.Providers } break; default: - item.SetImage(type, path); + item.SetImagePath(type, path); break; } } @@ -472,6 +456,11 @@ namespace MediaBrowser.Server.Implementations.Providers if (imageIndex.Value == 0) { + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart", extension) }; + } + if (season != null && item.IndexNumber.HasValue) { var seriesFolder = season.SeriesPath; @@ -493,6 +482,11 @@ namespace MediaBrowser.Server.Implementations.Providers var outputIndex = imageIndex.Value; + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) }; + } + var extraFanartFilename = GetBackdropSaveFilename(item.BackdropImagePaths, "fanart", "fanart", outputIndex); return new[] @@ -526,7 +520,7 @@ namespace MediaBrowser.Server.Implementations.Providers return new[] { Path.Combine(seasonFolder, imageFilename) }; } - if (item.IsInMixedFolder) + if (item.IsInMixedFolder || item is MusicVideo) { return new[] { GetSavePathForItemInMixedFolder(item, type, string.Empty, extension) }; } @@ -569,6 +563,13 @@ namespace MediaBrowser.Server.Implementations.Providers return new[] { Path.Combine(seriesFolder, imageFilename) }; } + + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "landscape", extension) }; + } + + return new[] { Path.Combine(item.MetaLocation, "landscape" + extension) }; } // All other paths are the same @@ -583,7 +584,7 @@ namespace MediaBrowser.Server.Implementations.Providers /// <param name="imageFilename">The image filename.</param> /// <param name="extension">The extension.</param> /// <returns>System.String.</returns> - private string GetSavePathForItemInMixedFolder(BaseItem item, ImageType type, string imageFilename, string extension) + private string GetSavePathForItemInMixedFolder(IHasImages item, ImageType type, string imageFilename, string extension) { if (type == ImageType.Primary) { diff --git a/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs b/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs index 511092759..cbfd7d74d 100644 --- a/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs +++ b/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs @@ -2,6 +2,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -104,9 +105,8 @@ namespace MediaBrowser.Server.Implementations.Providers cancellationToken.ThrowIfCancellationRequested(); var enableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders; - var excludeTypes = ConfigurationManager.Configuration.InternetProviderExcludeTypes; - var providerHistories = item.DateLastSaved == DateTime.MinValue ? + var providerHistories = item.DateLastSaved == default(DateTime) ? new List<BaseProviderInfo>() : _itemRepo.GetProviderHistory(item.Id).ToList(); @@ -132,15 +132,6 @@ namespace MediaBrowser.Server.Implementations.Providers continue; } - // Skip if internet provider and this type is not allowed - if (provider.RequiresInternet && - enableInternetProviders && - excludeTypes.Length > 0 && - excludeTypes.Contains(item.GetType().Name, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - // Put this check below the await because the needs refresh of the next tier of providers may depend on the previous ones running // This is the case for the fan art provider which depends on the movie and tv providers having run before them if (provider.RequiresInternet && item.DontFetchMeta && provider.EnforceDontFetchMetadata) @@ -387,35 +378,45 @@ namespace MediaBrowser.Server.Implementations.Providers providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); } - var preferredLanguage = ConfigurationManager.Configuration.PreferredMetadataLanguage; + var preferredLanguage = item.GetPreferredMetadataLanguage(); + + var tasks = providers.Select(i => GetImages(item, cancellationToken, i, preferredLanguage, type)); - var tasks = providers.Select(i => Task.Run(async () => + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + return results.SelectMany(i => i); + } + + /// <summary> + /// Gets the images. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="i">The i.</param> + /// <param name="preferredLanguage">The preferred language.</param> + /// <param name="type">The type.</param> + /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> + private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IImageProvider i, string preferredLanguage, ImageType? type = null) + { + try { - try + if (type.HasValue) { - if (type.HasValue) - { - var result = await i.GetImages(item, type.Value, cancellationToken).ConfigureAwait(false); + var result = await i.GetImages(item, type.Value, cancellationToken).ConfigureAwait(false); - return FilterImages(result, preferredLanguage); - } - else - { - var result = await i.GetAllImages(item, cancellationToken).ConfigureAwait(false); - return FilterImages(result, preferredLanguage); - } + return FilterImages(result, preferredLanguage); } - catch (Exception ex) + else { - _logger.ErrorException("{0} failed in GetImages for type {1}", ex, i.GetType().Name, item.GetType().Name); - return new List<RemoteImageInfo>(); + var result = await i.GetAllImages(item, cancellationToken).ConfigureAwait(false); + return FilterImages(result, preferredLanguage); } - - }, cancellationToken)); - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - - return results.SelectMany(i => i); + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed in GetImages for type {1}", ex, i.GetType().Name, item.GetType().Name); + return new List<RemoteImageInfo>(); + } } private IEnumerable<RemoteImageInfo> FilterImages(IEnumerable<RemoteImageInfo> images, string preferredLanguage) diff --git a/MediaBrowser.Server.Implementations/Roku/RokuControllerFactory.cs b/MediaBrowser.Server.Implementations/Roku/RokuControllerFactory.cs new file mode 100644 index 000000000..71f70421a --- /dev/null +++ b/MediaBrowser.Server.Implementations/Roku/RokuControllerFactory.cs @@ -0,0 +1,32 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Serialization; +using System; + +namespace MediaBrowser.Server.Implementations.Roku +{ + public class RokuControllerFactory : ISessionControllerFactory + { + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _json; + private readonly IServerApplicationHost _appHost; + + public RokuControllerFactory(IHttpClient httpClient, IJsonSerializer json, IServerApplicationHost appHost) + { + _httpClient = httpClient; + _json = json; + _appHost = appHost; + } + + public ISessionController GetSessionController(SessionInfo session) + { + if (string.Equals(session.Client, "roku", StringComparison.OrdinalIgnoreCase)) + { + return new RokuSessionController(_httpClient, _json, _appHost, session); + } + + return null; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs b/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs new file mode 100644 index 000000000..b9e8b7950 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs @@ -0,0 +1,149 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.System; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Roku +{ + public class RokuSessionController : ISessionController + { + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _json; + private readonly IServerApplicationHost _appHost; + + public SessionInfo Session { get; private set; } + + public RokuSessionController(IHttpClient httpClient, IJsonSerializer json, IServerApplicationHost appHost, SessionInfo session) + { + _httpClient = httpClient; + _json = json; + _appHost = appHost; + Session = session; + } + + public bool SupportsMediaRemoteControl + { + get { return true; } + } + + public bool IsSessionActive + { + get + { + return (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 10; + } + } + + public Task SendSystemCommand(SystemCommand command, CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<string> + { + MessageType = "SystemCommand", + Data = command.ToString() + + }, cancellationToken); + } + + public Task SendMessageCommand(MessageCommand command, CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<MessageCommand> + { + MessageType = "MessageCommand", + Data = command + + }, cancellationToken); + } + + public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<PlayRequest> + { + MessageType = "Play", + Data = command + + }, cancellationToken); + } + + public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<BrowseRequest> + { + MessageType = "Browse", + Data = command + + }, cancellationToken); + } + + public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<PlaystateRequest> + { + MessageType = "Playstate", + Data = command + + }, cancellationToken); + } + + public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) + { + // Roku probably won't care about this + return Task.FromResult(true); + } + + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<SystemInfo> + { + MessageType = "RestartRequired", + Data = _appHost.GetSystemInfo() + + }, cancellationToken); + } + + public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) + { + // Roku probably won't care about this + return Task.FromResult(true); + } + + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<string> + { + MessageType = "ServerShuttingDown", + Data = string.Empty + + }, cancellationToken); + } + + public Task SendServerRestartNotification(CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<string> + { + MessageType = "ServerRestarting", + Data = string.Empty + + }, cancellationToken); + } + + private Task SendCommand(object obj, CancellationToken cancellationToken) + { + var json = _json.SerializeToString(obj); + + return _httpClient.Post(new HttpRequestOptions + { + Url = "http://" + Session.RemoteEndPoint + "/mb/remotecontrol", + CancellationToken = cancellationToken, + RequestContent = json, + RequestContentType = "application/json" + }); + } + } +} diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index 9270b879a..2608ac172 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -1,7 +1,7 @@ using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; @@ -21,10 +21,6 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks class ChapterImagesTask : IScheduledTask { /// <summary> - /// The _kernel - /// </summary> - private readonly Kernel _kernel; - /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; @@ -48,13 +44,11 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// <summary> /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> - /// <param name="kernel">The kernel.</param> /// <param name="logManager">The log manager.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="itemRepo">The item repo.</param> - public ChapterImagesTask(Kernel kernel, ILogManager logManager, ILibraryManager libraryManager, IItemRepository itemRepo) + public ChapterImagesTask(ILogManager logManager, ILibraryManager libraryManager, IItemRepository itemRepo) { - _kernel = kernel; _logger = logManager.GetLogger(GetType().Name); _libraryManager = libraryManager; _itemRepo = itemRepo; @@ -102,13 +96,13 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks // Limit to video files to reduce changes of ffmpeg crash dialog foreach (var item in newItems .Where(i => i.LocationType == LocationType.FileSystem && i.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(i.PrimaryImagePath) && i.DefaultVideoStreamIndex.HasValue) - .Take(2)) + .Take(1)) { try { var chapters = _itemRepo.GetChapters(item.Id).ToList(); - await _kernel.FFMpegManager.PopulateChapterImages(item, chapters, true, true, CancellationToken.None); + await FFMpegManager.Instance.PopulateChapterImages(item, chapters, true, true, CancellationToken.None); } catch (Exception ex) { @@ -123,8 +117,6 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<ITaskTrigger> GetDefaultTriggers() { - // IMPORTANT: Make sure to update the dashboard "wizardsettings" page if this default ever changes - return new ITaskTrigger[] { new DailyTrigger { TimeOfDay = TimeSpan.FromHours(4) } @@ -145,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks var numComplete = 0; - var failHistoryPath = Path.Combine(_kernel.FFMpegManager.VideoImagesDataPath, "failures.txt"); + var failHistoryPath = Path.Combine(FFMpegManager.Instance.ChapterImagesPath, "failures.txt"); List<string> previouslyFailedImages; @@ -174,7 +166,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks var chapters = _itemRepo.GetChapters(video.Id).ToList(); - var success = await _kernel.FFMpegManager.PopulateChapterImages(video, chapters, extract, true, cancellationToken); + var success = await FFMpegManager.Instance.PopulateChapterImages(video, chapters, extract, true, cancellationToken); if (!success) { @@ -203,7 +195,6 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks { get { - // IMPORTANT: Make sure to update the dashboard "wizardsettings" page if this name ever changes return "Chapter image extraction"; } } diff --git a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs index 553aae285..a2dfb51d2 100644 --- a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs +++ b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs @@ -243,7 +243,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <param name="dataFunction">The function that generates the data to send, if there are any connected clients</param> public void SendWebSocketMessage<T>(string messageType, Func<T> dataFunction) { - Task.Run(async () => await SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None).ConfigureAwait(false)); + SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 3a07d33a6..c42f33ec3 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Server.Implementations.Session private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - + /// <summary> /// Gets or sets the configuration manager. /// </summary> @@ -65,6 +65,8 @@ namespace MediaBrowser.Server.Implementations.Session /// </summary> public event EventHandler<PlaybackProgressEventArgs> PlaybackStopped; + private IEnumerable<ISessionControllerFactory> _sessionFactories = new List<ISessionControllerFactory>(); + /// <summary> /// Initializes a new instance of the <see cref="SessionManager" /> class. /// </summary> @@ -83,6 +85,15 @@ namespace MediaBrowser.Server.Implementations.Session } /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="sessionFactories">The session factories.</param> + public void AddParts(IEnumerable<ISessionControllerFactory> sessionFactories) + { + _sessionFactories = sessionFactories.ToList(); + } + + /// <summary> /// Gets all connections. /// </summary> /// <value>All connections.</value> @@ -98,11 +109,12 @@ namespace MediaBrowser.Server.Implementations.Session /// <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> /// <returns>Task.</returns> - /// <exception cref="System.UnauthorizedAccessException"></exception> /// <exception cref="System.ArgumentNullException">user</exception> - public async Task<SessionInfo> LogSessionActivity(string clientType, string appVersion, string deviceId, string deviceName, User user) + /// <exception cref="System.UnauthorizedAccessException"></exception> + public async Task<SessionInfo> LogSessionActivity(string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) { if (string.IsNullOrEmpty(clientType)) { @@ -128,7 +140,7 @@ namespace MediaBrowser.Server.Implementations.Session var activityDate = DateTime.UtcNow; - var session = GetSessionInfo(clientType, appVersion, deviceId, deviceName, user); + var session = GetSessionInfo(clientType, appVersion, deviceId, deviceName, remoteEndPoint, user); session.LastActivityDate = activityDate; @@ -196,9 +208,10 @@ namespace MediaBrowser.Server.Implementations.Session /// <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> /// <returns>SessionInfo.</returns> - private SessionInfo GetSessionInfo(string clientType, string appVersion, string deviceId, string deviceName, User user) + private SessionInfo GetSessionInfo(string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) { var key = clientType + deviceId + appVersion; @@ -212,6 +225,14 @@ namespace MediaBrowser.Server.Implementations.Session connection.DeviceName = deviceName; connection.User = user; + connection.RemoteEndPoint = remoteEndPoint; + + if (connection.SessionController == null) + { + connection.SessionController = _sessionFactories + .Select(i => i.GetSessionController(connection)) + .FirstOrDefault(i => i != null); + } return connection; } @@ -335,7 +356,7 @@ namespace MediaBrowser.Server.Implementations.Session { throw new ArgumentException("PlaybackStopInfo.SessionId cannot be Guid.Empty"); } - + if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0) { throw new ArgumentOutOfRangeException("positionTicks"); @@ -497,7 +518,7 @@ namespace MediaBrowser.Server.Implementations.Session { throw new ArgumentException("Virtual items are not playable."); } - + if (command.PlayCommand != PlayCommand.PlayNow) { if (items.Any(i => !session.QueueableMediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase))) @@ -505,7 +526,7 @@ namespace MediaBrowser.Server.Implementations.Session throw new ArgumentException(string.Format("Session {0} is unable to queue the requested media type.", session.Id)); } } - + return session.SessionController.SendPlayCommand(command, cancellationToken); } @@ -587,7 +608,7 @@ namespace MediaBrowser.Server.Implementations.Session _logger.ErrorException("Error in SendServerShutdownNotification.", ex); } - })); + }, cancellationToken)); return Task.WhenAll(tasks); } @@ -612,7 +633,7 @@ namespace MediaBrowser.Server.Implementations.Session _logger.ErrorException("Error in SendServerRestartNotification.", ex); } - })); + }, cancellationToken)); return Task.WhenAll(tasks); } diff --git a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs index 41cb7eb6b..08481f09f 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs @@ -104,7 +104,7 @@ namespace MediaBrowser.Server.Implementations.Session { _logger.Debug("Logging session activity"); - await _sessionManager.LogSessionActivity(client, version, deviceId, deviceName, null).ConfigureAwait(false); + await _sessionManager.LogSessionActivity(client, version, deviceId, deviceName, message.Connection.RemoteEndPoint, null).ConfigureAwait(false); session = _sessionManager.Sessions .FirstOrDefault(i => string.Equals(i.DeviceId, deviceId) && @@ -114,7 +114,13 @@ namespace MediaBrowser.Server.Implementations.Session if (session != null) { - var controller = new WebSocketController(session, _appHost); + var controller = session.SessionController as WebSocketController; + + if (controller == null) + { + controller = new WebSocketController(session, _appHost); + } + controller.Sockets.Add(message.Connection); session.SessionController = controller; diff --git a/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 76971342a..8707f2e5b 100644 --- a/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.Sorting return CompareEpisodes(x, y); } - if (!isXSpecial && isYSpecial) + if (!isXSpecial) { return CompareEpisodeToSpecial(x, y); } @@ -80,16 +80,36 @@ namespace MediaBrowser.Server.Implementations.Sorting return xSeason.CompareTo(ySeason); } - // Now we know they have the same season + // Special comes after episode + if (y.AirsAfterSeasonNumber.HasValue) + { + return -1; + } + + var yEpisode = y.AirsBeforeEpisodeNumber; + + // Special comes before the season + if (!yEpisode.HasValue) + { + return 1; + } // Compare episode number + var xEpisode = x.IndexNumber; + + if (!xEpisode.HasValue) + { + // Can't really compare if this happens + return 0; + } - // Add 1 to to non-specials to account for AirsBeforeEpisodeNumber - var xEpisode = x.IndexNumber ?? -1; - xEpisode++; - var yEpisode = y.AirsBeforeEpisodeNumber ?? 10000; + // Special comes before episode + if (xEpisode.Value == yEpisode.Value) + { + return 1; + } - return xEpisode.CompareTo(yEpisode); + return xEpisode.Value.CompareTo(yEpisode.Value); } private int CompareSpecials(Episode x, Episode y) diff --git a/MediaBrowser.Server.Implementations/Sorting/AlphanumComparator.cs b/MediaBrowser.Server.Implementations/Sorting/AlphanumComparator.cs new file mode 100644 index 000000000..39a68b3f6 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sorting/AlphanumComparator.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Server.Implementations.Sorting +{ + public 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.Server.Implementations/Sorting/NameComparer.cs b/MediaBrowser.Server.Implementations/Sorting/NameComparer.cs index 49f86c485..83b1b2d16 100644 --- a/MediaBrowser.Server.Implementations/Sorting/NameComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/NameComparer.cs @@ -1,7 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; -using System; namespace MediaBrowser.Server.Implementations.Sorting { @@ -18,7 +17,7 @@ namespace MediaBrowser.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem x, BaseItem y) { - return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase); + return AlphanumComparator.CompareValues(x.Name, y.Name); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 4efc3218b..09612a49c 100644 --- a/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem x, BaseItem y) { - return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); + return AlphanumComparator.CompareValues(GetValue(x), GetValue(y)); } private string GetValue(BaseItem item) diff --git a/MediaBrowser.Server.Implementations/Sorting/SortNameComparer.cs b/MediaBrowser.Server.Implementations/Sorting/SortNameComparer.cs index 873753a2b..e635cfbe5 100644 --- a/MediaBrowser.Server.Implementations/Sorting/SortNameComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,7 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; -using System; namespace MediaBrowser.Server.Implementations.Sorting { @@ -18,7 +17,7 @@ namespace MediaBrowser.Server.Implementations.Sorting /// <returns>System.Int32.</returns> public int Compare(BaseItem x, BaseItem y) { - return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase); + return AlphanumComparator.CompareValues(x.SortName, y.SortName); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs b/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs index 8be35071a..e46dab23e 100644 --- a/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs +++ b/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs @@ -4,6 +4,9 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; using System; using System.Net; +#if __MonoCS__ +using Mono.Unix.Native; +#endif namespace MediaBrowser.Server.Implementations.WebSocket { @@ -66,6 +69,20 @@ namespace MediaBrowser.Server.Implementations.WebSocket TimeOut = TimeSpan.FromHours(24) }; + #if __MonoCS__ + //Linux: port below 1024 require root or cap_net_bind_service + if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) + { + if (Syscall.getuid() == 0) + { + WebSocketServer.FlashAccessPolicyEnabled = true; + } + else + { + WebSocketServer.FlashAccessPolicyEnabled = false; + } + } + #endif WebSocketServer.Start(); } catch (Exception ex) diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 54c8c9f9d..a504bc6ab 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -3,5 +3,5 @@ <package id="Alchemy" version="2.2.1" targetFramework="net45" />
<package id="MediaBrowser.BdInfo" version="1.0.0.5" targetFramework="net45" />
<package id="morelinq" version="1.0.16006" targetFramework="net45" />
- <package id="System.Data.SQLite.x86" version="1.0.89.0" targetFramework="net45" />
+ <package id="System.Data.SQLite.x86" version="1.0.90.0" targetFramework="net45" />
</packages>
\ No newline at end of file diff --git a/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloadInfo.cs b/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloadInfo.cs index 970e5a3e0..7cb7278dc 100644 --- a/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloadInfo.cs +++ b/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloadInfo.cs @@ -1,20 +1,57 @@ - +using System; + namespace MediaBrowser.ServerApplication.FFMpeg { public static class FFMpegDownloadInfo { - public static string Version = "ffmpeg20130904"; + public static string Version = ffmpegOsType("Version"); - public static string[] FfMpegUrls = new[] - { - "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.2013-10-11.tar.gz", + public static string[] FfMpegUrls = ffmpegOsType("FfMpegUrls").Split(','); - "https://www.dropbox.com/s/b9v17h105cps7p0/ffmpeg.static.32bit.2013-10-11.tar.gz?dl=1" - }; + public static string FFMpegFilename = ffmpegOsType("FFMpegFilename"); + public static string FFProbeFilename = ffmpegOsType("FFProbeFilename"); - public static string FFMpegFilename = "ffmpeg"; - public static string FFProbeFilename = "ffprobe"; + public static string ArchiveType = ffmpegOsType("ArchiveType"); - public static string ArchiveType = "gz"; + private static string ffmpegOsType(string arg) + { + OperatingSystem os = Environment.OSVersion; + PlatformID pid = os.Platform; + switch (pid) + { + case PlatformID.Win32NT: + switch (arg) + { + case "Version": + return "ffmpeg20131221"; + case "FfMpegUrls": + return "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20131221-git-70d6ce7-win32-static.7z,https://www.dropbox.com/s/d38uj7857trbw1g/ffmpeg-20131209-git-a12f679-win32-static.7z?dl=1"; + case "FFMpegFilename": + return "ffmpeg.exe"; + case "FFProbeFilename": + return "ffprobe.exe"; + case "ArchiveType": + return "7z"; + } + break; + case PlatformID.Unix: + case PlatformID.MacOSX: + switch (arg) + { + case "Version": + return "ffmpeg20131221"; + case "FfMpegUrls": + return "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.2013-12-21.tar.gz,https://www.dropbox.com/s/b9v17h105cps7p0/ffmpeg.static.32bit.2013-10-11.tar.gz?dl=1"; + case "FFMpegFilename": + return "ffmpeg"; + case "FFProbeFilename": + return "ffprobe"; + case "ArchiveType": + return "gz"; + } + break; + } + return ""; + } } } diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index be7fb7b27..d37330821 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -22,6 +22,7 @@ <WarningLevel>4</WarningLevel> <PlatformTarget>x86</PlatformTarget> <Externalconsole>true</Externalconsole> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <DebugType>full</DebugType> @@ -31,20 +32,25 @@ <WarningLevel>4</WarningLevel> <PlatformTarget>x86</PlatformTarget> <Externalconsole>true</Externalconsole> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <Optimize>false</Optimize> <OutputPath>bin\Release</OutputPath> <WarningLevel>4</WarningLevel> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> + <Optimize>false</Optimize> + <OutputPath>bin\Release Mono</OutputPath> + <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> <Reference Include="System" /> - <Reference Include="ServiceStack.Common"> - <HintPath>..\packages\ServiceStack.Common.3.9.70\lib\net35\ServiceStack.Common.dll</HintPath> - </Reference> <Reference Include="ServiceStack.Interfaces"> - <HintPath>..\packages\ServiceStack.Common.3.9.70\lib\net35\ServiceStack.Interfaces.dll</HintPath> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> </Reference> + <Reference Include="Mono.Posix" Condition=" '$(ConfigurationName)' == 'Release Mono' "/> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -74,6 +80,9 @@ </Compile> <Compile Include="FFMpeg\FFMpegDownloadInfo.cs" /> <Compile Include="IO\FileSystemFactory.cs" /> + <Compile Include="..\MediaBrowser.ServerApplication\EntryPoints\WanAddressEntryPoint.cs"> + <Link>EntryPoints\WanAddressEntryPoint.cs</Link> + </Compile> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <ItemGroup> @@ -119,8 +128,11 @@ </ItemGroup> <ItemGroup> <None Include="app.config" /> - <None Include="sqlite3.dll"> + </ItemGroup> + <ItemGroup> + <Content Include="..\ThirdParty\SQLite3\x86\3.8.2\sqlite3.dll"> + <Link>sqlite3.dll</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </None> + </Content> </ItemGroup> </Project>
\ No newline at end of file diff --git a/MediaBrowser.Server.Mono/Program.cs b/MediaBrowser.Server.Mono/Program.cs index cf0b4c6d7..59fc11c07 100644 --- a/MediaBrowser.Server.Mono/Program.cs +++ b/MediaBrowser.Server.Mono/Program.cs @@ -102,7 +102,9 @@ namespace MediaBrowser.Server.Mono Console.WriteLine ("appHost.Init"); - var task = _appHost.Init(); + var initProgress = new Progress<double>(); + + var task = _appHost.Init(initProgress); Task.WaitAll (task); Console.WriteLine ("Running startup tasks"); diff --git a/MediaBrowser.Server.Mono/app.config b/MediaBrowser.Server.Mono/app.config index 7a240c6dd..c5abd3a20 100644 --- a/MediaBrowser.Server.Mono/app.config +++ b/MediaBrowser.Server.Mono/app.config @@ -8,7 +8,6 @@ </nlog> <appSettings> <add key="DebugProgramDataPath" value="ProgramData-Server" /> - <add key="ReleaseProgramDataPath" value="" /> - <add key="ProgramDataFolderName" value="ProgramData-Server" /> + <add key="ReleaseProgramDataPath" value="ProgramData-Server" /> </appSettings> </configuration> diff --git a/MediaBrowser.ServerApplication/App.config b/MediaBrowser.ServerApplication/App.config index ba6d74214..53788e09a 100644 --- a/MediaBrowser.ServerApplication/App.config +++ b/MediaBrowser.ServerApplication/App.config @@ -11,8 +11,7 @@ </nlog> <appSettings> <add key="DebugProgramDataPath" value="..\..\..\..\ProgramData-Server" /> - <add key="ReleaseProgramDataPath" value="%ApplicationData%" /> - <add key="ProgramDataFolderName" value="MediaBrowser-Server" /> + <add key="ReleaseProgramDataPath" value="%ApplicationData%\MediaBrowser-Server" /> <add key="ClientSettingsProvider.ServiceUri" value="" /> </appSettings> <startup useLegacyV2RuntimeActivationPolicy="true"> @@ -46,6 +45,10 @@ <assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.0.89.0" newVersion="1.0.89.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="SimpleInjector" publicKeyToken="984cb50dea722e99" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-2.3.6.0" newVersion="2.3.6.0" /> + </dependentAssembly> </assemblyBinding> </runtime> <system.web> diff --git a/MediaBrowser.ServerApplication/App.xaml.cs b/MediaBrowser.ServerApplication/App.xaml.cs index 9b978ca2d..839841620 100644 --- a/MediaBrowser.ServerApplication/App.xaml.cs +++ b/MediaBrowser.ServerApplication/App.xaml.cs @@ -72,12 +72,14 @@ namespace MediaBrowser.ServerApplication { try { + var initProgress = new Progress<double>(); + if (!IsRunningAsService) { - ShowSplashWindow(); + ShowSplashWindow(initProgress); } - await _appHost.Init(); + await _appHost.Init(initProgress); var task = _appHost.RunStartupTasks(); @@ -114,7 +116,8 @@ namespace MediaBrowser.ServerApplication var win = new MainWindow(host.LogManager, host, host.ServerConfigurationManager, host.UserManager, host.LibraryManager, host.JsonSerializer, - host.DisplayPreferencesRepository); + host.DisplayPreferencesRepository, + host.ItemRepository); win.Show(); @@ -131,9 +134,9 @@ namespace MediaBrowser.ServerApplication } private SplashWindow _splashWindow; - private void ShowSplashWindow() + private void ShowSplashWindow(Progress<double> progress) { - var win = new SplashWindow(_appHost.ApplicationVersion); + var win = new SplashWindow(_appHost.ApplicationVersion, progress); win.Show(); _splashWindow = win; diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 6058c5958..514fbe6c1 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common.Implementations.ScheduledTasks; using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Common.Net; +using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; @@ -28,7 +29,6 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using MediaBrowser.Model.Updates; using MediaBrowser.Providers; @@ -49,6 +49,7 @@ using MediaBrowser.Server.Implementations.Providers; using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.WebSocket; +using MediaBrowser.ServerApplication.EntryPoints; using MediaBrowser.ServerApplication.FFMpeg; using MediaBrowser.ServerApplication.IO; using MediaBrowser.ServerApplication.Native; @@ -70,12 +71,6 @@ namespace MediaBrowser.ServerApplication public class ApplicationHost : BaseApplicationHost<ServerApplicationPaths>, IServerApplicationHost { /// <summary> - /// Gets the server kernel. - /// </summary> - /// <value>The server kernel.</value> - protected Kernel ServerKernel { get; set; } - - /// <summary> /// Gets the server configuration manager. /// </summary> /// <value>The server configuration manager.</value> @@ -167,11 +162,9 @@ namespace MediaBrowser.ServerApplication private IUserDataManager UserDataManager { get; set; } private IUserRepository UserRepository { get; set; } internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; } - private IItemRepository ItemRepository { get; set; } + internal IItemRepository ItemRepository { get; set; } private INotificationsRepository NotificationsRepository { get; set; } - private Task<IHttpServer> _httpServerCreationTask; - /// <summary> /// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// </summary> @@ -216,39 +209,24 @@ namespace MediaBrowser.ServerApplication } /// <summary> - /// Called when [logger loaded]. - /// </summary> - protected override void OnLoggerLoaded() - { - base.OnLoggerLoaded(); - - _httpServerCreationTask = Task.Run(() => ServerFactory.CreateServer(this, LogManager, "Media Browser", "mediabrowser", "dashboard/index.html")); - } - - /// <summary> /// Registers resources that classes will depend on /// </summary> /// <returns>Task.</returns> - protected override async Task RegisterResources() + protected override async Task RegisterResources(IProgress<double> progress) { - ServerKernel = new Kernel(); - - await base.RegisterResources().ConfigureAwait(false); + await base.RegisterResources(progress).ConfigureAwait(false); RegisterSingleInstance<IHttpResultFactory>(new HttpResultFactory(LogManager, FileSystemManager)); RegisterSingleInstance<IServerApplicationHost>(this); RegisterSingleInstance<IServerApplicationPaths>(ApplicationPaths); - RegisterSingleInstance(ServerKernel); RegisterSingleInstance(ServerConfigurationManager); RegisterSingleInstance<IWebSocketServer>(() => new AlchemyServer(Logger)); RegisterSingleInstance<IBlurayExaminer>(() => new BdInfoExaminer()); - var mediaEncoderTask = RegisterMediaEncoder(); - UserDataManager = new UserDataManager(LogManager); RegisterSingleInstance(UserDataManager); @@ -278,8 +256,9 @@ namespace MediaBrowser.ServerApplication SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager); RegisterSingleInstance(SessionManager); - HttpServer = await _httpServerCreationTask.ConfigureAwait(false); + HttpServer = ServerFactory.CreateServer(this, LogManager, "Media Browser", "mediabrowser", "dashboard/index.html"); RegisterSingleInstance(HttpServer, false); + progress.Report(10); ServerManager = new ServerManager(this, JsonSerializer, Logger, ServerConfigurationManager); RegisterSingleInstance(ServerManager); @@ -293,16 +272,27 @@ namespace MediaBrowser.ServerApplication DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataManager, ItemRepository, ImageProcessor); RegisterSingleInstance(DtoService); - LiveTvManager = new LiveTvManager(ApplicationPaths, FileSystemManager, Logger, ItemRepository, ImageProcessor, UserManager, LocalizationManager, UserDataManager, DtoService); + LiveTvManager = new LiveTvManager(ApplicationPaths, FileSystemManager, Logger, ItemRepository, ImageProcessor, LocalizationManager, UserDataManager, DtoService, UserManager); RegisterSingleInstance(LiveTvManager); + progress.Report(15); + + var innerProgress = new ActionableProgress<double>(); + innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15)); + + await RegisterMediaEncoder(innerProgress).ConfigureAwait(false); + progress.Report(90); var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false)); var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false)); var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false)); await ConfigureNotificationsRepository().ConfigureAwait(false); + progress.Report(92); + + await Task.WhenAll(itemsTask, displayPreferencesTask, userdataTask).ConfigureAwait(false); + progress.Report(100); - await Task.WhenAll(itemsTask, displayPreferencesTask, userdataTask, mediaEncoderTask).ConfigureAwait(false); + await ((UserManager) UserManager).Initialize().ConfigureAwait(false); SetKernelProperties(); } @@ -321,9 +311,9 @@ namespace MediaBrowser.ServerApplication /// Registers the media encoder. /// </summary> /// <returns>Task.</returns> - private async Task RegisterMediaEncoder() + private async Task RegisterMediaEncoder(IProgress<double> progress) { - var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager).GetFFMpegInfo().ConfigureAwait(false); + var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager).GetFFMpegInfo(progress).ConfigureAwait(false); MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.Path, info.ProbePath, info.Version, FileSystemManager); RegisterSingleInstance(MediaEncoder); @@ -334,11 +324,11 @@ namespace MediaBrowser.ServerApplication /// </summary> private void SetKernelProperties() { - Parallel.Invoke( - () => ServerKernel.FFMpegManager = new FFMpegManager(ApplicationPaths, MediaEncoder, Logger, ItemRepository, FileSystemManager), - () => LocalizedStrings.StringFiles = GetExports<LocalizedStringData>(), - SetStaticProperties - ); + new FFMpegManager(MediaEncoder, Logger, ItemRepository, FileSystemManager, ServerConfigurationManager); + + LocalizedStrings.StringFiles = GetExports<LocalizedStringData>(); + + SetStaticProperties(); } /// <summary> @@ -454,6 +444,8 @@ namespace MediaBrowser.ServerApplication ImageProcessor.AddParts(GetExports<IImageEnhancer>()); LiveTvManager.AddParts(GetExports<ILiveTvService>()); + + SessionManager.AddParts(GetExports<ISessionControllerFactory>()); } /// <summary> @@ -550,9 +542,7 @@ namespace MediaBrowser.ServerApplication /// <returns>IEnumerable{Assembly}.</returns> protected override IEnumerable<Assembly> GetComposablePartAssemblies() { - var list = Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) - .Select(LoadAssembly) - .Where(a => a != null) + var list = GetPluginAssemblies() .ToList(); // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that @@ -571,7 +561,7 @@ namespace MediaBrowser.ServerApplication list.Add(typeof(IApplicationHost).Assembly); // Include composable parts in the Controller assembly - list.Add(typeof(Kernel).Assembly); + list.Add(typeof(IServerApplicationHost).Assembly); // Include composable parts in the Providers assembly list.Add(typeof(ImagesByNameProvider).Assembly); @@ -590,6 +580,25 @@ namespace MediaBrowser.ServerApplication return list; } + /// <summary> + /// Gets the plugin assemblies. + /// </summary> + /// <returns>IEnumerable{Assembly}.</returns> + private IEnumerable<Assembly> GetPluginAssemblies() + { + try + { + return Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) + .Select(LoadAssembly) + .Where(a => a != null) + .ToList(); + } + catch (DirectoryNotFoundException) + { + return new List<Assembly>(); + } + } + private readonly string _systemId = Environment.MachineName.GetMD5().ToString(); /// <summary> @@ -612,11 +621,13 @@ namespace MediaBrowser.ServerApplication ProgramDataPath = ApplicationPaths.ProgramDataPath, LogPath = ApplicationPaths.LogDirectoryPath, ItemsByNamePath = ApplicationPaths.ItemsByNamePath, + CachePath = ApplicationPaths.CachePath, MacAddress = GetMacAddress(), HttpServerPortNumber = ServerConfigurationManager.Configuration.HttpServerPortNumber, OperatingSystem = Environment.OSVersion.ToString(), CanSelfRestart = CanSelfRestart, - CanSelfUpdate = CanSelfUpdate + CanSelfUpdate = CanSelfUpdate, + WanAddress = WanAddressEntryPoint.WanAddress }; } diff --git a/MediaBrowser.ServerApplication/EntryPoints/WanAddressEntryPoint.cs b/MediaBrowser.ServerApplication/EntryPoints/WanAddressEntryPoint.cs new file mode 100644 index 000000000..7b2a1314e --- /dev/null +++ b/MediaBrowser.ServerApplication/EntryPoints/WanAddressEntryPoint.cs @@ -0,0 +1,55 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Plugins; +using System; +using System.IO; +using System.Threading; + +namespace MediaBrowser.ServerApplication.EntryPoints +{ + public class WanAddressEntryPoint : IServerEntryPoint + { + public static string WanAddress; + private Timer _timer; + private readonly IHttpClient _httpClient; + + public WanAddressEntryPoint(IHttpClient httpClient) + { + _httpClient = httpClient; + } + + public void Run() + { + _timer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(1), TimeSpan.FromHours(24)); + } + + private async void TimerCallback(object state) + { + try + { + using (var stream = await _httpClient.Get(new HttpRequestOptions + { + Url = "http://bot.whatismyipaddress.com/" + + }).ConfigureAwait(false)) + { + using (var reader = new StreamReader(stream)) + { + WanAddress = await reader.ReadToEndAsync().ConfigureAwait(false); + } + } + } + catch + { + } + } + + public void Dispose() + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + } + } +} diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs index ec7dc582d..fc50df216 100644 --- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs +++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs @@ -3,14 +3,12 @@ namespace MediaBrowser.ServerApplication.FFMpeg { public static class FFMpegDownloadInfo { - public static string Version = "ffmpeg20131110.1"; + public static string Version = "ffmpeg20131209"; public static string[] FfMpegUrls = new[] { - "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/windows/ffmpeg-20131110-git-8cdf4e0-win32-static.7z", - - "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20131110-git-8cdf4e0-win32-static.7z", - "https://www.dropbox.com/s/5clspc636v9hie6/ffmpeg-20131110-git-8cdf4e0-win32-static.7z?dl=1" + "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20131209-git-a12f679-win32-static.7z", + "https://www.dropbox.com/s/d38uj7857trbw1g/ffmpeg-20131209-git-a12f679-win32-static.7z?dl=1" }; public static string FFMpegFilename = "ffmpeg.exe"; diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs index 1f329446e..1e99c4eb0 100644 --- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs @@ -1,7 +1,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.IO; +using MediaBrowser.Common.Progress; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; @@ -12,6 +12,9 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +#if __MonoCS__ +using Mono.Unix.Native; +#endif namespace MediaBrowser.ServerApplication.FFMpeg { @@ -37,7 +40,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg _fileSystem = fileSystem; } - public async Task<FFMpegInfo> GetFFMpegInfo() + public async Task<FFMpegInfo> GetFFMpegInfo(IProgress<double> progress) { var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), FFMpegDownloadInfo.Version); @@ -52,30 +55,69 @@ namespace MediaBrowser.ServerApplication.FFMpeg var tasks = new List<Task>(); + double ffmpegPercent = 0; + double fontPercent = 0; + var syncLock = new object(); + if (!File.Exists(info.ProbePath) || !File.Exists(info.Path)) { - tasks.Add(DownloadFFMpeg(info)); + var ffmpegProgress = new ActionableProgress<double>(); + ffmpegProgress.RegisterAction(p => + { + ffmpegPercent = p; + + lock (syncLock) + { + progress.Report((ffmpegPercent / 2) + (fontPercent / 2)); + } + }); + + tasks.Add(DownloadFFMpeg(info, ffmpegProgress)); + } + else + { + ffmpegPercent = 100; + progress.Report(50); } - tasks.Add(DownloadFonts(versionedDirectoryPath)); + var fontProgress = new ActionableProgress<double>(); + fontProgress.RegisterAction(p => + { + fontPercent = p; + + lock (syncLock) + { + progress.Report((ffmpegPercent / 2) + (fontPercent / 2)); + } + }); + + tasks.Add(DownloadFonts(versionedDirectoryPath, fontProgress)); await Task.WhenAll(tasks).ConfigureAwait(false); return info; } - private async Task DownloadFFMpeg(FFMpegInfo info) + private async Task DownloadFFMpeg(FFMpegInfo info, IProgress<double> progress) { foreach (var url in FFMpegDownloadInfo.FfMpegUrls) { + progress.Report(0); + try { - var tempFile = await DownloadFFMpeg(info, url).ConfigureAwait(false); + var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + CancellationToken = CancellationToken.None, + Progress = progress + + }).ConfigureAwait(false); ExtractFFMpeg(tempFile, Path.GetDirectoryName(info.Path)); return; } - catch (HttpException ex) + catch (HttpException) { } @@ -84,16 +126,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg throw new ApplicationException("Unable to download required components. Please try again later."); } - private Task<string> DownloadFFMpeg(FFMpegInfo info, string url) - { - return _httpClient.GetTempFile(new HttpRequestOptions - { - Url = url, - CancellationToken = CancellationToken.None, - Progress = new Progress<double>() - }); - } - private void ExtractFFMpeg(string tempFile, string targetFolder) { _logger.Debug("Extracting ffmpeg from {0}", tempFile); @@ -118,6 +150,13 @@ namespace MediaBrowser.ServerApplication.FFMpeg })) { File.Copy(file, Path.Combine(targetFolder, Path.GetFileName(file)), true); + #if __MonoCS__ + //Linux: File permission to 666, and user's execute bit + if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) + { + Syscall.chmod(Path.Combine(targetFolder, Path.GetFileName(file)), FilePermissions.DEFFILEMODE | FilePermissions.S_IXUSR); + } + #endif } } finally @@ -158,7 +197,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg /// Extracts the fonts. /// </summary> /// <param name="targetPath">The target path.</param> - private async Task DownloadFonts(string targetPath) + private async Task DownloadFonts(string targetPath, IProgress<double> progress) { try { @@ -172,7 +211,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg if (!File.Exists(fontFile)) { - await DownloadFontFile(fontsDirectory, fontFilename).ConfigureAwait(false); + await DownloadFontFile(fontsDirectory, fontFilename, progress).ConfigureAwait(false); } await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); @@ -187,6 +226,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg // Don't let the server crash because of this _logger.ErrorException("Error writing ffmpeg font files", ex); } + + progress.Report(100); } /// <summary> @@ -195,7 +236,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg /// <param name="fontsDirectory">The fonts directory.</param> /// <param name="fontFilename">The font filename.</param> /// <returns>Task.</returns> - private async Task DownloadFontFile(string fontsDirectory, string fontFilename) + private async Task DownloadFontFile(string fontsDirectory, string fontFilename, IProgress<double> progress) { var existingFile = Directory .EnumerateFiles(_appPaths.ProgramDataPath, fontFilename, SearchOption.AllDirectories) @@ -219,12 +260,14 @@ namespace MediaBrowser.ServerApplication.FFMpeg foreach (var url in _fontUrls) { + progress.Report(0); + try { tempFile = await _httpClient.GetTempFile(new HttpRequestOptions { Url = url, - Progress = new Progress<double>() + Progress = progress }).ConfigureAwait(false); diff --git a/MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs b/MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs index a3d470689..1a5d73e6b 100644 --- a/MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs +++ b/MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs @@ -35,6 +35,8 @@ namespace MediaBrowser.ServerApplication private readonly ILibraryManager _libraryManager; private readonly IDisplayPreferencesRepository _displayPreferencesManager; + private readonly IItemRepository _itemRepository; + /// <summary> /// The current user /// </summary> @@ -48,7 +50,7 @@ namespace MediaBrowser.ServerApplication /// <param name="userManager">The user manager.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="displayPreferencesManager">The display preferences manager.</param> - public LibraryExplorer(IJsonSerializer jsonSerializer, ILogger logger, IApplicationHost appHost, IUserManager userManager, ILibraryManager libraryManager, IDisplayPreferencesRepository displayPreferencesManager) + public LibraryExplorer(IJsonSerializer jsonSerializer, ILogger logger, IApplicationHost appHost, IUserManager userManager, ILibraryManager libraryManager, IDisplayPreferencesRepository displayPreferencesManager, IItemRepository itemRepo) { _logger = logger; _jsonSerializer = jsonSerializer; @@ -62,7 +64,7 @@ namespace MediaBrowser.ServerApplication ddlProfile.Items.Insert(0, new User { Name = "Physical" }); ddlProfile.SelectedIndex = 0; ddlIndexBy.Visibility = ddlSortBy.Visibility = lblIndexBy.Visibility = lblSortBy.Visibility = Visibility.Hidden; - + _itemRepository = itemRepo; } /// <summary> @@ -88,7 +90,7 @@ namespace MediaBrowser.ServerApplication Cursor = Cursors.Wait; await Task.Run(() => { - IEnumerable<BaseItem> children = CurrentUser.Name == "Physical" ? _libraryManager.RootFolder.Children : _libraryManager.RootFolder.GetChildren(CurrentUser, true); + IEnumerable<BaseItem> children = CurrentUser.Name == "Physical" ? new[] { _libraryManager.RootFolder } : _libraryManager.RootFolder.GetChildren(CurrentUser, true); children = OrderByName(children, CurrentUser); foreach (Folder folder in children) @@ -100,7 +102,7 @@ namespace MediaBrowser.ServerApplication var prefs = ddlProfile.SelectedItem != null ? _displayPreferencesManager.GetDisplayPreferences(currentFolder.DisplayPreferencesId, (ddlProfile.SelectedItem as User).Id, "LibraryExplorer") ?? new DisplayPreferences { SortBy = ItemSortBy.SortName } : new DisplayPreferences { SortBy = ItemSortBy.SortName }; var node = new TreeViewItem { Tag = currentFolder }; - var subChildren = currentFolder.GetChildren(CurrentUser, true, prefs.IndexBy); + var subChildren = currentFolder.GetChildren(CurrentUser, true); subChildren = OrderByName(subChildren, CurrentUser); AddChildren(node, subChildren, CurrentUser); node.Header = currentFolder.Name + " (" + @@ -212,7 +214,24 @@ namespace MediaBrowser.ServerApplication lblIndexBy.Visibility = ddlIndexBy.Visibility = ddlSortBy.Visibility = lblSortBy.Visibility = Visibility.Hidden; } - txtData.Text = FormatJson(_jsonSerializer.SerializeToString(item)); + + var json = FormatJson(_jsonSerializer.SerializeToString(item)); + + if (item is IHasMediaStreams) + { + var mediaStreams = _itemRepository.GetMediaStreams(new MediaStreamQuery + { + ItemId = item.Id + + }).ToList(); + + if (mediaStreams.Count > 0) + { + json += "\n\nMedia Streams:\n\n"+FormatJson(_jsonSerializer.SerializeToString(mediaStreams)); + } + } + + txtData.Text = json; var previews = new List<PreviewItem>(); await Task.Run(() => @@ -223,23 +242,23 @@ namespace MediaBrowser.ServerApplication } if (item.HasImage(ImageType.Banner)) { - previews.Add(new PreviewItem(item.GetImage(ImageType.Banner), "Banner")); + previews.Add(new PreviewItem(item.GetImagePath(ImageType.Banner), "Banner")); } if (item.HasImage(ImageType.Logo)) { - previews.Add(new PreviewItem(item.GetImage(ImageType.Logo), "Logo")); + previews.Add(new PreviewItem(item.GetImagePath(ImageType.Logo), "Logo")); } if (item.HasImage(ImageType.Art)) { - previews.Add(new PreviewItem(item.GetImage(ImageType.Art), "Art")); + previews.Add(new PreviewItem(item.GetImagePath(ImageType.Art), "Art")); } if (item.HasImage(ImageType.Thumb)) { - previews.Add(new PreviewItem(item.GetImage(ImageType.Thumb), "Thumb")); + previews.Add(new PreviewItem(item.GetImagePath(ImageType.Thumb), "Thumb")); } - previews.AddRange( - item.BackdropImagePaths.Select( - image => new PreviewItem(image, "Backdrop"))); + previews.AddRange( + item.BackdropImagePaths.Select( + image => new PreviewItem(image, "Backdrop"))); }); lstPreviews.ItemsSource = previews; lstPreviews.Items.Refresh(); @@ -371,7 +390,7 @@ namespace MediaBrowser.ServerApplication //re-build the current item's children as an index prefs.IndexBy = ddlIndexBy.SelectedItem as string; treeItem.Items.Clear(); - AddChildren(treeItem, OrderBy(folder.GetChildren(CurrentUser, true, prefs.IndexBy), CurrentUser, prefs.SortBy), CurrentUser); + AddChildren(treeItem, OrderBy(folder.GetChildren(CurrentUser, true), CurrentUser, prefs.SortBy), CurrentUser); treeItem.Header = folder.Name + "(" + treeItem.Items.Count + ")"; Cursor = Cursors.Arrow; @@ -412,7 +431,7 @@ namespace MediaBrowser.ServerApplication //re-sort prefs.SortBy = ddlSortBy.SelectedItem as string; treeItem.Items.Clear(); - AddChildren(treeItem, OrderBy(folder.GetChildren(CurrentUser, true, prefs.IndexBy), CurrentUser, prefs.SortBy ?? ItemSortBy.SortName), CurrentUser); + AddChildren(treeItem, OrderBy(folder.GetChildren(CurrentUser, true), CurrentUser, prefs.SortBy ?? ItemSortBy.SortName), CurrentUser); treeItem.Header = folder.Name + "(" + treeItem.Items.Count + ")"; Cursor = Cursors.Arrow; diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index 3733d55af..7da17bc22 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -36,7 +36,9 @@ namespace MediaBrowser.ServerApplication var startFlag = Environment.GetCommandLineArgs().ElementAtOrDefault(1); _isRunningAsService = string.Equals(startFlag, "-service", StringComparison.OrdinalIgnoreCase); - var appPaths = CreateApplicationPaths(_isRunningAsService); + var applicationPath = Process.GetCurrentProcess().MainModule.FileName; + + var appPaths = CreateApplicationPaths(applicationPath, _isRunningAsService); var logManager = new NlogManager(appPaths.LogDirectoryPath, "server"); logManager.ReloadLogger(LogSeverity.Debug); @@ -49,7 +51,7 @@ namespace MediaBrowser.ServerApplication if (string.Equals(startFlag, "-installservice", StringComparison.OrdinalIgnoreCase)) { logger.Info("Performing service installation"); - InstallService(logger); + InstallService(applicationPath, logger); return; } @@ -57,7 +59,7 @@ namespace MediaBrowser.ServerApplication if (string.Equals(startFlag, "-installserviceasadmin", StringComparison.OrdinalIgnoreCase)) { logger.Info("Performing service installation"); - RunServiceInstallation(); + RunServiceInstallation(applicationPath); return; } @@ -65,7 +67,7 @@ namespace MediaBrowser.ServerApplication if (string.Equals(startFlag, "-uninstallservice", StringComparison.OrdinalIgnoreCase)) { logger.Info("Performing service uninstallation"); - UninstallService(logger); + UninstallService(applicationPath, logger); return; } @@ -73,17 +75,17 @@ namespace MediaBrowser.ServerApplication if (string.Equals(startFlag, "-uninstallserviceasadmin", StringComparison.OrdinalIgnoreCase)) { logger.Info("Performing service uninstallation"); - RunServiceUninstallation(); + RunServiceUninstallation(applicationPath); return; } AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - RunServiceInstallationIfNeeded(); + RunServiceInstallationIfNeeded(applicationPath); var currentProcess = Process.GetCurrentProcess(); - if (IsAlreadyRunning(currentProcess)) + if (IsAlreadyRunning(applicationPath, currentProcess)) { logger.Info("Shutting down because another instance of Media Browser Server is already running."); return; @@ -110,21 +112,19 @@ namespace MediaBrowser.ServerApplication /// </summary> /// <param name="currentProcess">The current process.</param> /// <returns><c>true</c> if [is already running] [the specified current process]; otherwise, <c>false</c>.</returns> - private static bool IsAlreadyRunning(Process currentProcess) + private static bool IsAlreadyRunning(string applicationPath, Process currentProcess) { - var runningPath = currentProcess.MainModule.FileName; - var duplicate = Process.GetProcesses().FirstOrDefault(i => + { + try + { + return string.Equals(applicationPath, i.MainModule.FileName) && currentProcess.Id != i.Id; + } + catch (Exception) { - try - { - return string.Equals(runningPath, i.MainModule.FileName) && currentProcess.Id != i.Id; - } - catch (Exception) - { - return false; - } - }); + return false; + } + }); if (duplicate != null) { @@ -145,19 +145,17 @@ namespace MediaBrowser.ServerApplication /// </summary> /// <param name="runAsService">if set to <c>true</c> [run as service].</param> /// <returns>ServerApplicationPaths.</returns> - private static ServerApplicationPaths CreateApplicationPaths(bool runAsService) + private static ServerApplicationPaths CreateApplicationPaths(string applicationPath, bool runAsService) { if (runAsService) { - var systemPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + var systemPath = Path.GetDirectoryName(applicationPath); var programDataPath = Path.GetDirectoryName(systemPath); - return new ServerApplicationPaths(programDataPath); + return new ServerApplicationPaths(programDataPath, applicationPath); } - var applicationPath = Process.GetCurrentProcess().MainModule.FileName; - return new ServerApplicationPaths(applicationPath); } @@ -199,8 +197,7 @@ namespace MediaBrowser.ServerApplication logger.Info("Operating system: {0}", Environment.OSVersion.ToString()); logger.Info("Program data path: {0}", appPaths.ProgramDataPath); - var runningPath = Process.GetCurrentProcess().MainModule.FileName; - logger.Info("Executable: {0}", runningPath); + logger.Info("Application Path: {0}", appPaths.ApplicationPath); } /// <summary> @@ -279,13 +276,11 @@ namespace MediaBrowser.ServerApplication /// <summary> /// Installs the service. /// </summary> - private static void InstallService(ILogger logger) + private static void InstallService(string applicationPath, ILogger logger) { - var runningPath = Process.GetCurrentProcess().MainModule.FileName; - try { - ManagedInstallerClass.InstallHelper(new[] { runningPath }); + ManagedInstallerClass.InstallHelper(new[] { applicationPath }); logger.Info("Service installation succeeded"); } @@ -298,13 +293,11 @@ namespace MediaBrowser.ServerApplication /// <summary> /// Uninstalls the service. /// </summary> - private static void UninstallService(ILogger logger) + private static void UninstallService(string applicationPath, ILogger logger) { - var runningPath = Process.GetCurrentProcess().MainModule.FileName; - try { - ManagedInstallerClass.InstallHelper(new[] { "/u", runningPath }); + ManagedInstallerClass.InstallHelper(new[] { "/u", applicationPath }); logger.Info("Service uninstallation succeeded"); } @@ -314,26 +307,24 @@ namespace MediaBrowser.ServerApplication } } - private static void RunServiceInstallationIfNeeded() + private static void RunServiceInstallationIfNeeded(string applicationPath) { var ctl = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName == BackgroundService.Name); if (ctl == null) { - RunServiceInstallation(); + RunServiceInstallation(applicationPath); } } /// <summary> /// Runs the service installation. /// </summary> - private static void RunServiceInstallation() + private static void RunServiceInstallation(string applicationPath) { - var runningPath = Process.GetCurrentProcess().MainModule.FileName; - var startInfo = new ProcessStartInfo { - FileName = runningPath, + FileName = applicationPath, Arguments = "-installservice", @@ -352,13 +343,11 @@ namespace MediaBrowser.ServerApplication /// <summary> /// Runs the service uninstallation. /// </summary> - private static void RunServiceUninstallation() + private static void RunServiceUninstallation(string applicationPath) { - var runningPath = Process.GetCurrentProcess().MainModule.FileName; - var startInfo = new ProcessStartInfo { - FileName = runningPath, + FileName = applicationPath, Arguments = "-uninstallservice", @@ -416,6 +405,7 @@ namespace MediaBrowser.ServerApplication _logger.ErrorException("UnhandledException", ex); var path = Path.Combine(_appHost.ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, "unhandled_" + Guid.NewGuid() + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(path)); var builder = LogHelper.GetLogMessage(ex); diff --git a/MediaBrowser.ServerApplication/MainWindow.xaml.cs b/MediaBrowser.ServerApplication/MainWindow.xaml.cs index b1972fbdb..040d714cf 100644 --- a/MediaBrowser.ServerApplication/MainWindow.xaml.cs +++ b/MediaBrowser.ServerApplication/MainWindow.xaml.cs @@ -45,6 +45,7 @@ namespace MediaBrowser.ServerApplication private readonly ILibraryManager _libraryManager; private readonly IJsonSerializer _jsonSerializer; private readonly IDisplayPreferencesRepository _displayPreferencesManager; + private readonly IItemRepository _itemRepository; /// <summary> /// Initializes a new instance of the <see cref="MainWindow" /> class. @@ -57,7 +58,7 @@ namespace MediaBrowser.ServerApplication /// <param name="jsonSerializer">The json serializer.</param> /// <param name="displayPreferencesManager">The display preferences manager.</param> /// <exception cref="System.ArgumentNullException">logger</exception> - public MainWindow(ILogManager logManager, IServerApplicationHost appHost, IServerConfigurationManager configurationManager, IUserManager userManager, ILibraryManager libraryManager, IJsonSerializer jsonSerializer, IDisplayPreferencesRepository displayPreferencesManager) + public MainWindow(ILogManager logManager, IServerApplicationHost appHost, IServerConfigurationManager configurationManager, IUserManager userManager, ILibraryManager libraryManager, IJsonSerializer jsonSerializer, IDisplayPreferencesRepository displayPreferencesManager, IItemRepository itemRepo) { if (logManager == null) { @@ -73,6 +74,7 @@ namespace MediaBrowser.ServerApplication } _logger = logManager.GetLogger("MainWindow"); + _itemRepository = itemRepo; _appHost = appHost; _logManager = logManager; _configurationManager = configurationManager; @@ -131,14 +133,10 @@ namespace MediaBrowser.ServerApplication { Dispatcher.InvokeAsync(() => { - var developerToolsVisibility = _configurationManager.Configuration.EnableDeveloperTools - ? Visibility.Visible - : Visibility.Collapsed; - - separatorDeveloperTools.Visibility = developerToolsVisibility; - cmdReloadServer.Visibility = developerToolsVisibility; - cmOpenExplorer.Visibility = developerToolsVisibility; - cmShowLogWindow.Visibility = developerToolsVisibility; + separatorDeveloperTools.Visibility = Visibility.Visible; + cmdReloadServer.Visibility = Visibility.Visible; + cmOpenExplorer.Visibility = Visibility.Visible; + cmShowLogWindow.Visibility = Visibility.Visible; }); } @@ -234,7 +232,7 @@ namespace MediaBrowser.ServerApplication /// <param name="e">The <see cref="RoutedEventArgs" /> instance containing the event data.</param> private void cmOpenExplorer_click(object sender, RoutedEventArgs e) { - new LibraryExplorer(_jsonSerializer, _logger, _appHost, _userManager, _libraryManager, _displayPreferencesManager).Show(); + new LibraryExplorer(_jsonSerializer, _logger, _appHost, _userManager, _libraryManager, _displayPreferencesManager, _itemRepository).Show(); } /// <summary> diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index e02bb1d69..cf7eb989b 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -134,9 +134,12 @@ <Reference Include="ServiceStack.Interfaces"> <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> </Reference> - <Reference Include="SimpleInjector, Version=2.3.6.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> + <Reference Include="SimpleInjector, Version=2.4.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\SimpleInjector.2.3.6\lib\net40-client\SimpleInjector.dll</HintPath> + <HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.dll</HintPath> + </Reference> + <Reference Include="SimpleInjector.Diagnostics"> + <HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Configuration.Install" /> @@ -162,6 +165,7 @@ </Compile> <Compile Include="EntryPoints\ResourceEntryPoint.cs" /> <Compile Include="EntryPoints\StartupWizard.cs" /> + <Compile Include="EntryPoints\WanAddressEntryPoint.cs" /> <Compile Include="FFMpeg\FFMpegDownloadInfo.cs" /> <Compile Include="FFMpeg\FFMpegInfo.cs" /> <Compile Include="IO\FileSystemFactory.cs" /> diff --git a/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs b/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs index 91f0974eb..d2e542536 100644 --- a/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs +++ b/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs @@ -20,6 +20,8 @@ namespace MediaBrowser.ServerApplication.Native /// <param name="tempDirectory">The temp directory.</param> public static void AuthorizeServer(int httpServerPort, string httpServerUrlPrefix, int webSocketPort, int udpPort, string tempDirectory) { + Directory.CreateDirectory(tempDirectory); + // Create a temp file path to extract the bat file to var tmpFile = Path.Combine(tempDirectory, Guid.NewGuid() + ".bat"); diff --git a/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml b/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml index 315c88cd2..b35eadd06 100644 --- a/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml +++ b/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml @@ -2,13 +2,18 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="386.939" Width="664.49" WindowStartupLocation="CenterScreen" Title="Media Browser Server" ShowInTaskbar="True" WindowStyle="None" BorderThickness="1" BorderBrush="#cccccc" AllowsTransparency="True"> - <Border BorderBrush="DarkGray" BorderThickness="2" Margin="0,0,0,0"> - <Grid Margin="-2,0,0,0"> - <Image x:Name="imgLogo" HorizontalAlignment="Center" Height="146" Margin="0,10,44,0" VerticalAlignment="Top" Width="616" Source="/Resources/Images/mb3logo800.png" Opacity="0.5"/> + + <Border BorderBrush="DarkGray" BorderThickness="1"> + <Grid> + + <Image HorizontalAlignment="Center" Height="146" Margin="0,10,44,0" VerticalAlignment="Top" Width="616" Source="/Resources/Images/mb3logo800.png" Opacity="0.5"/> + <Grid HorizontalAlignment="Left" Height="153" Margin="0,173,0,0" VerticalAlignment="Top" Width="662" Background="Gray"> + <TextBlock x:Name="lblStatus" HorizontalAlignment="Left" Margin="12,14,0,18" Width="637" FontSize="36" Foreground="#FFE6D7D7" Text="Loading Media Browser Server..." TextWrapping="WrapWithOverflow"/> + <Rectangle Fill="#FF49494B" HorizontalAlignment="Left" Height="13" Stroke="Black" VerticalAlignment="Bottom" Width="662"/> - <Rectangle x:Name="rectProgress" Fill="#FF0A0ABF" HorizontalAlignment="Left" Height="13" Stroke="Black" VerticalAlignment="Bottom" Width="0"/> + <Rectangle x:Name="RectProgress" Fill="#52B54B" HorizontalAlignment="Left" Height="13" Stroke="Black" VerticalAlignment="Bottom" Width="0"/> </Grid> </Grid> diff --git a/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml.cs b/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml.cs index db2f1dbc4..29eb4bf45 100644 --- a/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml.cs +++ b/MediaBrowser.ServerApplication/Splash/SplashWindow.xaml.cs @@ -1,16 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; namespace MediaBrowser.ServerApplication.Splash { @@ -19,10 +9,33 @@ namespace MediaBrowser.ServerApplication.Splash /// </summary> public partial class SplashWindow : Window { - public SplashWindow(Version version) + private readonly Progress<double> _progress; + + public SplashWindow(Version version, Progress<double> progress) { InitializeComponent(); lblStatus.Text = string.Format("Loading Media Browser Server\nVersion {0}...", version); + + _progress = progress; + + progress.ProgressChanged += progress_ProgressChanged; + } + + void progress_ProgressChanged(object sender, double e) + { + Dispatcher.InvokeAsync(() => + { + var width = e * 6.62; + + RectProgress.Width = width; + }); + } + + protected override void OnClosing(CancelEventArgs e) + { + _progress.ProgressChanged += progress_ProgressChanged; + + base.OnClosing(e); } } } diff --git a/MediaBrowser.ServerApplication/packages.config b/MediaBrowser.ServerApplication/packages.config index f7af7084e..740cfe5f3 100644 --- a/MediaBrowser.ServerApplication/packages.config +++ b/MediaBrowser.ServerApplication/packages.config @@ -3,5 +3,5 @@ <package id="Hardcodet.Wpf.TaskbarNotification" version="1.0.4.0" targetFramework="net45" /> <package id="MediaBrowser.IsoMounting" version="3.0.65" targetFramework="net45" /> <package id="NLog" version="2.1.0" targetFramework="net45" /> - <package id="SimpleInjector" version="2.3.6" targetFramework="net45" /> + <package id="SimpleInjector" version="2.4.0" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 371ec818b..7743cc527 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -242,7 +242,24 @@ namespace MediaBrowser.WebDashboard.Api pages = pages.Where(p => p.ConfigurationPageType == request.PageType.Value); } - return ResultFactory.GetOptimizedResult(Request, pages.Select(p => new ConfigurationPageInfo(p)).ToList()); + // Don't allow a failing plugin to fail them all + var configPages = pages.Select(p => + { + + try + { + return new ConfigurationPageInfo(p); + } + catch (Exception ex) + { + Logger.ErrorException("Error getting plugin information from {0}", ex, p.GetType().Name); + return null; + } + }) + .Where(i => i != null) + .ToList(); + + return ResultFactory.GetOptimizedResult(Request, configPages); } /// <summary> @@ -258,7 +275,9 @@ namespace MediaBrowser.WebDashboard.Api // Don't cache if not configured to do so // But always cache images to simulate production - if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching && !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching && + !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) && + !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) { return ResultFactory.GetResult(GetResourceStream(path).Result, contentType); } @@ -267,7 +286,7 @@ namespace MediaBrowser.WebDashboard.Api // Cache images unconditionally - updates to image files will require new filename // If there's a version number in the query string we can cache this unconditionally - if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(request.V)) + if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(request.V)) { cacheDuration = TimeSpan.FromDays(365); } @@ -397,8 +416,7 @@ namespace MediaBrowser.WebDashboard.Api var files = new[] { - "http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.css", - "thirdparty/jqm-icon-pack-3.0/font-awesome/jqm-icon-pack-3.0.0-fa.css" + versionString, + "thirdparty/jquerymobile-1.4.0/jquery.mobile-1.4.0.min.css", "css/all.css" + versionString }; @@ -423,10 +441,8 @@ namespace MediaBrowser.WebDashboard.Api var files = new[] { - "http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js", - "http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.js", "scripts/all.js" + versionString, - "thirdparty/jstree1.0/jquery.jstree.js" + "thirdparty/jstree1.0/jquery.jstree.min.js" }; var tags = files.Select(s => string.Format("<script src=\"{0}\"></script>", s)).ToArray(); @@ -449,21 +465,22 @@ namespace MediaBrowser.WebDashboard.Api "extensions.js", "site.js", "librarybrowser.js", + "librarymenu.js", "ratingdialog.js", "aboutpage.js", "allusersettings.js", "alphapicker.js", "addpluginpage.js", "advancedconfigurationpage.js", - "advancedmetadataconfigurationpage.js", + "metadataadvanced.js", "boxsets.js", - "clientsettings.js", + "appsplayback.js", + "appsweather.js", "dashboardpage.js", "directorybrowser.js", "edititemmetadata.js", "edititempeople.js", "edititemimages.js", - "edituserpage.js", "gamesrecommendedpage.js", "gamesystemspage.js", "gamespage.js", @@ -478,9 +495,13 @@ namespace MediaBrowser.WebDashboard.Api "livetvchannel.js", "livetvchannels.js", "livetvguide.js", + "livetvnewrecording.js", + "livetvprogram.js", "livetvrecording.js", "livetvrecordings.js", "livetvtimer.js", + "livetvseriestimer.js", + "livetvseriestimers.js", "livetvtimers.js", "loginpage.js", "logpage.js", @@ -520,11 +541,14 @@ namespace MediaBrowser.WebDashboard.Api "tvshows.js", "tvstudios.js", "tvupcoming.js", - "updatepasswordpage.js", + "useredit.js", + "userpassword.js", "userimagepage.js", "userprofilespage.js", "usersettings.js", + "userparentalcontrol.js", "wizardfinishpage.js", + "wizardimagesettings.js", "wizardservice.js", "wizardstartpage.js", "wizardsettings.js", @@ -532,17 +556,20 @@ namespace MediaBrowser.WebDashboard.Api }; var memoryStream = new MemoryStream(); - var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine); + await AppendResource(memoryStream, "thirdparty/jquery-2.0.3.min.js", newLineBytes).ConfigureAwait(false); + await AppendResource(memoryStream, "thirdparty/jquerymobile-1.4.0/jquery.mobile-1.4.0.min.js", newLineBytes).ConfigureAwait(false); + + //await AppendResource(memoryStream, "thirdparty/jquery.infinite-scroll-helper.min.js", newLineBytes).ConfigureAwait(false); + var versionString = string.Format("window.dashboardVersion='{0}';", _appHost.ApplicationVersion); var versionBytes = Encoding.UTF8.GetBytes(versionString); await memoryStream.WriteAsync(versionBytes, 0, versionBytes.Length).ConfigureAwait(false); await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); - await AppendResource(memoryStream, "thirdparty/autoNumeric.js", newLineBytes).ConfigureAwait(false); - await AppendResource(memoryStream, "thirdparty/html5slider.js", newLineBytes).ConfigureAwait(false); + await AppendResource(memoryStream, "thirdparty/autonumeric/autoNumeric.min.js", newLineBytes).ConfigureAwait(false); await AppendResource(assembly, memoryStream, "MediaBrowser.WebDashboard.ApiClient.js", newLineBytes).ConfigureAwait(false); @@ -564,6 +591,7 @@ namespace MediaBrowser.WebDashboard.Api var files = new[] { "site.css", + "mediaplayer.css", "librarybrowser.css", "detailtable.css", "posteritem.css", @@ -573,7 +601,9 @@ namespace MediaBrowser.WebDashboard.Api "search.css", "pluginupdates.css", "remotecontrol.css", - "userimage.css" + "userimage.css", + "livetv.css", + "icons.css" }; var memoryStream = new MemoryStream(); diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index 16e2ae3b9..274be16b9 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -389,13 +389,21 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; - self.getLiveTvChannel = function (id) { + self.getLiveTvChannel = function (id, userId) { if (!id) { throw new Error("null id"); } - var url = self.getUrl("LiveTv/Channels/" + id); + var options = { + + }; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("LiveTv/Channels/" + id, options); return self.ajax({ type: "GET", @@ -437,13 +445,55 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; - self.getLiveTvRecording = function (id) { + self.getLiveTvRecordingGroups = function (options) { + + var url = self.getUrl("LiveTv/Recordings/Groups", options || {}); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + }); + }; + + self.getLiveTvRecording = function (id, userId) { if (!id) { throw new Error("null id"); } - var url = self.getUrl("LiveTv/Recordings/" + id); + var options = { + + }; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("LiveTv/Recordings/" + id, options); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + }); + }; + + self.getLiveTvProgram = function (id, userId) { + + if (!id) { + throw new Error("null id"); + } + + var options = { + + }; + + if (userId) { + options.userId = userId; + } + + var url = self.getUrl("LiveTv/Programs/" + id, options); return self.ajax({ type: "GET", @@ -506,20 +556,123 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; - self.createLiveTvTimer = function (options) { + self.getNewLiveTvTimerDefaults = function (options) { - if (!options) { - throw new Error("null options"); + options = options || {}; + + var url = self.getUrl("LiveTv/Timers/Defaults", options); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + }); + }; + + self.createLiveTvTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/Timers"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.updateLiveTvTimer = function (item) { + + if (!item) { + throw new Error("null item"); } - var url = self.getUrl("LiveTv/Timers", options); + var url = self.getUrl("LiveTv/Timers/" + item.Id); return self.ajax({ type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.getLiveTvSeriesTimers = function (options) { + + var url = self.getUrl("LiveTv/SeriesTimers", options || {}); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + }); + }; + + self.getLiveTvSeriesTimer = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/SeriesTimers/" + id); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + }); + }; + + self.cancelLiveTvSeriesTimer = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/SeriesTimers/" + id); + + return self.ajax({ + type: "DELETE", url: url }); }; + self.createLiveTvSeriesTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/SeriesTimers"); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + + self.updateLiveTvSeriesTimer = function (item) { + + if (!item) { + throw new Error("null item"); + } + + var url = self.getUrl("LiveTv/SeriesTimers/" + item.Id); + + return self.ajax({ + type: "POST", + url: url, + data: JSON.stringify(item), + contentType: "application/json" + }); + }; + /** * Gets the current server status */ @@ -1175,10 +1328,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; - url += "/" + name; - url = self.getUrl(url, { - refreshLibrary: refreshLibrary ? true : false + refreshLibrary: refreshLibrary ? true : false, + name: name }); return self.ajax({ @@ -1204,10 +1356,10 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi } options.refreshLibrary = refreshLibrary ? true : false; + options.name = name; var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; - url += "/" + name; url = self.getUrl(url, options); return self.ajax({ @@ -1228,11 +1380,12 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; - url += "/" + name + "/Name"; + url += "/Name"; url = self.getUrl(url, { refreshLibrary: refreshLibrary ? true : false, - newName: newName + newName: newName, + name: name }); return self.ajax({ @@ -1257,11 +1410,12 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; - url += "/" + virtualFolderName + "/Paths"; + url += "/Paths"; url = self.getUrl(url, { refreshLibrary: refreshLibrary ? true : false, - path: mediaPath + path: mediaPath, + name: virtualFolderName }); return self.ajax({ @@ -1286,11 +1440,12 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/VirtualFolders"; - url += "/" + virtualFolderName + "/Paths"; + url += "/Paths"; url = self.getUrl(url, { refreshLibrary: refreshLibrary ? true : false, - path: mediaPath + path: mediaPath, + name: virtualFolderName }); return self.ajax({ diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 35ed3f1e2..e11c8390f 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -9,12 +9,12 @@ <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MediaBrowser.WebDashboard</RootNamespace>
<AssemblyName>MediaBrowser.WebDashboard</AssemblyName>
- <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
<ProductVersion>10.0.0</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@@ -24,6 +24,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@@ -32,21 +33,30 @@ <DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release Mono\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
- <Reference Include="ServiceStack.Interfaces, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
- <SpecificVersion>False</SpecificVersion>
- <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath>
- </Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
+ <Reference Include="ServiceStack.Interfaces">
+ <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath>
+ </Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs">
@@ -75,18 +85,78 @@ </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ApiClient.js" />
- <Content Include="dashboard-ui\css\images\editor.png">
+ <Content Include="dashboard-ui\appsplayback.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\icons.css">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\clients\xbmc.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\audiocd.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\filter.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\mute.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\nexttrack.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\pause.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\play.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\previoustrack.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\remote.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\sort.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\stop.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\subtitles.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\volumedown.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\icons\volumeup.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\css\images\items\detail\tv.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\css\livetv.css">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\mediaplayer.css">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\livetvchannel.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\livetvnewrecording.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\livetvprogram.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\livetvrecording.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\livetvseriestimer.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\livetvtimers.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -120,9 +190,6 @@ <Content Include="dashboard-ui\css\images\editor\missingtrailer.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\remote.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\css\metadataeditor.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -135,9 +202,6 @@ <Content Include="dashboard-ui\css\detailtable.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\bgflip.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\css\images\clients\chrome.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -159,19 +223,19 @@ <Content Include="dashboard-ui\css\images\fresh.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\items\searchhints\film.png">
+ <Content Include="dashboard-ui\css\images\items\searchhintsv2\film.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\items\searchhints\game.png">
+ <Content Include="dashboard-ui\css\images\items\searchhintsv2\game.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\items\searchhints\music.png">
+ <Content Include="dashboard-ui\css\images\items\searchhintsv2\music.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\items\searchhints\person.png">
+ <Content Include="dashboard-ui\css\images\items\searchhintsv2\person.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\items\searchhints\tv.png">
+ <Content Include="dashboard-ui\css\images\items\searchhintsv2\tv.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\css\images\media\audioflyout.png">
@@ -216,9 +280,6 @@ <Content Include="dashboard-ui\css\images\rotten.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\css\images\searchbutton.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\css\images\currentuserdefaultblack.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -342,15 +403,30 @@ <Content Include="dashboard-ui\livetvrecordings.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\appsplayback.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\scripts\librarymenu.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\livetvchannel.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\livetvtimer.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\livetvnewrecording.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\scripts\livetvprogram.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\livetvrecording.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\livetvseriestimer.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\livetvtimer.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -375,24 +451,663 @@ <Content Include="dashboard-ui\scripts\episodes.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\livetvseriestimers.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\tvupcoming.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\userparentalcontrol.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\usersettings.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\wizardimagesettings.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\wizardservice.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\wizardsettings.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\livetvseriestimers.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\autonumeric\autoNumeric.min.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquery-2.0.3.min.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquery.infinite-scroll-helper.min.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\ajax-loader.gif">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\action-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\action-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\alert-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\alert-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-d-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-d-l-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-d-l-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-d-r-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-d-r-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-d-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-l-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-l-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-r-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-r-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-u-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-u-l-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-u-l-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-u-r-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-u-r-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\arrow-u-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\audio-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\audio-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\back-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\back-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\bars-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\bars-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\bullets-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\bullets-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\calendar-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\calendar-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\camera-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\camera-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-d-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-d-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-l-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-l-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-r-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-r-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-u-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\carat-u-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\check-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\check-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\clock-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\clock-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\cloud-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\cloud-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\comment-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\comment-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\delete-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\delete-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\edit-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\edit-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\eye-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\eye-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\forbidden-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\forbidden-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\forward-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\forward-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\gear-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\gear-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\grid-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\grid-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\heart-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\heart-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\home-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\home-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\info-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\info-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\location-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\location-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\lock-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\lock-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\mail-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\mail-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\minus-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\minus-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\navigation-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\navigation-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\phone-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\phone-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\plus-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\plus-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\power-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\power-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\recycle-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\recycle-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\refresh-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\refresh-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\search-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\search-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\shop-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\shop-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\star-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\star-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\tag-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\tag-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\user-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\user-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\video-black.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-png\video-white.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\action-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\action-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\alert-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\alert-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-d-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-d-l-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-d-l-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-d-r-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-d-r-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-d-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-l-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-l-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-r-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-r-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-u-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-u-l-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-u-l-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-u-r-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-u-r-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\arrow-u-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\audio-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\audio-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\back-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\back-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\bars-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\bars-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\bullets-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\bullets-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\calendar-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\calendar-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\camera-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\camera-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-d-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-d-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-l-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-l-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-r-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-r-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-u-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\carat-u-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\check-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\check-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\clock-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\clock-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\cloud-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\cloud-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\comment-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\comment-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\delete-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\delete-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\edit-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\edit-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\eye-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\eye-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\forbidden-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\forbidden-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\forward-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\forward-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\gear-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\gear-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\grid-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\grid-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\heart-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\heart-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\home-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\home-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\info-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\info-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\location-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\location-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\lock-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\lock-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\mail-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\mail-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\minus-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\minus-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\navigation-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\navigation-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\phone-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\phone-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\plus-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\plus-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\power-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\power-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\recycle-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\recycle-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\refresh-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\refresh-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\search-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\search-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\shop-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\shop-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\star-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\star-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\tag-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\tag-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\user-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\user-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\video-black.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\images\icons-svg\video-white.svg">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\jquery.mobile-1.4.0.min.css">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.0\jquery.mobile-1.4.0.min.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\thirdparty\jstree1.0\jquery.jstree.min.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\tvupcoming.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\userparentalcontrol.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\usersettings.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\wizardimagesettings.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\wizardservice.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -596,10 +1311,7 @@ <Content Include="dashboard-ui\scripts\tvstudios.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\thirdparty\autoNumeric.js">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\html5slider.js">
+ <Content Include="dashboard-ui\thirdparty\autonumeric\autoNumeric.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\thirdparty\jstree1.0\jquery.jstree.js">
@@ -712,12 +1424,12 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\clientsettings.html">
+ <Content Include="dashboard-ui\appsweather.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\edituser.html">
+ <Content Include="dashboard-ui\useredit.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -743,7 +1455,7 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\updatepassword.html">
+ <Content Include="dashboard-ui\userpassword.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -763,11 +1475,6 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\css\images\toolswhite.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
<Content Include="dashboard-ui\css\images\rightarrow.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -783,7 +1490,7 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\scripts\clientsettings.js">
+ <Content Include="dashboard-ui\scripts\appsweather.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -803,7 +1510,7 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\scripts\updatepasswordpage.js">
+ <Content Include="dashboard-ui\scripts\userpassword.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -811,7 +1518,7 @@ <Content Include="dashboard-ui\scripts\advancedconfigurationpage.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\scripts\edituserpage.js">
+ <Content Include="dashboard-ui\scripts\useredit.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\userimagepage.js">
@@ -822,12 +1529,12 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\advancedmetadata.html">
+ <Content Include="dashboard-ui\metadataadvanced.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\scripts\advancedmetadataconfigurationpage.js">
+ <Content Include="dashboard-ui\scripts\metadataadvanced.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -850,11 +1557,6 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\css\images\bg.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
<Content Include="dashboard-ui\plugincatalog.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -863,21 +1565,6 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\css\images\leftarrowblack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
- <Content Include="dashboard-ui\css\images\leftarrowwhite.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
- <Content Include="dashboard-ui\css\images\toolsblack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
<Content Include="dashboard-ui\scheduledtasks.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -1037,86 +1724,11 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\faicons-v2.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\faicons.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\images\ajax-loader.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\images\icons-18-black-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\images\icons-18-white-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\images\icons-36-black-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\images\icons-36-white-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\index.html">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\jqm-icon-pack-3.0.0-fa.css">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\images\ajax-loader.gif">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\images\ajax-loader.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\images\icons-18-black-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\images\icons-18-white-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\images\icons-36-black-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\images\icons-36-white-pack.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\index.html">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\original\jqm-icon-pack-2.0-original.css">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\font\fontawesome-webfont.eot">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\font\fontawesome-webfont.ttf">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\font\fontawesome-webfont.woff">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\font\FontAwesome.otf">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jqm-icon-pack-3.0\font-awesome\jqm-icon-pack-3.0.0-fa.scss">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
<Content Include="dashboard-ui\css\images\supporter\registerpaypal.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\css\images\home.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- </ItemGroup>
- <ItemGroup>
<Content Include="dashboard-ui\css\images\notifications\done.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -1210,14 +1822,27 @@ </Content>
</ItemGroup>
<ItemGroup>
- <EmbeddedResource Include="packages.config" />
+ <None Include="dashboard-ui\css\fonts\OpenSans-ExtraBold.woff">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ <None Include="dashboard-ui\css\fonts\OpenSans-Bold.woff">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ <None Include="dashboard-ui\css\fonts\OpenSans.woff">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ <None Include="dashboard-ui\css\fonts\OpenSans-Light.woff">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ <None Include="packages.config" />
</ItemGroup>
+ <ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>
</PostBuildEvent>
</PropertyGroup>
- <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
+ <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 9cbd2cd59..4cc0c4d55 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="MediaBrowser.ApiClient.Javascript" version="3.0.204" targetFramework="net45" /> + <package id="MediaBrowser.ApiClient.Javascript" version="3.0.213" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 1531534db..cdbd51d84 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common.Internal</id> - <version>3.0.262</version> + <version>3.0.293</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,13 +12,14 @@ <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.262" /> + <dependency id="MediaBrowser.Common" version="3.0.293" /> <dependency id="NLog" version="2.1.0" /> - <dependency id="SimpleInjector" version="2.3.6" /> + <dependency id="SimpleInjector" version="2.4.0" /> <dependency id="sharpcompress" version="0.10.2" /> </dependencies> </metadata> <files> <file src="dlls\MediaBrowser.Common.Implementations.dll" target="lib\net45\MediaBrowser.Common.Implementations.dll" /> + <file src="..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll" target="lib\net45\ServiceStack.Text.dll" /> </files> </package>
\ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index d8f155a8a..20bdf1300 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common</id> - <version>3.0.262</version> + <version>3.0.293</version> <title>MediaBrowser.Common</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index e8a63a36a..8f040c00f 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>MediaBrowser.Server.Core</id> - <version>3.0.262</version> + <version>3.0.293</version> <title>Media Browser.Server.Core</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains core components required to build plugins for Media Browser Server.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.262" /> + <dependency id="MediaBrowser.Common" version="3.0.293" /> </dependencies> </metadata> <files> @@ -11,7 +11,7 @@ We have several client apps released and in production: - Html5 - [iOS](https://itunes.apple.com/us/app/media-browser-for-ios/id705058087 "iOS") - [Media Portal](http://www.team-mediaportal.com/ "Media Portal") -- Roku +- [Roku](http://www.roku.com/channels/#!details/34503/media-browser "Roku") - Windows 7/8 Desktop - Windows Media Center - [Windows Phone](http://www.windowsphone.com/s?appid=f4971ed9-f651-4bf6-84bb-94fd98613b86 "Windows Phone") @@ -36,8 +36,8 @@ http://mediabrowser3.com/community ## Current Versions ## -Release: 3.0.4999.38224<br/> -Beta: 3.0.5028.39800<br/> +Release: 3.0.5099.2102<br/> +Beta: 3.0.5097.16641<br/> ## Images |
