diff options
190 files changed, 5350 insertions, 2270 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 018d8a92e..785cc395c 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -29,12 +29,16 @@ namespace MediaBrowser.Api /// <value>The logger.</value> 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> + /// <param name="appPaths">The application paths.</param> public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths) { Logger = logger; @@ -52,6 +56,10 @@ namespace MediaBrowser.Api { DeleteEncodedMediaCache(); } + catch (DirectoryNotFoundException) + { + // Don't clutter the log + } catch (IOException ex) { Logger.ErrorException("Error deleting encoded media cache", ex); @@ -88,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) 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 ba6809f4d..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,71 +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) - { - Directory.CreateDirectory(_appPaths.TempDirectory); - - 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> @@ -837,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); @@ -926,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; @@ -941,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 17a87a861..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); @@ -311,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; diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 843e39f78..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); } 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 979088ee0..e2280cdc8 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -73,6 +73,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>> @@ -92,6 +100,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 @@ -114,7 +134,13 @@ namespace MediaBrowser.Api.LiveTv { } - [Route("/LiveTv/Timers/{Id}", "GET")] + [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> { @@ -128,6 +154,33 @@ namespace MediaBrowser.Api.LiveTv { } + [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; } + } + public class LiveTvService : BaseApiService { private readonly ILiveTvManager _liveTvManager; @@ -252,7 +305,7 @@ namespace MediaBrowser.Api.LiveTv public object Get(GetSeriesTimers request) { var result = _liveTvManager.GetSeriesTimers(new SeriesTimerQuery - { + { }, CancellationToken.None).Result; @@ -265,5 +318,65 @@ namespace MediaBrowser.Api.LiveTv 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); + } } }
\ No newline at end of file diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index bae3716bc..0732ee00c 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -90,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" /> diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 90996296d..c04648c37 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,11 @@ 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"); // 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 +755,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 +788,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 +805,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 +853,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 +866,62 @@ 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 + { + 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 +946,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/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 9d335d2d3..99772d2ec 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) { } @@ -123,7 +124,7 @@ 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); 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..31dbcaafb 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=1000{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/UserService.cs b/MediaBrowser.Api/UserService.cs index a64152246..254fa6ff1 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -269,10 +269,12 @@ namespace MediaBrowser.Api /// Posts the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(AuthenticateUser request) + public object Post(AuthenticateUser request) { // No response needed. Will throw an exception on failure. var result = AuthenticateUser(request).Result; + + return result; } public object Post(AuthenticateUserByName request) diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs index b5317319f..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) { - ValidateParams(options.Url, options.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); 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) - { - 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) + public Task<HttpResponseInfo> Post(HttpRequestOptions options) { - 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); + options.RequestContent = postContent; + options.RequestContentType = "application/x-www-form-urlencoded"; - try - { - options.CancellationToken.ThrowIfCancellationRequested(); + var response = await Post(options).ConfigureAwait(false); - 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; - - 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,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options) { - ValidateParams(options.Url, options.CancellationToken); + ValidateParams(options); Directory.CreateDirectory(_appPaths.TempDirectory); @@ -592,7 +556,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager { return new HttpException(ex.Message, ex); } - + return ex; } @@ -608,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/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/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/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.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..a02369b2c 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> @@ -1310,31 +1381,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 +1395,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 +1467,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 +1641,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 +1655,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 +1704,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/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8ef86e54e..fed206a30 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; @@ -365,123 +364,125 @@ 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)) + { + var currentChildLocationType = currentChild.LocationType; + if (currentChildLocationType != LocationType.Remote && + currentChildLocationType != LocationType.Virtual) + { + EntityResolutionHelper.EnsureDates(FileSystem, currentChild, child.ResolveArgs, false); + } + + validChildren.Add(new Tuple<BaseItem, bool>(currentChild, true)); + } + else { - EntityResolutionHelper.EnsureDates(FileSystem, currentChild, child.ResolveArgs, false); + 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); + await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false); - AddChildrenInternal(newItems); + AddChildrenInternal(newItems); - await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); + 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); 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/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 7f94ab8e8..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 { @@ -292,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 5727b316b..2d781118e 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using System; @@ -260,5 +261,11 @@ namespace MediaBrowser.Controller.Entities.TV { 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 1565de4f8..f7e78ccd4 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using System; @@ -13,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>(); @@ -183,7 +190,9 @@ namespace MediaBrowser.Controller.Entities.TV episodes = episodes.Where(i => !i.IsVirtualUnaired); } - return LibraryManager.Sort(episodes, user, new[] { ItemSortBy.AiredEpisodeOrder }, SortOrder.Ascending) + var sortBy = seasonNumber == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder; + + return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending) .Cast<Episode>(); } @@ -215,5 +224,12 @@ namespace MediaBrowser.Controller.Entities.TV 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/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/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 7186cfaf3..000000000 --- a/MediaBrowser.Controller/LiveTv/Channel.cs +++ /dev/null @@ -1,75 +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; } - - public bool? HasProviderImage { 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 bb0636673..cdc9c76c8 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -32,9 +32,22 @@ namespace MediaBrowser.Controller.LiveTv public ChannelType ChannelType { get; set; } /// <summary> - /// Set this value to true or false if it is known via channel info whether there is an image or not. - /// Leave it null if the only way to determine is by requesting the image and handling the failure. + /// 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 9ed7b633d..1d98dc7cf 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities; +using System.IO; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using System.Collections.Generic; @@ -25,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> @@ -46,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> @@ -122,9 +138,41 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <param name="id">The identifier.</param> /// <returns>Channel.</returns> - Channel GetChannel(string id); + LiveTvChannel GetInternalChannel(string id); + + /// <summary> + /// Gets the internal program. + /// </summary> + /// <param name="id">The identifier.</param> + /// <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 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> @@ -147,5 +195,21 @@ namespace MediaBrowser.Controller.LiveTv /// <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); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index 8b1801f9e..31dbd8e99 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -31,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> @@ -71,28 +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. + /// 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="channelId">The channel identifier.</param> + /// <param name="recordingId">The recording identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{ImageResponseInfo}.</returns> - Task<ImageResponseInfo> GetRecordingImageAsync(string channelId, CancellationToken cancellationToken); + 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 channelId, CancellationToken cancellationToken); + Task<StreamResponseInfo> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken); /// <summary> /// Gets the recordings asynchronous. @@ -109,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> @@ -122,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="recordingId">The recording identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Stream}.</returns> + Task<StreamResponseInfo> GetChannelStream(string recordingId, 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 ce7a4a598..2c7b40415 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -92,11 +92,65 @@ namespace MediaBrowser.Controller.LiveTv public string EpisodeTitle { get; set; } /// <summary> - /// Set this value to true or false if it is known via program info whether there is an image or not. - /// Leave it null if the only way to determine is by requesting the image and handling the failure. + /// 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 4fc8c0f7a..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> @@ -108,11 +157,24 @@ namespace MediaBrowser.Controller.LiveTv public float? CommunityRating { get; set; } /// <summary> - /// Set this value to true or false if it is known via recording info whether there is an image or not. - /// Leave it null if the only way to determine is by requesting the image and handling the failure. + /// 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 607282796..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 @@ -17,11 +16,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> @@ -48,12 +42,24 @@ namespace MediaBrowser.Controller.LiveTv public DateTime EndDate { get; set; } /// <summary> - /// Gets or sets the type of the recurrence. + /// Gets or sets a value indicating whether [record any time]. /// </summary> - /// <value>The type of the recurrence.</value> - public RecurrenceType RecurrenceType { get; set; } + /// <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> @@ -66,28 +72,28 @@ namespace MediaBrowser.Controller.LiveTv public int Priority { 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>The required post padding seconds.</value> - public int RequiredPostPaddingSeconds { get; set; } + /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value> + public bool IsPostPaddingRequired { get; set; } public SeriesTimerInfo() { 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 786858e09..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> @@ -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 df108b590..0c5c0a5cd 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -85,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" /> @@ -94,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" /> @@ -106,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" /> @@ -188,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" /> 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 c9f57a927..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; 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 c89dcf4a5..afdabbf44 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> 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 78f970969..7937bc403 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> 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 0690de5ce..9adfcfa99 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -36,6 +36,9 @@ namespace MediaBrowser.Model.Dto 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> @@ -88,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> @@ -210,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 b3542fcf8..8b8b0cd07 100644 --- a/MediaBrowser.Model/LiveTv/ProgramInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/ProgramInfoDto.cs @@ -1,6 +1,7 @@ -using System; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; using System.Collections.Generic; -using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.LiveTv { @@ -12,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> @@ -24,6 +37,12 @@ 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> @@ -103,20 +122,80 @@ namespace MediaBrowser.Model.LiveTv 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 { - Unspecified, Mono, Stereo, Dolby, diff --git a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs index d17ebee6d..3cf44f5c4 100644 --- a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.LiveTv { @@ -12,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> @@ -34,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; } @@ -45,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; } @@ -83,10 +102,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. @@ -125,14 +144,69 @@ namespace MediaBrowser.Model.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 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 3aa94ff83..20a7074a5 100644 --- a/MediaBrowser.Model/LiveTv/RecordingQuery.cs +++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs @@ -16,6 +16,12 @@ /// </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; } } public class TimerQuery diff --git a/MediaBrowser.Model/LiveTv/RecordingStatus.cs b/MediaBrowser.Model/LiveTv/RecordingStatus.cs index 06bc98e63..95e9dcb01 100644 --- a/MediaBrowser.Model/LiveTv/RecordingStatus.cs +++ b/MediaBrowser.Model/LiveTv/RecordingStatus.cs @@ -14,15 +14,6 @@ namespace MediaBrowser.Model.LiveTv Error } - public enum RecurrenceType - { - Manual, - NewProgramEventsOneChannel, - AllProgramEventsOneChannel, - NewProgramEventsAllChannels, - AllProgramEventsAllChannels - } - public enum DayPattern { Daily, diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs index 3862c0745..a8c6a2e37 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -22,6 +22,12 @@ namespace MediaBrowser.Model.LiveTv 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> @@ -65,10 +71,22 @@ namespace MediaBrowser.Model.LiveTv public DateTime EndDate { get; set; } /// <summary> - /// Gets or sets the type of the recurrence. + /// 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>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. @@ -89,28 +107,28 @@ namespace MediaBrowser.Model.LiveTv public int Priority { 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>The required post padding seconds.</value> - public int RequiredPostPaddingSeconds { get; set; } + /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value> + public bool IsPostPaddingRequired { get; set; } public SeriesTimerInfoDto() { diff --git a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs index 6b7ab42d3..507ba0947 100644 --- a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs @@ -32,12 +32,24 @@ 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> /// 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; } @@ -74,35 +86,47 @@ namespace MediaBrowser.Model.LiveTv /// </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 the requested pre padding seconds. + /// 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 071f5bdde..bb6fedb9f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -12,7 +12,7 @@ <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> @@ -118,7 +118,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" /> @@ -209,8 +209,9 @@ <PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' ( xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\net45\" /y /d /r /i )</PostBuildEvent> + <PostBuildEvent Condition=" '$(ConfigurationName)' == 'Release Mono' ">cp -fu "$(MSBuildProjectDirectory)\..\packages\PropertyChanged.Fody.1.41.0.0\PropertyChanged.Fody.dll" "$(MSBuildProjectDirectory)\..\Tools\Fody\"</PostBuildEvent> </PropertyGroup> - <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> + <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/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/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.userprefs b/MediaBrowser.Mono.userprefs index fa9dbfdc2..4a043b8b8 100644 --- a/MediaBrowser.Mono.userprefs +++ b/MediaBrowser.Mono.userprefs @@ -2,7 +2,7 @@ <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="15" /> + <File FileName="MediaBrowser.Server.Mono\app.config" Line="14" Column="17" /> <File FileName="MediaBrowser.Server.Mono\FFMpeg\FFMpegDownloadInfo.cs" Line="1" Column="1" /> <File FileName="MediaBrowser.Server.Mono\Program.cs" Line="1" Column="1" /> <File FileName="MediaBrowser.Server.Implementations\Library\UserManager.cs" Line="1" Column="1" /> diff --git a/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs index 65e8afd7a..0b6accf33 100644 --- a/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs +++ b/MediaBrowser.Providers/ImageFromMediaLocationProvider.cs @@ -212,7 +212,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Logo, image.FullName); + item.SetImagePath(ImageType.Logo, image.FullName); } // Clearart @@ -220,7 +220,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Art, image.FullName); + item.SetImagePath(ImageType.Art, image.FullName); } // Disc @@ -229,7 +229,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Disc, image.FullName); + item.SetImagePath(ImageType.Disc, image.FullName); } // Box Image @@ -237,7 +237,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Box, image.FullName); + item.SetImagePath(ImageType.Box, image.FullName); } // BoxRear Image @@ -245,7 +245,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.BoxRear, image.FullName); + item.SetImagePath(ImageType.BoxRear, image.FullName); } // Thumbnail Image @@ -253,7 +253,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Menu, image.FullName); + item.SetImagePath(ImageType.Menu, image.FullName); } PopulateBanner(item, args); @@ -311,7 +311,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Primary, image.FullName); + item.SetImagePath(ImageType.Primary, image.FullName); } } @@ -339,7 +339,7 @@ namespace MediaBrowser.Providers if (image != null) { - item.SetImage(ImageType.Banner, image.FullName); + item.SetImagePath(ImageType.Banner, image.FullName); } } @@ -351,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) { @@ -367,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/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 2f35cb003..272274679 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -86,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" /> @@ -134,6 +133,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" /> @@ -146,6 +146,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"> 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..a2a4d5bbf 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; 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 952d7e218..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) { 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 a038d9b4c..c6c9df6f7 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); @@ -318,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; @@ -502,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)) { @@ -554,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"); + var dataFilePath = GetDataFilePath(isBoxSet, id, preferredMetadataLanguage); - Directory.CreateDirectory(dataPath); - - JsonSerializer.SerializeToFile(mainResult, dataFilePath); - - // Now get the language-less version - mainResult = await FetchMainResult(id, isBoxSet, null, cancellationToken).ConfigureAwait(false); - - dataFilePath = Path.Combine(dataPath, "default.json"); + Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); JsonSerializer.SerializeToFile(mainResult, dataFilePath); } @@ -593,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> @@ -633,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; @@ -745,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) @@ -811,7 +788,7 @@ namespace MediaBrowser.Providers.Movies } } - //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; @@ -838,15 +815,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/MovieUpdatesPrescanTask.cs b/MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs index 46e947c70..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; @@ -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 2e072b098..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); diff --git a/MediaBrowser.Providers/Music/FanArtArtistProvider.cs b/MediaBrowser.Providers/Music/FanArtArtistProvider.cs index 57a191ab1..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; + } } } } 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/XmlSaverHelpers.cs b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs index ac8270dfe..dc2d5eddd 100644 --- a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs @@ -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) 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/FanArtSeasonProvider.cs b/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs index 713a95b26..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); } diff --git a/MediaBrowser.Providers/TV/FanArtTVProvider.cs b/MediaBrowser.Providers/TV/FanArtTVProvider.cs index 90af81ec3..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) 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 56b06f490..b889b991e 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; 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 c5e5327c3..17ed6b5a2 100644 --- a/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs @@ -159,17 +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)) { - await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); - } + if (!item.HasImage(ImageType.Primary)) + { + await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false); + } - if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) - { - await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false); + if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) + { + 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)) { 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/Drawing/ImageProcessor.cs b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs index 36b6e5a90..7ddf63cf8 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs @@ -228,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)) { @@ -594,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) { @@ -623,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) { @@ -660,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(); @@ -673,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) { @@ -709,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)) { @@ -782,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; @@ -900,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 14496d362..932c7ba3f 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -243,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) @@ -738,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; @@ -818,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)); } } @@ -831,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)); } } @@ -844,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)); } } @@ -1037,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; diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 791beb941..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> diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index e335f4ad5..e54c0af55 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -179,7 +179,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"); @@ -268,7 +268,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; @@ -278,25 +280,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; } @@ -304,18 +307,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 4a8b2d638..d04ebe32d 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -1,4 +1,5 @@ using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -21,18 +22,20 @@ 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, IFileSystem fileSystem) + 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) @@ -48,43 +51,93 @@ namespace MediaBrowser.Server.Implementations.LiveTv return true; } - var channel = (Channel)item; + var changed = true; - if (channel.HasProviderImage ?? true) + try { - try - { - await DownloadImage(item, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) + changed = await DownloadImage((LiveTvChannel)item, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + // Don't fail the provider on a 404 + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) { - // Don't fail the provider on a 404 - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } + throw; } } - SetLastRefreshed(item, DateTime.UtcNow, providerInfo); - return true; + if (changed) + { + SetLastRefreshed(item, DateTime.UtcNow, providerInfo); + } + + 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 service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, channel.ServiceName, StringComparison.OrdinalIgnoreCase)); + var response = await _httpClient.GetResponse(options).ConfigureAwait(false); - if (service != null) + 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 (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 index f93821cc9..6552c6892 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -8,6 +8,9 @@ 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 { @@ -27,108 +30,151 @@ namespace MediaBrowser.Server.Implementations.LiveTv _logger = logger; } - public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service) + public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel) { var dto = new TimerInfoDto { Id = GetInternalTimerId(service.Name, info.Id).ToString("N"), - ChannelName = info.ChannelName, Overview = info.Overview, EndDate = info.EndDate, Name = info.Name, StartDate = info.StartDate, ExternalId = info.Id, - ChannelId = GetInternalChannelId(service.Name, info.ChannelId, info.ChannelName).ToString("N"), + ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"), Status = info.Status, SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"), - RequestedPostPaddingSeconds = info.RequestedPostPaddingSeconds, - RequestedPrePaddingSeconds = info.RequestedPrePaddingSeconds, - RequiredPostPaddingSeconds = info.RequiredPostPaddingSeconds, - RequiredPrePaddingSeconds = info.RequiredPrePaddingSeconds, + PrePaddingSeconds = info.PrePaddingSeconds, + PostPaddingSeconds = info.PostPaddingSeconds, + IsPostPaddingRequired = info.IsPostPaddingRequired, + IsPrePaddingRequired = info.IsPrePaddingRequired, ExternalChannelId = info.ChannelId, - ExternalSeriesTimerId = info.SeriesTimerId + ExternalSeriesTimerId = info.SeriesTimerId, + ServiceName = service.Name, + ExternalProgramId = info.ProgramId, + Priority = info.Priority, + RunTimeTicks = (info.EndDate - info.StartDate).Ticks }; - var duration = info.EndDate - info.StartDate; - dto.DurationMs = Convert.ToInt32(duration.TotalMilliseconds); - 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) + public SeriesTimerInfoDto GetSeriesTimerInfoDto(SeriesTimerInfo info, ILiveTvService service, string channelName) { var dto = new SeriesTimerInfoDto { Id = GetInternalSeriesTimerId(service.Name, info.Id).ToString("N"), - ChannelName = info.ChannelName, Overview = info.Overview, EndDate = info.EndDate, Name = info.Name, StartDate = info.StartDate, ExternalId = info.Id, - ChannelId = GetInternalChannelId(service.Name, info.ChannelId, info.ChannelName).ToString("N"), - RequestedPostPaddingSeconds = info.RequestedPostPaddingSeconds, - RequestedPrePaddingSeconds = info.RequestedPrePaddingSeconds, - RequiredPostPaddingSeconds = info.RequiredPostPaddingSeconds, - RequiredPrePaddingSeconds = info.RequiredPrePaddingSeconds, + PrePaddingSeconds = info.PrePaddingSeconds, + PostPaddingSeconds = info.PostPaddingSeconds, + IsPostPaddingRequired = info.IsPostPaddingRequired, + IsPrePaddingRequired = info.IsPrePaddingRequired, Days = info.Days, Priority = info.Priority, - RecurrenceType = info.RecurrenceType, + RecordAnyChannel = info.RecordAnyChannel, + RecordAnyTime = info.RecordAnyTime, + RecordNewOnly = info.RecordNewOnly, ExternalChannelId = info.ChannelId, - ExternalProgramId = info.ProgramId + 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 (info.Days != null && info.Days.Count > 0) + if (days.Count > 0) { - if (info.Days.Count == 7) + if (days.Count == 7) { pattern = DayPattern.Daily; } - else if (info.Days.Count == 2) + else if (days.Count == 2) { - if (info.Days.Contains(DayOfWeek.Saturday) && info.Days.Contains(DayOfWeek.Sunday)) + if (days.Contains(DayOfWeek.Saturday) && days.Contains(DayOfWeek.Sunday)) { pattern = DayPattern.Weekends; } } - else if (info.Days.Count == 5) + else if (days.Count == 5) { - if (info.Days.Contains(DayOfWeek.Monday) && info.Days.Contains(DayOfWeek.Tuesday) && info.Days.Contains(DayOfWeek.Wednesday) && info.Days.Contains(DayOfWeek.Thursday) && info.Days.Contains(DayOfWeek.Friday)) + if (days.Contains(DayOfWeek.Monday) && days.Contains(DayOfWeek.Tuesday) && days.Contains(DayOfWeek.Wednesday) && days.Contains(DayOfWeek.Thursday) && days.Contains(DayOfWeek.Friday)) { pattern = DayPattern.Weekdays; } } } - dto.DayPattern = pattern; + return pattern; + } - return dto; + /// <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 RecordingInfoDto GetRecordingInfoDto(RecordingInfo info, ILiveTvService service, User user = null) + 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"), - ChannelName = info.ChannelName, + 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, info.ChannelName).ToString("N"), + ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"), Status = info.Status, Path = info.Path, Genres = info.Genres, @@ -136,25 +182,44 @@ namespace MediaBrowser.Server.Implementations.LiveTv EpisodeTitle = info.EpisodeTitle, ChannelType = info.ChannelType, MediaType = info.ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video, - CommunityRating = info.CommunityRating, + CommunityRating = GetClientCommunityRating(info.CommunityRating), OfficialRating = info.OfficialRating, Audio = info.Audio, - IsHD = info.IsHD + 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 }; - if (user != null) + var imageTag = GetImageTag(recording); + + if (imageTag.HasValue) { - //dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey())); + dto.ImageTags[ImageType.Primary] = imageTag.Value; } - var duration = info.EndDate - info.StartDate; - dto.DurationMs = Convert.ToInt32(duration.TotalMilliseconds); + 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; } @@ -164,17 +229,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv /// <param name="info">The info.</param> /// <param name="user">The user.</param> /// <returns>ChannelInfoDto.</returns> - public ChannelInfoDto GetChannelInfoDto(Channel info, User user = null) + public ChannelInfoDto GetChannelInfoDto(LiveTvChannel info, User user = null) { + var channelInfo = info.ChannelInfo; + var dto = new ChannelInfoDto { Name = info.Name, ServiceName = info.ServiceName, - ChannelType = info.ChannelType, - Number = info.ChannelNumber, - Type = info.GetType().Name, + ChannelType = channelInfo.ChannelType, + Number = channelInfo.Number, + Type = info.GetClientTypeName(), Id = info.Id.ToString("N"), - MediaType = info.MediaType + MediaType = info.MediaType, + ExternalId = channelInfo.Id }; if (user != null) @@ -182,7 +250,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey())); } - var imageTag = GetLogoImageTag(info); + var imageTag = GetImageTag(info); if (imageTag.HasValue) { @@ -192,38 +260,49 @@ namespace MediaBrowser.Server.Implementations.LiveTv return dto; } - public ProgramInfoDto GetProgramInfoDto(ProgramInfo program, Channel channel, User user = null) + public ProgramInfoDto GetProgramInfoDto(LiveTvProgram item, string channelName, User user = null) { + var program = item.ProgramInfo; + var dto = new ProgramInfoDto { - Id = GetInternalProgramId(channel.ServiceName, program.Id).ToString("N"), - ChannelId = channel.Id.ToString("N"), + 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 = channel.ServiceName, + ServiceName = item.ServiceName, StartDate = program.StartDate, OfficialRating = program.OfficialRating, IsHD = program.IsHD, OriginalAirDate = program.OriginalAirDate, Audio = program.Audio, - CommunityRating = program.CommunityRating, + CommunityRating = GetClientCommunityRating(program.CommunityRating), AspectRatio = program.AspectRatio, IsRepeat = program.IsRepeat, - EpisodeTitle = program.EpisodeTitle + 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 }; if (user != null) { - //dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, info.GetUserDataKey())); + dto.UserData = _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(user.Id, item.GetUserDataKey())); } return dto; } - private Guid? GetLogoImageTag(Channel info) + private Guid? GetImageTag(BaseItem info) { var path = info.PrimaryImagePath; @@ -238,17 +317,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.ErrorException("Error getting channel image info for {0}", ex, info.Name); + _logger.ErrorException("Error getting image info for {0}", ex, info.Name); } return null; } - public Guid GetInternalChannelId(string serviceName, string externalId, string channelName) + public Guid GetInternalChannelId(string serviceName, string externalId) { - var name = serviceName + externalId + channelName; + var name = serviceName + externalId; - return name.ToLower().GetMBId(typeof(Channel)); + return name.ToLower().GetMBId(typeof(LiveTvChannel)); } public Guid GetInternalTimerId(string serviceName, string externalId) @@ -279,46 +358,118 @@ namespace MediaBrowser.Server.Implementations.LiveTv return name.ToLower().GetMD5(); } - public TimerInfo GetTimerInfo(TimerInfoDto dto) + public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, ILiveTvManager liveTv, CancellationToken cancellationToken) { - return new TimerInfo + var info = new TimerInfo { - Id = dto.ExternalId, - ChannelName = dto.ChannelName, Overview = dto.Overview, EndDate = dto.EndDate, Name = dto.Name, StartDate = dto.StartDate, - ChannelId = dto.ExternalChannelId, Status = dto.Status, + PrePaddingSeconds = dto.PrePaddingSeconds, + PostPaddingSeconds = dto.PostPaddingSeconds, + IsPostPaddingRequired = dto.IsPostPaddingRequired, + IsPrePaddingRequired = dto.IsPrePaddingRequired, + Priority = dto.Priority, SeriesTimerId = dto.ExternalSeriesTimerId, - RequestedPostPaddingSeconds = dto.RequestedPostPaddingSeconds, - RequestedPrePaddingSeconds = dto.RequestedPrePaddingSeconds, - RequiredPostPaddingSeconds = dto.RequiredPostPaddingSeconds, - RequiredPrePaddingSeconds = dto.RequiredPrePaddingSeconds + 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 SeriesTimerInfo GetSeriesTimerInfo(SeriesTimerInfoDto dto) + public async Task<SeriesTimerInfo> GetSeriesTimerInfo(SeriesTimerInfoDto dto, bool isNew, ILiveTvManager liveTv, CancellationToken cancellationToken) { - return new SeriesTimerInfo + var info = new SeriesTimerInfo { - Id = dto.ExternalId, - ChannelName = dto.ChannelName, Overview = dto.Overview, EndDate = dto.EndDate, Name = dto.Name, StartDate = dto.StartDate, - ChannelId = dto.ExternalChannelId, - RequestedPostPaddingSeconds = dto.RequestedPostPaddingSeconds, - RequestedPrePaddingSeconds = dto.RequestedPrePaddingSeconds, - RequiredPostPaddingSeconds = dto.RequiredPostPaddingSeconds, - RequiredPrePaddingSeconds = dto.RequiredPrePaddingSeconds, + PrePaddingSeconds = dto.PrePaddingSeconds, + PostPaddingSeconds = dto.PostPaddingSeconds, + IsPostPaddingRequired = dto.IsPostPaddingRequired, + IsPrePaddingRequired = dto.IsPrePaddingRequired, Days = dto.Days, Priority = dto.Priority, - RecurrenceType = dto.RecurrenceType, - ProgramId = dto.ExternalProgramId + 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 318f450f1..1d12e2d45 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -36,8 +36,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv 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, ILocalizationManager localization, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager) { @@ -77,18 +77,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv { 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; @@ -100,9 +101,9 @@ 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; @@ -120,14 +121,52 @@ namespace MediaBrowser.Server.Implementations.LiveTv return Task.FromResult(result); } - public Channel GetChannel(string id) + public LiveTvChannel GetInternalChannel(string id) + { + return GetInternalChannel(new Guid(id)); + } + + private LiveTvChannel GetInternalChannel(Guid id) + { + LiveTvChannel channel = null; + + _channels.TryGetValue(id, out channel); + return channel; + } + + public LiveTvProgram GetInternalProgram(string id) { var guid = new Guid(id); - return _channels.FirstOrDefault(i => i.Id == guid); + LiveTvProgram obj = null; + + _programs.TryGetValue(guid, out obj); + return obj; + } + + public async Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken) + { + var service = ActiveService; + + 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 async Task<Channel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken) + public async Task<StreamResponseInfo> GetRecordingStream(string id, CancellationToken cancellationToken) + { + var service = ActiveService; + + var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); + + var recording = recordings.FirstOrDefault(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(id)); + + return await service.GetRecordingStream(recording.Id, cancellationToken).ConfigureAwait(false); + } + + 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)); @@ -148,28 +187,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv isNew = true; } - var id = _tvDtoService.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, - HasProviderImage = channelInfo.HasImage + 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); @@ -178,26 +216,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 => guids.Contains(new Guid(i.ChannelId))); + programs = programs.Where(i => + { + var programChannelId = i.ProgramInfo.ChannelId; + + var internalProgramChannelId = _tvDtoService.GetInternalChannelId(serviceName, programChannelId); + + return guids.Contains(internalProgramChannelId); + }); } - var returnArray = programs.ToArray(); + var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId)); + + if (user != null) + { + programs = programs.Where(i => i.IsParentalAllowed(user, _localization)); + } + + 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) @@ -216,8 +388,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; @@ -229,7 +401,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channelPrograms = await service.GetProgramsAsync(channelInfo.Item2.Id, cancellationToken).ConfigureAwait(false); - programs.AddRange(channelPrograms.Select(program => _tvDtoService.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); } @@ -249,8 +424,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) @@ -262,26 +437,43 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken) { - var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId)); + var service = ActiveService; - var list = new List<RecordingInfoDto>(); + var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId)); - if (ActiveService != null) - { - var recordings = await ActiveService.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); + var list = new List<RecordingInfo>(); - var dtos = recordings.Select(i => _tvDtoService.GetRecordingInfoDto(i, ActiveService, user)); + var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); + list.AddRange(recordings); - list.AddRange(dtos); + if (!string.IsNullOrEmpty(query.ChannelId)) + { + list = list + .Where(i => _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId) == new Guid(query.ChannelId)) + .ToList(); } - if (!string.IsNullOrEmpty(query.ChannelId)) + if (!string.IsNullOrEmpty(query.Id)) { - list = list.Where(i => string.Equals(i.ChannelId, query.ChannelId)) + list = list + .Where(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(query.Id)) .ToList(); } - var returnArray = list.OrderByDescending(i => i.StartDate) + var entities = await GetEntities(list, service.Name, cancellationToken).ConfigureAwait(false); + + if (user != null) + { + entities = entities.Where(i => i.IsParentalAllowed(user, _localization)).ToArray(); + } + + 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); + }) + .OrderByDescending(i => i.StartDate) .ToArray(); return new QueryResult<RecordingInfoDto> @@ -291,17 +483,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)) @@ -312,31 +512,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv return services; } - public Task ScheduleRecording(string programId) - { - throw new NotImplementedException(); - } - public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) { - var list = new List<TimerInfoDto>(); - - if (ActiveService != null) - { - var timers = await ActiveService.GetTimersAsync(cancellationToken).ConfigureAwait(false); - - var dtos = timers.Select(i => _tvDtoService.GetTimerInfoDto(i, ActiveService)); - - list.AddRange(dtos); - } + var service = ActiveService; + var timers = await service.GetTimersAsync(cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(query.ChannelId)) { - list = list.Where(i => string.Equals(i.ChannelId, query.ChannelId)) - .ToList(); + var guid = new Guid(query.ChannelId); + timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId)); } - var returnArray = list.OrderByDescending(i => i.StartDate) + 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> @@ -348,22 +543,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task DeleteRecording(string recordingId) { - var recordings = await GetRecordings(new RecordingQuery - { - - }, CancellationToken.None).ConfigureAwait(false); - - var recording = recordings.Items - .FirstOrDefault(i => string.Equals(recordingId, i.Id, StringComparison.OrdinalIgnoreCase)); + var recording = await GetRecording(recordingId, CancellationToken.None).ConfigureAwait(false); if (recording == null) { throw new ResourceNotFoundException(string.Format("Recording with Id {0} not found", recordingId)); } - var channel = GetChannel(recording.ChannelId); - - var service = GetServices(channel.ServiceName, null) + var service = GetServices(recording.ServiceName, null) .First(); await service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None).ConfigureAwait(false); @@ -371,36 +558,44 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task CancelTimer(string id) { - var timers = await GetTimers(new TimerQuery + var timer = await GetTimer(id, CancellationToken.None).ConfigureAwait(false); + + if (timer == null) { + throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); + } - }, CancellationToken.None).ConfigureAwait(false); + var service = GetServices(timer.ServiceName, null) + .First(); - var timer = timers.Items - .FirstOrDefault(i => string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)); + 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) { throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); } - var channel = GetChannel(timer.ChannelId); - - var service = GetServices(channel.ServiceName, null) + var service = GetServices(timer.ServiceName, null) .First(); - await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); + 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") + UserId = user == null ? null : user.Id.ToString("N"), + Id = id }, cancellationToken).ConfigureAwait(false); - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); + return results.Items.FirstOrDefault(); } public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) @@ -416,53 +611,128 @@ namespace MediaBrowser.Server.Implementations.LiveTv return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); } - - public Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) + + public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) { - var info = _tvDtoService.GetTimerInfo(timer); + 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 ActiveService.UpdateTimerAsync(info, cancellationToken); + return _tvDtoService.GetSeriesTimerInfoDto(i, service, channelName); + + }) + .OrderByDescending(i => i.StartDate) + .ToArray(); + + return new QueryResult<SeriesTimerInfoDto> + { + Items = returnArray, + TotalRecordCount = returnArray.Length + }; } - public Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) + public Task<ChannelInfoDto> GetChannel(string id, CancellationToken cancellationToken, User user = null) { - var info = _tvDtoService.GetSeriesTimerInfo(timer); + var channel = GetInternalChannel(id); - return ActiveService.UpdateSeriesTimerAsync(info, cancellationToken); + var dto = _tvDtoService.GetChannelInfoDto(channel, user); + + return Task.FromResult(dto); } - public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) + public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken) { - var list = new List<SeriesTimerInfoDto>(); + var service = ActiveService; - if (ActiveService != null) - { - var timers = await ActiveService.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); + var info = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); - var dtos = timers.Select(i => _tvDtoService.GetSeriesTimerInfoDto(i, ActiveService)); + var obj = _tvDtoService.GetSeriesTimerInfoDto(info, service, null); - list.AddRange(dtos); - } + obj.Id = obj.ExternalId = string.Empty; - var returnArray = list.OrderByDescending(i => i.StartDate) - .ToArray(); + return obj; + } - return new QueryResult<SeriesTimerInfoDto> + 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> { - Items = returnArray, - TotalRecordCount = returnArray.Length + program.StartDate.ToLocalTime().DayOfWeek }; + + info.DayPattern = _tvDtoService.GetDayPattern(info.Days); + + 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 info; } - public async Task<ChannelInfoDto> GetChannel(string id, CancellationToken cancellationToken, User user = null) + public async Task CreateTimer(TimerInfoDto timer, CancellationToken cancellationToken) { - var results = await GetChannels(new ChannelQuery - { - UserId = user == null ? null : user.Id.ToString("N") + var service = string.IsNullOrEmpty(timer.ServiceName) ? ActiveService : GetServices(timer.ServiceName, null).First(); - }, cancellationToken).ConfigureAwait(false); + var info = await _tvDtoService.GetTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.CurrentCulture)); + // Set priority from default values + var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); + info.Priority = defaultValues.Priority; + + await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + + public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) + { + var service = string.IsNullOrEmpty(timer.ServiceName) ? ActiveService : GetServices(timer.ServiceName, null).First(); + + var info = await _tvDtoService.GetSeriesTimerInfo(timer, true, this, cancellationToken).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 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); } } } 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/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 2516a3ae3..86dd0bc75 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -48,14 +48,18 @@ <Reference Include="Alchemy"> <HintPath>..\packages\Alchemy.2.2.1\lib\net40\Alchemy.dll</HintPath> </Reference> - <Reference Include="Mono.Data.Sqlite"> - <HintPath>..\ThirdParty\Mono.Data.Sqlite\Mono.Data.Sqlite.dll</HintPath> - </Reference> <Reference Include="ServiceStack.Api.Swagger"> <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll</HintPath> </Reference> <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>..\packages\System.Data.SQLite.x86.1.0.90.0\lib\net45\System.Data.SQLite.dll</HintPath> + </Reference> + <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.Drawing" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> @@ -67,8 +71,9 @@ <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> @@ -85,6 +90,7 @@ <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"> @@ -153,6 +159,8 @@ <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" /> @@ -166,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> @@ -341,6 +351,16 @@ </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" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 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/Providers/ImageSaver.cs b/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs index 49194ba8d..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; } } @@ -502,7 +486,7 @@ namespace MediaBrowser.Server.Implementations.Providers { return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) }; } - + var extraFanartFilename = GetBackdropSaveFilename(item.BackdropImagePaths, "fanart", "fanart", outputIndex); return new[] @@ -579,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 @@ -593,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 cccabe2b8..e9c672151 100644 --- a/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs +++ b/MediaBrowser.Server.Implementations/Providers/ProviderManager.cs @@ -105,7 +105,6 @@ namespace MediaBrowser.Server.Implementations.Providers cancellationToken.ThrowIfCancellationRequested(); var enableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders; - var excludeTypes = ConfigurationManager.Configuration.InternetProviderExcludeTypes; var providerHistories = item.DateLastSaved == default(DateTime) ? new List<BaseProviderInfo>() : @@ -133,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) @@ -388,7 +378,7 @@ 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 => Task.Run(async () => { 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/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 3a07d33a6..1a94b9c79 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); } 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 d6b78bd84..8707f2e5b 100644 --- a/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -80,25 +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; + } - // Compare episode number + var yEpisode = y.AirsBeforeEpisodeNumber; + + // Special comes before the season + if (!yEpisode.HasValue) + { + return 1; + } - // Add 1 to to non-specials to account for AirsBeforeEpisodeNumber - var xEpisode = x.IndexNumber ?? -1; - xEpisode++; + // Compare episode number + var xEpisode = x.IndexNumber; - var yEpisode = y.AirsBeforeEpisodeNumber ?? 10000; + if (!xEpisode.HasValue) + { + // Can't really compare if this happens + return 0; + } - // Sometimes they'll both have a value. - // For example AirsAfterSeasonNumber=1, AirsBeforeSeasonNumber=2, AirsBeforeEpisodeNumber=1 - // The episode should be displayed at the end of season 1 - if (y.AirsAfterSeasonNumber.HasValue && y.AirsBeforeSeasonNumber.HasValue && y.AirsBeforeSeasonNumber.Value > y.AirsAfterSeasonNumber.Value) + // Special comes before episode + if (xEpisode.Value == yEpisode.Value) { - yEpisode = 10000; + 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 index 3fbb01f77..39a68b3f6 100644 --- a/MediaBrowser.Server.Implementations/Sorting/AlphanumComparator.cs +++ b/MediaBrowser.Server.Implementations/Sorting/AlphanumComparator.cs @@ -78,8 +78,14 @@ namespace MediaBrowser.Server.Implementations.Sorting // If both chunks contain numeric characters, sort them numerically if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) { - thisNumericChunk = Convert.ToInt32(thisChunk.ToString()); - thatNumericChunk = Convert.ToInt32(thatChunk.ToString()); + if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) + { + return 0; + } + if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) + { + return 0; + } if (thisNumericChunk < thatNumericChunk) { 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 39321876c..d37330821 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -50,6 +50,7 @@ <Reference Include="ServiceStack.Interfaces"> <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath> </Reference> + <Reference Include="Mono.Posix" Condition=" '$(ConfigurationName)' == 'Release Mono' "/> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -127,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.ServerApplication/App.xaml.cs b/MediaBrowser.ServerApplication/App.xaml.cs index 50bf02deb..839841620 100644 --- a/MediaBrowser.ServerApplication/App.xaml.cs +++ b/MediaBrowser.ServerApplication/App.xaml.cs @@ -116,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(); diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 77b49fbf1..00db91f41 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -71,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> @@ -168,7 +162,7 @@ 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; } /// <summary> @@ -220,8 +214,6 @@ namespace MediaBrowser.ServerApplication /// <returns>Task.</returns> protected override async Task RegisterResources(IProgress<double> progress) { - ServerKernel = new Kernel(); - await base.RegisterResources(progress).ConfigureAwait(false); RegisterSingleInstance<IHttpResultFactory>(new HttpResultFactory(LogManager, FileSystemManager)); @@ -229,7 +221,6 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance<IServerApplicationHost>(this); RegisterSingleInstance<IServerApplicationPaths>(ApplicationPaths); - RegisterSingleInstance(ServerKernel); RegisterSingleInstance(ServerConfigurationManager); RegisterSingleInstance<IWebSocketServer>(() => new AlchemyServer(Logger)); @@ -301,6 +292,8 @@ namespace MediaBrowser.ServerApplication await Task.WhenAll(itemsTask, displayPreferencesTask, userdataTask).ConfigureAwait(false); progress.Report(100); + await ((UserManager) UserManager).Initialize().ConfigureAwait(false); + SetKernelProperties(); } @@ -331,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> @@ -451,6 +444,8 @@ namespace MediaBrowser.ServerApplication ImageProcessor.AddParts(GetExports<IImageEnhancer>()); LiveTvManager.AddParts(GetExports<ILiveTvService>()); + + SessionManager.AddParts(GetExports<ISessionControllerFactory>()); } /// <summary> @@ -568,7 +563,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); diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs index d257ff362..1e99c4eb0 100644 --- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs @@ -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 { @@ -147,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 diff --git a/MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs b/MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs index 6d6157fa7..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> @@ -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,19 +242,19 @@ 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( 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.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 2188722b2..7743cc527 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -275,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); } @@ -284,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); } @@ -414,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 }; @@ -440,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(); @@ -466,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", @@ -495,6 +495,8 @@ namespace MediaBrowser.WebDashboard.Api "livetvchannel.js", "livetvchannels.js", "livetvguide.js", + "livetvnewrecording.js", + "livetvprogram.js", "livetvrecording.js", "livetvrecordings.js", "livetvtimer.js", @@ -539,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", @@ -551,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); @@ -583,6 +591,7 @@ namespace MediaBrowser.WebDashboard.Api var files = new[] { "site.css", + "mediaplayer.css", "librarybrowser.css", "detailtable.css", "posteritem.css", @@ -592,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 53f4fde2a..e260a0a05 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,44 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; - self.getLiveTvRecording = function (id) { + 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,6 +545,19 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; + self.getNewLiveTvTimerDefaults = function (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) { diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 09e68f59f..50d25de26 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -85,15 +85,75 @@ </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ApiClient.js" />
+ <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\editor.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>
@@ -172,19 +232,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">
@@ -355,12 +415,24 @@ <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>
@@ -397,9 +469,15 @@ <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>
@@ -409,12 +487,639 @@ <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>
@@ -618,10 +1323,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">
@@ -734,12 +1436,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>
@@ -765,7 +1467,7 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\updatepassword.html">
+ <Content Include="dashboard-ui\userpassword.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -805,7 +1507,7 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\scripts\clientsettings.js">
+ <Content Include="dashboard-ui\scripts\appsweather.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -825,7 +1527,7 @@ </Content>
</ItemGroup>
<ItemGroup>
- <Content Include="dashboard-ui\scripts\updatepasswordpage.js">
+ <Content Include="dashboard-ui\scripts\userpassword.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@@ -833,7 +1535,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">
@@ -844,12 +1546,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>
@@ -1059,76 +1761,6 @@ </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>
@@ -1232,8 +1864,21 @@ </Content>
</ItemGroup>
<ItemGroup>
+ <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>
diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 808183a67..659c5db1e 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.206" targetFramework="net45" /> + <package id="MediaBrowser.ApiClient.Javascript" version="3.0.210" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 116671f44..1df7b17bf 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.271</version> + <version>3.0.292</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <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.271" /> + <dependency id="MediaBrowser.Common" version="3.0.292" /> <dependency id="NLog" version="2.1.0" /> <dependency id="SimpleInjector" version="2.4.0" /> <dependency id="sharpcompress" version="0.10.2" /> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 126ddf4ca..14e09d90c 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.271</version> + <version>3.0.292</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 7de6b9e00..d49dfb85d 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.271</version> + <version>3.0.292</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.271" /> + <dependency id="MediaBrowser.Common" version="3.0.292" /> </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 |
