diff --git a/Emby.Drawing/GDI/DynamicImageHelpers.cs b/Emby.Drawing/GDI/DynamicImageHelpers.cs index 7b8ef2f98a..59340af8a9 100644 --- a/Emby.Drawing/GDI/DynamicImageHelpers.cs +++ b/Emby.Drawing/GDI/DynamicImageHelpers.cs @@ -33,7 +33,9 @@ namespace Emby.Drawing.GDI graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.CompositingMode = CompositingMode.SourceCopy; + + // SourceCopy causes the image to be blank in OSX + //graphics.CompositingMode = CompositingMode.SourceCopy; for (var row = 0; row < rows; row++) { @@ -44,19 +46,9 @@ namespace Emby.Drawing.GDI if (files.Count > index) { - using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true)) + using (var imgtemp = Image.FromFile(files[index])) { - using (var memoryStream = new MemoryStream()) - { - fileStream.CopyTo(memoryStream); - - memoryStream.Position = 0; - - using (var imgtemp = Image.FromStream(memoryStream, true, false)) - { - graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight); - } - } + graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight); } } @@ -90,7 +82,9 @@ namespace Emby.Drawing.GDI graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.CompositingMode = CompositingMode.SourceCopy; + + // SourceCopy causes the image to be blank in OSX + //graphics.CompositingMode = CompositingMode.SourceCopy; for (var row = 0; row < rows; row++) { @@ -99,21 +93,10 @@ namespace Emby.Drawing.GDI var x = col * singleSize; var y = row * singleSize; - using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true)) + using (var imgtemp = Image.FromFile(files[index])) { - using (var memoryStream = new MemoryStream()) - { - fileStream.CopyTo(memoryStream); - - memoryStream.Position = 0; - - using (var imgtemp = Image.FromStream(memoryStream, true, false)) - { - graphics.DrawImage(imgtemp, x, y, singleSize, singleSize); - } - } + graphics.DrawImage(imgtemp, x, y, singleSize, singleSize); } - index++; } } @@ -121,16 +104,5 @@ namespace Emby.Drawing.GDI } } } - - private static Stream GetStream(Image image) - { - var ms = new MemoryStream(); - - image.Save(ms, ImageFormat.Png); - - ms.Position = 0; - - return ms; - } } } diff --git a/Emby.Drawing/GDI/GDIImageEncoder.cs b/Emby.Drawing/GDI/GDIImageEncoder.cs index bdd1c5a22f..afd16899dc 100644 --- a/Emby.Drawing/GDI/GDIImageEncoder.cs +++ b/Emby.Drawing/GDI/GDIImageEncoder.cs @@ -119,9 +119,11 @@ namespace Emby.Drawing.GDI thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; - thumbnailGraph.CompositingMode = !hasPostProcessing ? - CompositingMode.SourceCopy : - CompositingMode.SourceOver; + + // SourceCopy causes the image to be blank in OSX + //thumbnailGraph.CompositingMode = !hasPostProcessing ? + // CompositingMode.SourceCopy : + // CompositingMode.SourceOver; SetBackgroundColor(thumbnailGraph, options); diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index dc811812ae..7c5f7cde40 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -63,6 +63,15 @@ namespace MediaBrowser.Api Instance = this; _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress; + _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; + } + + private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) + { + if (!string.IsNullOrWhiteSpace(e.PlaySessionId)) + { + PingTranscodingJob(e.PlaySessionId, e.IsPaused); + } } void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e) @@ -126,9 +135,10 @@ namespace MediaBrowser.Api /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { - var jobCount = _activeTranscodingJobs.Count; + var list = _activeTranscodingJobs.ToList(); + var jobCount = list.Count; - Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, false, path => true)); + Parallel.ForEach(list, j => KillTranscodingJob(j, false, path => true)); // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files if (jobCount > 0) @@ -182,13 +192,13 @@ namespace MediaBrowser.Api _activeTranscodingJobs.Add(job); - ReportTranscodingProgress(job, state, null, null, null, null); + ReportTranscodingProgress(job, state, null, null, null, null, null); return job; } } - public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded) + public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null; @@ -198,6 +208,7 @@ namespace MediaBrowser.Api job.CompletionPercentage = percentComplete; job.TranscodingPositionTicks = ticks; job.BytesTranscoded = bytesTranscoded; + job.BitRate = bitRate; } var deviceId = state.Request.DeviceId; @@ -209,7 +220,7 @@ namespace MediaBrowser.Api _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo { - Bitrate = state.TotalOutputBitrate, + Bitrate = bitRate ?? state.TotalOutputBitrate, AudioCodec = audioCodec, VideoCodec = videoCodec, Container = state.OutputContainer, @@ -348,7 +359,7 @@ namespace MediaBrowser.Api return; } - var timerDuration = 1000; + var timerDuration = 10000; if (job.Type != TranscodingJobType.Progressive) { @@ -400,7 +411,7 @@ namespace MediaBrowser.Api } } - Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); + Logger.Info("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); KillTranscodingJob(job, true, path => true); } @@ -558,13 +569,13 @@ namespace MediaBrowser.Api { } - catch (IOException ex) + catch (IOException) { //Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path); DeletePartialStreamFiles(path, jobType, retryCount + 1, 500); } - catch (Exception ex) + catch { //Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path); } @@ -684,6 +695,7 @@ namespace MediaBrowser.Api public long? BytesDownloaded { get; set; } public long? BytesTranscoded { get; set; } + public int? BitRate { get; set; } public long? TranscodingPositionTicks { get; set; } public long? DownloadPositionTicks { get; set; } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 44a367be09..3ff432d741 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -139,6 +139,10 @@ namespace MediaBrowser.Api { options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value; } + if (hasDtoOptions.EnableUserData.HasValue) + { + options.EnableUserData = hasDtoOptions.EnableUserData.Value; + } if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes)) { diff --git a/MediaBrowser.Api/IHasDtoOptions.cs b/MediaBrowser.Api/IHasDtoOptions.cs index dac366113c..6ed1670c21 100644 --- a/MediaBrowser.Api/IHasDtoOptions.cs +++ b/MediaBrowser.Api/IHasDtoOptions.cs @@ -4,6 +4,7 @@ namespace MediaBrowser.Api public interface IHasDtoOptions : IHasItemFields { bool? EnableImages { get; set; } + bool? EnableUserData { get; set; } int? ImageTypeLimit { get; set; } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 5866ad15bc..3280358dfa 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -573,11 +573,9 @@ namespace MediaBrowser.Api.Images var outputFormats = GetOutputFormats(request, imageInfo, cropwhitespace, supportedImageEnhancers); - var cacheGuid = new Guid(_imageProcessor.GetImageCacheTag(item, imageInfo, supportedImageEnhancers)); - TimeSpan? cacheDuration = null; - if (!string.IsNullOrEmpty(request.Tag) && cacheGuid == new Guid(request.Tag)) + if (!string.IsNullOrEmpty(request.Tag)) { cacheDuration = TimeSpan.FromDays(365); } diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs index 5aab15dff1..a918e841fb 100644 --- a/MediaBrowser.Api/ItemRefreshService.cs +++ b/MediaBrowser.Api/ItemRefreshService.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Providers; using ServiceStack; using System.Threading; using CommonIO; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Api { @@ -39,12 +40,14 @@ namespace MediaBrowser.Api private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; - public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem) + public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem, ILogger logger) { _libraryManager = libraryManager; _providerManager = providerManager; _fileSystem = fileSystem; + _logger = logger; } /// @@ -69,7 +72,7 @@ namespace MediaBrowser.Api private MetadataRefreshOptions GetRefreshOptions(BaseRefreshRequest request) { - return new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + return new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) { MetadataRefreshMode = request.MetadataRefreshMode, ImageRefreshMode = request.ImageRefreshMode, diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index b944a39b6a..2778cfe29c 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -304,7 +304,7 @@ namespace MediaBrowser.Api item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null; item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null; item.ProductionYear = request.ProductionYear; - item.OfficialRating = request.OfficialRating; + item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating; item.CustomRating = request.CustomRating; SetProductionLocations(item, request); diff --git a/MediaBrowser.Api/Library/FileOrganizationService.cs b/MediaBrowser.Api/Library/FileOrganizationService.cs index 0ed08a8607..ca391bef08 100644 --- a/MediaBrowser.Api/Library/FileOrganizationService.cs +++ b/MediaBrowser.Api/Library/FileOrganizationService.cs @@ -154,9 +154,12 @@ namespace MediaBrowser.Api.Library public void Post(PerformOrganization request) { + // Don't await this var task = _iFileOrganizationService.PerformOrganization(request.Id); - Task.WaitAll(task); + // Async processing (close dialog early instead of waiting until the file has been copied) + // Wait 2s for exceptions that may occur to have them forwarded to the client for immediate error display + task.Wait(2000); } public void Post(OrganizeEpisode request) @@ -168,6 +171,7 @@ namespace MediaBrowser.Api.Library dicNewProviderIds = request.NewSeriesProviderIds; } + // Don't await this var task = _iFileOrganizationService.PerformEpisodeOrganization(new EpisodeFileOrganizationRequest { EndingEpisodeNumber = request.EndingEpisodeNumber, @@ -182,11 +186,9 @@ namespace MediaBrowser.Api.Library TargetFolder = request.TargetFolder }); - // For async processing (close dialog early instead of waiting until the file has been copied) - //var tasks = new Task[] { task }; - //Task.WaitAll(tasks, 8000); - - Task.WaitAll(task); + // Async processing (close dialog early instead of waiting until the file has been copied) + // Wait 2s for exceptions that may occur to have them forwarded to the client for immediate error display + task.Wait(2000); } public object Get(GetSmartMatchInfos request) diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 3cf0d5d937..72966a7cdc 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -10,6 +10,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Api.Library { @@ -52,6 +55,8 @@ namespace MediaBrowser.Api.Library /// /// The path. public string[] Paths { get; set; } + + public LibraryOptions LibraryOptions { get; set; } } [Route("/Library/VirtualFolders", "DELETE")] @@ -136,6 +141,14 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } + [Route("/Library/VirtualFolders/LibraryOptions", "POST")] + public class UpdateLibraryOptions : IReturnVoid + { + public string Id { get; set; } + + public LibraryOptions LibraryOptions { get; set; } + } + /// /// Class LibraryStructureService /// @@ -184,13 +197,22 @@ namespace MediaBrowser.Api.Library return ToOptimizedSerializedResultUsingCache(result); } + public void Post(UpdateLibraryOptions request) + { + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); + + collectionFolder.UpdateLibraryOptions(request.LibraryOptions); + } + /// /// Posts the specified request. /// /// The request. public void Post(AddVirtualFolder request) { - _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, request.RefreshLibrary); + var libraryOptions = request.LibraryOptions ?? new LibraryOptions(); + + _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, libraryOptions, request.RefreshLibrary); } /// @@ -214,12 +236,12 @@ namespace MediaBrowser.Api.Library var currentPath = Path.Combine(rootFolderPath, request.Name); var newPath = Path.Combine(rootFolderPath, request.NewName); - if (!_fileSystem.DirectoryExists(currentPath)) + if (!_fileSystem.DirectoryExists(currentPath)) { throw new DirectoryNotFoundException("The media collection does not exist"); } - if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath)) + if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath)) { throw new ArgumentException("There is already a media collection with the name " + newPath + "."); } @@ -234,11 +256,11 @@ namespace MediaBrowser.Api.Library //Create an unique name var temporaryName = Guid.NewGuid().ToString(); var temporaryPath = Path.Combine(rootFolderPath, temporaryName); - _fileSystem.MoveDirectory(currentPath, temporaryPath); + _fileSystem.MoveDirectory(currentPath, temporaryPath); currentPath = temporaryPath; } - _fileSystem.MoveDirectory(currentPath, newPath); + _fileSystem.MoveDirectory(currentPath, newPath); } finally { diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index c687758b72..545ac162ff 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -82,6 +82,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "AddCurrentProgram", Description = "Optional. Adds current program info to each channel", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public bool AddCurrentProgram { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + public GetChannels() { AddCurrentProgram = true; @@ -149,6 +152,9 @@ namespace MediaBrowser.Api.LiveTv public bool EnableTotalRecordCount { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + public GetRecordings() { EnableTotalRecordCount = true; @@ -271,6 +277,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string EnableImageTypes { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + /// /// Fields to return within the items, in addition to basic information /// @@ -331,6 +340,9 @@ namespace MediaBrowser.Api.LiveTv /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Fields { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } } [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")] @@ -726,7 +738,12 @@ namespace MediaBrowser.Api.LiveTv var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId); - var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, GetDtoOptions(Request), user).ConfigureAwait(false)).ToArray(); + var options = GetDtoOptions(request); + RemoveFields(options); + + options.AddCurrentProgram = request.AddCurrentProgram; + + var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, options, user).ConfigureAwait(false)).ToArray(); var result = new QueryResult { @@ -737,6 +754,14 @@ namespace MediaBrowser.Api.LiveTv return ToOptimizedSerializedResultUsingCache(result); } + private void RemoveFields(DtoOptions options) + { + options.Fields.Remove(ItemFields.CanDelete); + options.Fields.Remove(ItemFields.CanDownload); + options.Fields.Remove(ItemFields.DisplayPreferencesId); + options.Fields.Remove(ItemFields.Etag); + } + public object Get(GetChannel request) { var user = string.IsNullOrWhiteSpace(request.UserId) ? null : _userManager.GetUserById(request.UserId); @@ -997,10 +1022,7 @@ namespace MediaBrowser.Api.LiveTv public async Task Get(GetRecordingGroup request) { - var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery - { - - }, CancellationToken.None).ConfigureAwait(false); + var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery(), CancellationToken.None).ConfigureAwait(false); var group = result.Items.FirstOrDefault(i => string.Equals(i.Id, request.Id, StringComparison.OrdinalIgnoreCase)); diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 755713c845..66f2fac812 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -192,7 +192,8 @@ namespace MediaBrowser.Api.Movies SortOrder = SortOrder.Descending, Limit = 7, ParentId = parentIdGuid, - Recursive = true + Recursive = true, + IsPlayed = true }; var recentlyPlayedMovies = _libraryManager.GetItemList(query).ToList(); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index ce3691095c..4af564a5aa 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -22,6 +22,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; namespace MediaBrowser.Api.Playback { @@ -69,6 +71,9 @@ namespace MediaBrowser.Api.Playback protected IZipClient ZipClient { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } + public static IServerApplicationHost AppHost; + public static IHttpClient HttpClient; + /// /// Initializes a new instance of the class. /// @@ -286,28 +291,46 @@ namespace MediaBrowser.Api.Playback protected string GetH264Encoder(StreamState state) { + var defaultEncoder = "libx264"; + // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. if (state.VideoType == VideoType.VideoFile) { - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) || - string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); + var hwType = encodingOptions.HardwareAccelerationType; + + if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - return "h264_qsv"; + return GetAvailableEncoder("h264_qsv", defaultEncoder); } - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_nvenc", defaultEncoder); + } + if (string.Equals(hwType, "h264_omx", StringComparison.OrdinalIgnoreCase)) { - return "h264_nvenc"; + return GetAvailableEncoder("h264_omx", defaultEncoder); } - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(encodingOptions.VaapiDevice)) { - return "h264_omx"; + return GetAvailableEncoder("h264_vaapi", defaultEncoder); } } - return "libx264"; + return defaultEncoder; + } + + private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder) + { + if (MediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; + } + return defaultEncoder; } /// @@ -409,7 +432,8 @@ namespace MediaBrowser.Api.Playback if (!string.IsNullOrEmpty(state.VideoRequest.Profile)) { - if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { // not supported by h264_omx param += " -profile:v " + state.VideoRequest.Profile; @@ -464,7 +488,8 @@ namespace MediaBrowser.Api.Playback if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; } @@ -530,59 +555,97 @@ namespace MediaBrowser.Api.Playback var filters = new List(); - if (state.DeInterlace) + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + filters.Add("format=nv12|vaapi"); + filters.Add("hwupload"); + } + else if (state.DeInterlace && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { filters.Add("yadif=0:-1:0"); } - // If fixed dimensions were supplied - if (request.Width.HasValue && request.Height.HasValue) + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { - var widthParam = request.Width.Value.ToString(UsCulture); - var heightParam = request.Height.Value.ToString(UsCulture); + // Work around vaapi's reduced scaling features + var scaler = "scale_vaapi"; - filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam)); - } + // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions + // (outputWidth, outputHeight). The user may request precise output dimensions or maximum + // output dimensions. Output dimensions are guaranteed to be even. + decimal inputWidth = Convert.ToDecimal(state.VideoStream.Width); + decimal inputHeight = Convert.ToDecimal(state.VideoStream.Height); + decimal outputWidth = request.Width.HasValue ? Convert.ToDecimal(request.Width.Value) : inputWidth; + decimal outputHeight = request.Height.HasValue ? Convert.ToDecimal(request.Height.Value) : inputHeight; + decimal maximumWidth = request.MaxWidth.HasValue ? Convert.ToDecimal(request.MaxWidth.Value) : outputWidth; + decimal maximumHeight = request.MaxHeight.HasValue ? Convert.ToDecimal(request.MaxHeight.Value) : outputHeight; - // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size - else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue) - { - var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); - var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); + if (outputWidth > maximumWidth || outputHeight > maximumHeight) + { + var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight); + outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale)); + outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale)); + } - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); - } + outputWidth = 2 * Math.Truncate(outputWidth / 2); + outputHeight = 2 * Math.Truncate(outputHeight / 2); - // If a fixed width was requested - else if (request.Width.HasValue) + if (outputWidth != inputWidth || outputHeight != inputHeight) + { + filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(UsCulture), outputHeight.ToString(UsCulture))); + } + } + else { - var widthParam = request.Width.Value.ToString(UsCulture); + // If fixed dimensions were supplied + if (request.Width.HasValue && request.Height.HasValue) + { + var widthParam = request.Width.Value.ToString(UsCulture); + var heightParam = request.Height.Value.ToString(UsCulture); - filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); - } + filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam)); + } - // If a fixed height was requested - else if (request.Height.HasValue) - { - var heightParam = request.Height.Value.ToString(UsCulture); + // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size + else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue) + { + var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); - filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); - } + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + } - // If a max width was requested - else if (request.MaxWidth.HasValue) - { - var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + // If a fixed width was requested + else if (request.Width.HasValue) + { + var widthParam = request.Width.Value.ToString(UsCulture); - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); - } + filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); + } - // If a max height was requested - else if (request.MaxHeight.HasValue) - { - var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); + // If a fixed height was requested + else if (request.Height.HasValue) + { + var heightParam = request.Height.Value.ToString(UsCulture); + + filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); + } + + // If a max width was requested + else if (request.MaxWidth.HasValue) + { + var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); + } + + // If a max height was requested + else if (request.MaxHeight.HasValue) + { + var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); - filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(ih\\,{0})", maxHeightParam)); + filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + } } var output = string.Empty; @@ -917,6 +980,15 @@ namespace MediaBrowser.Api.Playback } } + if (state.VideoRequest != null) + { + var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); + if (GetVideoEncoder(state).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1) + { + arg = "-hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device " + encodingOptions.VaapiDevice + " " + arg; + } + } + return arg.Trim(); } @@ -1042,14 +1114,14 @@ namespace MediaBrowser.Api.Playback var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; Logger.Info(commandLineLogMessage); - var logFilePrefix = "transcode"; + var logFilePrefix = "ffmpeg-transcode"; if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "directstream"; + logFilePrefix = "ffmpeg-directstream"; } else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "remux"; + logFilePrefix = "ffmpeg-remux"; } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); @@ -1080,7 +1152,7 @@ namespace MediaBrowser.Api.Playback //process.BeginOutputReadLine(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream)); + var task = Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream)); // Wait for the file to exist before proceeeding while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) @@ -1099,28 +1171,30 @@ namespace MediaBrowser.Api.Playback } StartThrottler(state, transcodingJob); + ReportUsage(state); return transcodingJob; } private void StartThrottler(StreamState state, TranscodingJob transcodingJob) { - if (EnableThrottling(state) && state.InputProtocol == MediaProtocol.File && - state.RunTimeTicks.HasValue && - state.VideoType == VideoType.VideoFile && - !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EnableThrottling(state) && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) - { - transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager); - state.TranscodingThrottler.Start(); - } + transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager); + state.TranscodingThrottler.Start(); } } protected virtual bool EnableThrottling(StreamState state) { - return true; + // do not use throttling with hardware encoders + return state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && + state.IsInputVideo && + state.VideoType == VideoType.VideoFile && + !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && + string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase); } private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) @@ -1158,6 +1232,7 @@ namespace MediaBrowser.Api.Playback double? percent = null; TimeSpan? transcodingPosition = null; long? bytesTranscoded = null; + int? bitRate = null; var parts = line.Split(' '); @@ -1221,11 +1296,32 @@ namespace MediaBrowser.Api.Playback } } } + else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + float val; + + if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val)) + { + bitRate = (int)Math.Ceiling(val * scale.Value); + } + } + } } if (framerate.HasValue || percent.HasValue) { - ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded); + ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } @@ -1547,13 +1643,6 @@ namespace MediaBrowser.Api.Playback } } else if (i == 25) - { - if (videoRequest != null) - { - videoRequest.ForceLiveStream = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 26) { if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) { @@ -1564,17 +1653,21 @@ namespace MediaBrowser.Api.Playback } } } - else if (i == 27) + else if (i == 26) { request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture); } - else if (i == 28) + else if (i == 27) { if (videoRequest != null) { videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); } } + else if (i == 28) + { + request.Tag = val; + } } } @@ -1788,6 +1881,19 @@ namespace MediaBrowser.Api.Playback { state.OutputAudioCodec = "copy"; } + else + { + // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + if (!string.IsNullOrWhiteSpace(auth.UserId)) + { + var user = UserManager.GetUserById(auth.UserId); + if (!user.Policy.EnableAudioPlaybackTranscoding) + { + state.OutputAudioCodec = "copy"; + } + } + } } private void AttachMediaSourceInfo(StreamState state, @@ -2159,13 +2265,127 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null) { state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; - state.VideoRequest.ForceLiveStream = transcodingProfile.ForceLiveStream; state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; } } } } + private async void ReportUsage(StreamState state) + { + try + { + await ReportUsageInternal(state).ConfigureAwait(false); + } + catch + { + + } + } + + private Task ReportUsageInternal(StreamState state) + { + if (!ServerConfigurationManager.Configuration.EnableAnonymousUsageReporting) + { + return Task.FromResult(true); + } + + if (!MediaEncoder.IsDefaultEncoderPath) + { + return Task.FromResult(true); + } + + var dict = new Dictionary(); + + var outputAudio = GetAudioEncoder(state); + if (!string.IsNullOrWhiteSpace(outputAudio)) + { + dict["outputAudio"] = outputAudio; + } + + var outputVideo = GetVideoEncoder(state); + if (!string.IsNullOrWhiteSpace(outputVideo)) + { + dict["outputVideo"] = outputVideo; + } + + if (ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase) && + ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return Task.FromResult(true); + } + + dict["id"] = AppHost.SystemId; + dict["type"] = state.VideoRequest == null ? "Audio" : "Video"; + + var audioStream = state.AudioStream; + if (audioStream != null && !string.IsNullOrWhiteSpace(audioStream.Codec)) + { + dict["inputAudio"] = audioStream.Codec; + } + + var videoStream = state.VideoStream; + if (videoStream != null && !string.IsNullOrWhiteSpace(videoStream.Codec)) + { + dict["inputVideo"] = videoStream.Codec; + } + + var cert = GetType().Assembly.GetModules().First().GetSignerCertificate(); + if (cert != null) + { + dict["assemblySig"] = cert.GetCertHashString(); + dict["certSubject"] = cert.Subject ?? string.Empty; + dict["certIssuer"] = cert.Issuer ?? string.Empty; + } + else + { + return Task.FromResult(true); + } + + if (state.SupportedAudioCodecs.Count > 0) + { + dict["supportedAudioCodecs"] = string.Join(",", state.SupportedAudioCodecs.ToArray()); + } + + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + + dict["appName"] = auth.Client ?? string.Empty; + dict["appVersion"] = auth.Version ?? string.Empty; + dict["device"] = auth.Device ?? string.Empty; + dict["deviceId"] = auth.DeviceId ?? string.Empty; + dict["context"] = "streaming"; + + //Logger.Info(JsonSerializer.SerializeToString(dict)); + if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList(); + list.Add(outputAudio); + ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray(); + } + + if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList(); + list.Add(outputVideo); + ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray(); + } + + ServerConfigurationManager.SaveConfiguration(); + + //Logger.Info(JsonSerializer.SerializeToString(dict)); + var options = new HttpRequestOptions() + { + Url = "https://mb3admin.com/admin/service/transcoding/report", + CancellationToken = CancellationToken.None, + LogRequest = false, + LogErrors = false + }; + options.RequestContent = JsonSerializer.SerializeToString(dict); + options.RequestContentType = "application/json"; + + return HttpClient.Post(options); + } + /// /// Adds the dlna headers. /// diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 3d8957086d..a0ac96b9d2 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -282,11 +281,6 @@ namespace MediaBrowser.Api.Playback.Hls { var isLiveStream = (state.RunTimeTicks ?? 0) == 0; - if (state.VideoRequest.ForceLiveStream) - { - return true; - } - return isLiveStream; } } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index e029d4e99a..7de3096997 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -257,8 +257,7 @@ namespace MediaBrowser.Api.Playback.Hls return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false); } - // 256k - private const int BufferSize = 262144; + private const int BufferSize = 81920; private long GetStartPositionTicks(StreamState state, int requestedIndex) { diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 0b989784c0..91e62b4e32 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -284,6 +284,13 @@ namespace MediaBrowser.Api.Playback options.ForceDirectPlay = true; } } + else if (item is Video) + { + if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + { + options.ForceDirectPlay = true; + } + } // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? @@ -315,6 +322,13 @@ namespace MediaBrowser.Api.Playback options.ForceDirectStream = true; } } + else if (item is Video) + { + if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + { + options.ForceDirectStream = true; + } + } // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index d75b8947a8..b8cb6b14f0 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -142,7 +142,8 @@ namespace MediaBrowser.Api.Playback.Progressive var outputPath = state.OutputFilePath; var outputPathExists = FileSystem.FileExists(outputPath); - var isTranscodeCached = outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive); + var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); + var isTranscodeCached = outputPathExists && transcodingJob != null; AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached); @@ -153,42 +154,64 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { - return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - ResponseHeaders = responseHeaders, - ContentType = contentType, - IsHeadRequest = isHeadRequest, - Path = state.MediaPath - }).ConfigureAwait(false); - } - } + TimeSpan? cacheDuration = null; - // Not static but transcode cache file exists - if (isTranscodeCached) - { - var contentType = state.GetMimeType(outputPath); + if (!string.IsNullOrEmpty(request.Tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } - try - { return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { ResponseHeaders = responseHeaders, ContentType = contentType, IsHeadRequest = isHeadRequest, - Path = outputPath + Path = state.MediaPath, + CacheDuration = cacheDuration + }).ConfigureAwait(false); } - finally - { - state.Dispose(); - } } + //// Not static but transcode cache file exists + //if (isTranscodeCached && state.VideoRequest == null) + //{ + // var contentType = state.GetMimeType(outputPath); + + // try + // { + // if (transcodingJob != null) + // { + // ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob); + // } + + // return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + // { + // ResponseHeaders = responseHeaders, + // ContentType = contentType, + // IsHeadRequest = isHeadRequest, + // Path = outputPath, + // FileShare = FileShare.ReadWrite, + // OnComplete = () => + // { + // if (transcodingJob != null) + // { + // ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob); + // } + // } + + // }).ConfigureAwait(false); + // } + // finally + // { + // state.Dispose(); + // } + //} + // Need to start ffmpeg try { - return await GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource) - .ConfigureAwait(false); + return await GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false); } catch { @@ -347,9 +370,9 @@ namespace MediaBrowser.Api.Playback.Progressive outputHeaders[item.Key] = item.Value; } - Func streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream, CancellationToken.None); + var streamSource = new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None); - return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders); + return ResultFactory.GetAsyncStreamWriter(streamSource); } finally { @@ -368,7 +391,7 @@ namespace MediaBrowser.Api.Playback.Progressive if (totalBitrate > 0 && state.RunTimeTicks.HasValue) { - return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds); + return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8); } return null; diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 13d59240f5..0a9a446412 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -1,59 +1,84 @@ using MediaBrowser.Model.Logging; -using ServiceStack.Web; using System; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Net; +using System.Collections.Generic; +using ServiceStack.Web; namespace MediaBrowser.Api.Playback.Progressive { - public class ProgressiveFileCopier + public class ProgressiveFileCopier : IAsyncStreamSource, IHasOptions { private readonly IFileSystem _fileSystem; private readonly TranscodingJob _job; private readonly ILogger _logger; + private readonly string _path; + private readonly CancellationToken _cancellationToken; + private readonly Dictionary _outputHeaders; // 256k - private const int BufferSize = 262144; + private const int BufferSize = 81920; private long _bytesWritten = 0; - public ProgressiveFileCopier(IFileSystem fileSystem, TranscodingJob job, ILogger logger) + public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken) { _fileSystem = fileSystem; + _path = path; + _outputHeaders = outputHeaders; _job = job; _logger = logger; + _cancellationToken = cancellationToken; } - public async Task StreamFile(string path, Stream outputStream, CancellationToken cancellationToken) + public IDictionary Options { - var eofCount = 0; + get + { + return _outputHeaders; + } + } - using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + public async Task WriteToAsync(Stream outputStream) + { + try { - while (eofCount < 15) + var eofCount = 0; + + using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) { - var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, cancellationToken).ConfigureAwait(false); + while (eofCount < 15) + { + var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false); - //var position = fs.Position; - //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); + //var position = fs.Position; + //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); - if (bytesRead == 0) - { - if (_job == null || _job.HasExited) + if (bytesRead == 0) { - eofCount++; + if (_job == null || _job.HasExited) + { + eofCount++; + } + await Task.Delay(100, _cancellationToken).ConfigureAwait(false); + } + else + { + eofCount = 0; } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - else - { - eofCount = 0; } } } + finally + { + if (_job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(_job); + } + } } private async Task CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken) diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 3fd67c51ea..21e8845f5f 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -149,11 +149,11 @@ namespace MediaBrowser.Api.Playback.Progressive { args += " -copyts -avoid_negative_ts disabled -start_at_zero"; } - + return args; } - var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", + var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", 5.ToString(UsCulture)); args += keyFrameArg; @@ -237,4 +237,4 @@ namespace MediaBrowser.Api.Playback.Progressive return args; } } -} +} \ No newline at end of file diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index a8ca6aaa3b..8cdf846ed5 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -74,6 +74,7 @@ namespace MediaBrowser.Api.Playback public string Params { get; set; } public string PlaySessionId { get; set; } public string LiveStreamId { get; set; } + public string Tag { get; set; } } public class VideoStreamRequest : StreamRequest @@ -192,8 +193,6 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool CopyTimestamps { get; set; } - public bool ForceLiveStream { get; set; } - public bool EnableSubtitlesInManifest { get; set; } public VideoStreamRequest() diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index da6be97b61..d7d94c69b4 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -80,7 +80,10 @@ namespace MediaBrowser.Api.Playback { return 10; } - if (userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1) + if (userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) { return 10; } @@ -208,7 +211,7 @@ namespace MediaBrowser.Api.Playback private async void DisposeLiveStream() { - if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId)) + if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) { try { diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index 604227a150..9693992882 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -72,7 +72,7 @@ namespace MediaBrowser.Api } [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")] - public class GetPlaylistItems : IReturn>, IHasItemFields + public class GetPlaylistItems : IReturn>, IHasDtoOptions { [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } @@ -104,6 +104,18 @@ namespace MediaBrowser.Api /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Fields { get; set; } + + [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableImages { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ImageTypeLimit { get; set; } + + [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string EnableImageTypes { get; set; } } [Authenticated] diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs index 3bf70715fb..7ad69fd041 100644 --- a/MediaBrowser.Api/PluginService.cs +++ b/MediaBrowser.Api/PluginService.cs @@ -227,7 +227,7 @@ namespace MediaBrowser.Api .ToList(); } } - catch (Exception ex) + catch { //Logger.ErrorException("Error getting plugin list", ex); // Play it safe here diff --git a/MediaBrowser.Api/Reports/ReportsService.cs b/MediaBrowser.Api/Reports/ReportsService.cs index d0b6d6e787..cb03d93822 100644 --- a/MediaBrowser.Api/Reports/ReportsService.cs +++ b/MediaBrowser.Api/Reports/ReportsService.cs @@ -275,8 +275,6 @@ namespace MediaBrowser.Api.Reports case ItemFilter.IsPlayed: query.IsPlayed = true; break; - case ItemFilter.IsRecentlyAdded: - break; case ItemFilter.IsResumable: query.IsResumable = true; break; diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs index a1e47bd8fd..1621c80567 100644 --- a/MediaBrowser.Api/SimilarItemsHelper.cs +++ b/MediaBrowser.Api/SimilarItemsHelper.cs @@ -29,8 +29,20 @@ namespace MediaBrowser.Api public string ExcludeArtistIds { get; set; } } - public class BaseGetSimilarItems : IReturn, IHasItemFields + public class BaseGetSimilarItems : IReturn, IHasDtoOptions { + [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableImages { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ImageTypeLimit { get; set; } + + [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string EnableImageTypes { get; set; } + /// /// Gets or sets the user id. /// diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 1bebd42eb1..ef898eb53e 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -117,7 +117,7 @@ namespace MediaBrowser.Api config.EnableStandaloneMusicKeys = true; config.EnableCaseSensitiveItemIds = true; //config.EnableFolderView = true; - config.SchemaVersion = 108; + config.SchemaVersion = 109; } public void Post(UpdateStartupConfiguration request) diff --git a/MediaBrowser.Api/Sync/SyncHelper.cs b/MediaBrowser.Api/Sync/SyncHelper.cs index 0d3e8707db..60df2bb1e9 100644 --- a/MediaBrowser.Api/Sync/SyncHelper.cs +++ b/MediaBrowser.Api/Sync/SyncHelper.cs @@ -24,7 +24,20 @@ namespace MediaBrowser.Api.Sync } break; } - if (item.IsFolder && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre) + if (item.IsAudio) + { + options.Add(SyncJobOption.Quality); + options.Add(SyncJobOption.Profile); + break; + } + if (item.IsMusicGenre || item.IsArtist|| item.IsType("musicalbum")) + { + options.Add(SyncJobOption.Quality); + options.Add(SyncJobOption.Profile); + options.Add(SyncJobOption.ItemLimit); + break; + } + if (item.IsFolderItem && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre) { options.Add(SyncJobOption.Quality); options.Add(SyncJobOption.Profile); @@ -44,7 +57,7 @@ namespace MediaBrowser.Api.Sync { if (item.SupportsSync ?? false) { - if (item.IsFolder || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson) + if (item.IsFolderItem || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson) { options.Add(SyncJobOption.SyncNewContent); options.Add(SyncJobOption.ItemLimit); diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index a15ce216f2..821591dc4e 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -66,6 +66,7 @@ namespace MediaBrowser.Api.Sync public string Id { get; set; } } + [Route("/Sync/Items/Cancel", "POST", Summary = "Cancels items from a sync target")] [Route("/Sync/{TargetId}/Items", "DELETE", Summary = "Cancels items from a sync target")] public class CancelItems : IReturnVoid { @@ -211,7 +212,7 @@ namespace MediaBrowser.Api.Sync return ToOptimizedResult(result); } - public void Delete(CancelItems request) + public void Any(CancelItems request) { var itemIds = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); @@ -290,7 +291,8 @@ namespace MediaBrowser.Api.Sync { Fields = new List { - ItemFields.SyncInfo + ItemFields.SyncInfo, + ItemFields.BasicSyncInfo } }; diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 3f248ea8f0..daaa6343d1 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -69,6 +69,9 @@ namespace MediaBrowser.Api [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string EnableImageTypes { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } } [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")] @@ -117,6 +120,9 @@ namespace MediaBrowser.Api [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string EnableImageTypes { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } } [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")] @@ -184,6 +190,10 @@ namespace MediaBrowser.Api [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string EnableImageTypes { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + } [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")] @@ -226,6 +236,10 @@ namespace MediaBrowser.Api [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string EnableImageTypes { get; set; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + } /// @@ -409,23 +423,14 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("No series exists with Id " + request.Id); } - var seasons = series.GetSeasons(user); - - if (request.IsSpecialSeason.HasValue) + var seasons = (await series.GetItems(new InternalItemsQuery(user) { - var val = request.IsSpecialSeason.Value; + IsMissing = request.IsMissing, + IsVirtualUnaired = request.IsVirtualUnaired, + IsSpecialSeason = request.IsSpecialSeason, + AdjacentTo = request.AdjacentTo - seasons = seasons.Where(i => i.IsSpecialSeason == val); - } - - seasons = FilterVirtualSeasons(request, seasons); - - // This must be the last filter - if (!string.IsNullOrEmpty(request.AdjacentTo)) - { - seasons = UserViewBuilder.FilterForAdjacency(seasons, request.AdjacentTo) - .Cast(); - } + }).ConfigureAwait(false)).Items.OfType(); var dtoOptions = GetDtoOptions(request); @@ -439,23 +444,6 @@ namespace MediaBrowser.Api }; } - private IEnumerable FilterVirtualSeasons(GetSeasons request, IEnumerable items) - { - if (request.IsMissing.HasValue) - { - var val = request.IsMissing.Value; - items = items.Where(i => (i.IsMissingSeason) == val); - } - - if (request.IsVirtualUnaired.HasValue) - { - var val = request.IsVirtualUnaired.Value; - items = items.Where(i => i.IsVirtualUnaired == val); - } - - return items; - } - public async Task Get(GetEpisodes request) { var user = _userManager.GetUserById(request.UserId); @@ -490,7 +478,7 @@ namespace MediaBrowser.Api } else { - episodes = series.GetEpisodes(user, season); + episodes = series.GetSeasonEpisodes(user, season); } } else diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index df73ef720b..ad031f483f 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -8,8 +8,6 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using ServiceStack; using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 9465d1fdc5..5381f9004c 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -164,8 +164,6 @@ namespace MediaBrowser.Api.UserLibrary case ItemFilter.IsPlayed: query.IsPlayed = true; break; - case ItemFilter.IsRecentlyAdded: - break; case ItemFilter.IsResumable: query.IsResumable = true; break; @@ -180,9 +178,10 @@ namespace MediaBrowser.Api.UserLibrary var result = GetItems(request, query); + var syncProgess = DtoService.GetSyncedItemProgress(dtoOptions); var dtos = result.Items.Select(i => { - var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user); + var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, syncProgess, user); if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes)) { @@ -213,6 +212,7 @@ namespace MediaBrowser.Api.UserLibrary dto.AlbumCount = counts.AlbumCount; dto.SongCount = counts.SongCount; dto.GameCount = counts.GameCount; + dto.ArtistCount = counts.ArtistCount; } /// @@ -325,7 +325,8 @@ namespace MediaBrowser.Api.UserLibrary tuples = ibnItems.Select(i => new Tuple>(i, new List())); } - var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user)); + var syncProgess = DtoService.GetSyncedItemProgress(dtoOptions); + var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, syncProgess, user)); result.Items = dtos.Where(i => i != null).ToArray(); diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 3e9a541c0c..96acb1f607 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -226,6 +226,9 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] public bool? EnableImages { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? ImageTypeLimit { get; set; } diff --git a/MediaBrowser.Api/UserLibrary/GameGenresService.cs b/MediaBrowser.Api/UserLibrary/GameGenresService.cs index a0883f98cc..a0f3855c55 100644 --- a/MediaBrowser.Api/UserLibrary/GameGenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GameGenresService.cs @@ -4,11 +4,9 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using ServiceStack; using System; using System.Collections.Generic; -using System.Linq; using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs index 57c11a1fad..ec2bc6bda5 100644 --- a/MediaBrowser.Api/UserLibrary/GenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GenresService.cs @@ -8,7 +8,6 @@ using MediaBrowser.Model.Entities; using ServiceStack; using System; using System.Collections.Generic; -using System.Linq; using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index b4d88a7f89..681624ba22 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; @@ -158,33 +157,11 @@ namespace MediaBrowser.Api.UserLibrary folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); } - if (!string.IsNullOrEmpty(request.Ids)) - { - request.Recursive = true; - var query = GetItemsQuery(request, user); - var result = await folder.GetItems(query).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(request.SortBy)) - { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray(); - } - - return result; - } - - if (request.Recursive) + if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null) { return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); } - if (user == null) - { - return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); - } - var userRoot = item as UserRootFolder; if (userRoot == null) @@ -294,8 +271,6 @@ namespace MediaBrowser.Api.UserLibrary case ItemFilter.IsPlayed: query.IsPlayed = true; break; - case ItemFilter.IsRecentlyAdded: - break; case ItemFilter.IsResumable: query.IsResumable = true; break; diff --git a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs index 887c999411..b2eba070fc 100644 --- a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs +++ b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs @@ -8,8 +8,6 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using ServiceStack; using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 3be11bdc56..c392ef4634 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -12,6 +12,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller.Providers; namespace MediaBrowser.Api.UserLibrary { @@ -244,6 +246,9 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string EnableImageTypes { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + public GetLatestMedia() { Limit = 20; @@ -262,14 +267,16 @@ namespace MediaBrowser.Api.UserLibrary private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly IUserViewManager _userViewManager; + private readonly IFileSystem _fileSystem; - public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager) + public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager, IFileSystem fileSystem) { _userManager = userManager; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _dtoService = dtoService; _userViewManager = userViewManager; + _fileSystem = fileSystem; } /// @@ -426,12 +433,14 @@ namespace MediaBrowser.Api.UserLibrary /// /// The request. /// System.Object. - public object Get(GetItem request) + public async Task Get(GetItem request) { var user = _userManager.GetUserById(request.UserId); var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _libraryManager.GetItemById(request.Id); + await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); + var dtoOptions = GetDtoOptions(request); var result = _dtoService.GetBaseItemDto(item, dtoOptions, user); @@ -439,6 +448,27 @@ namespace MediaBrowser.Api.UserLibrary return ToOptimizedSerializedResultUsingCache(result); } + private async Task RefreshItemOnDemandIfNeeded(BaseItem item) + { + if (item is Person) + { + var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); + var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; + + if (!hasMetdata) + { + var options = new MetadataRefreshOptions(_fileSystem) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = ImageRefreshMode.FullRefresh, + ForceSave = performFullRefresh + }; + + await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); + } + } + } + /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index c8dbb7bb26..4a5eb1eab7 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/MediaBrowser.Api/VideosService.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Model.Dto; namespace MediaBrowser.Api { @@ -81,11 +82,18 @@ namespace MediaBrowser.Api var dtoOptions = GetDtoOptions(request); - var video = (Video)item; - - var items = video.GetAdditionalParts() - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) - .ToArray(); + var video = item as Video; + BaseItemDto[] items; + if (video != null) + { + items = video.GetAdditionalParts() + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) + .ToArray(); + } + else + { + items = new BaseItemDto[] { }; + } var result = new ItemsResult { diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index 108eddcf9e..ced2dd5a32 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -55,7 +55,7 @@ ..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll - ..\packages\NLog.4.3.5\lib\net45\NLog.dll + ..\packages\NLog.4.3.6\lib\net45\NLog.dll True diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index dcd3a3025e..ab2aa761b5 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -429,17 +429,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks GC.Collect(2, GCCollectionMode.Forced, true); } - /// - /// Executes the task. - /// - /// The cancellation token. - /// The progress. - /// Task. - private Task ExecuteTask(CancellationToken cancellationToken, IProgress progress) - { - return Task.Run(async () => await ScheduledTask.Execute(cancellationToken, progress).ConfigureAwait(false), cancellationToken); - } - /// /// Progress_s the progress changed. /// diff --git a/MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs b/MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs index 756741e0da..77f65b0c7a 100644 --- a/MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs +++ b/MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs @@ -1,6 +1,7 @@ using MediaBrowser.Model.Serialization; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Xml; using CommonIO; @@ -24,13 +25,22 @@ namespace MediaBrowser.Common.Implementations.Serialization // Need to cache these // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html - private readonly ConcurrentDictionary _serializers = - new ConcurrentDictionary(); + private readonly Dictionary _serializers = + new Dictionary(); private System.Xml.Serialization.XmlSerializer GetSerializer(Type type) { var key = type.FullName; - return _serializers.GetOrAdd(key, k => new System.Xml.Serialization.XmlSerializer(type)); + lock (_serializers) + { + System.Xml.Serialization.XmlSerializer serializer; + if (!_serializers.TryGetValue(key, out serializer)) + { + serializer = new System.Xml.Serialization.XmlSerializer(type); + _serializers[key] = serializer; + } + return serializer; + } } /// diff --git a/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs b/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs index d1ec30210e..6281ab3edb 100644 --- a/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs +++ b/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs @@ -33,7 +33,6 @@ namespace MediaBrowser.Common.Implementations.Updates EnableKeepAlive = false, CancellationToken = cancellationToken, UserAgent = "Emby/3.0" - }; if (_cacheLength.Ticks > 0) @@ -79,6 +78,69 @@ namespace MediaBrowser.Common.Implementations.Updates }; } + private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel) + { + if (updateLevel == PackageVersionClass.Beta) + { + return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase); + } + if (updateLevel == PackageVersionClass.Dev) + { + return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || + i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); + } + + // Technically all we need to do is check that it's not pre-release + // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. + return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && + !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); + } + + public async Task> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken) + { + var list = new List(); + + var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); + + var options = new HttpRequestOptions + { + Url = url, + EnableKeepAlive = false, + CancellationToken = cancellationToken, + UserAgent = "Emby/3.0" + }; + + if (_cacheLength.Ticks > 0) + { + options.CacheMode = CacheMode.Unconditional; + options.CacheLength = _cacheLength; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + var obj = _jsonSerializer.DeserializeFromStream(stream); + + obj = obj.Where(i => (i.assets ?? new List()).Any(a => IsAsset(a, assetFilename))).ToArray(); + + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1)); + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1)); + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1)); + + return list; + } + } + + public Version GetVersion(RootObject obj) + { + Version version; + if (!Version.TryParse(obj.tag_name, out version)) + { + return new Version(1, 0); + } + + return version; + } + private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename) { Version version; diff --git a/MediaBrowser.Common.Implementations/packages.config b/MediaBrowser.Common.Implementations/packages.config index 882acc9ffd..594b4c7c5d 100644 --- a/MediaBrowser.Common.Implementations/packages.config +++ b/MediaBrowser.Common.Implementations/packages.config @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/MediaBrowser.Common/IO/StreamDefaults.cs b/MediaBrowser.Common/IO/StreamDefaults.cs index 450d293d4b..8b16d89b3b 100644 --- a/MediaBrowser.Common/IO/StreamDefaults.cs +++ b/MediaBrowser.Common/IO/StreamDefaults.cs @@ -9,11 +9,11 @@ namespace MediaBrowser.Common.IO /// /// The default copy to buffer size /// - public const int DefaultCopyToBufferSize = 262144; + public const int DefaultCopyToBufferSize = 81920; /// /// The default file stream buffer size /// - public const int DefaultFileStreamBufferSize = 262144; + public const int DefaultFileStreamBufferSize = 81920; } } diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index a0716137b9..b75accf9b0 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -211,7 +211,7 @@ namespace MediaBrowser.Common.Plugins { return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); } - catch (Exception ex) + catch { return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); } diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index 718a0d8786..e0b7204b87 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -57,10 +57,7 @@ namespace MediaBrowser.Controller.Channels catch { // Already logged at lower levels - return new QueryResult - { - - }; + return new QueryResult(); } } diff --git a/MediaBrowser.Controller/Channels/ChannelMediaInfo.cs b/MediaBrowser.Controller/Channels/ChannelMediaInfo.cs index 9424568b45..1216ae3522 100644 --- a/MediaBrowser.Controller/Channels/ChannelMediaInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelMediaInfo.cs @@ -83,8 +83,7 @@ namespace MediaBrowser.Controller.Channels { var list = new List(); - if (!string.IsNullOrWhiteSpace(info.VideoCodec) && - !string.IsNullOrWhiteSpace(info.AudioCodec)) + if (!string.IsNullOrWhiteSpace(info.VideoCodec)) { list.Add(new MediaStream { @@ -99,7 +98,10 @@ namespace MediaBrowser.Controller.Channels BitRate = info.VideoBitrate, AverageFrameRate = info.Framerate }); + } + if (!string.IsNullOrWhiteSpace(info.AudioCodec)) + { list.Add(new MediaStream { Type = MediaStreamType.Audio, diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index d627cc67ae..e69b649488 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -19,12 +19,16 @@ namespace MediaBrowser.Controller.Dto public bool EnableImages { get; set; } public bool AddProgramRecordingInfo { get; set; } public string DeviceId { get; set; } + public bool EnableUserData { get; set; } + public bool AddCurrentProgram { get; set; } public DtoOptions() { Fields = new List(); ImageTypeLimit = int.MaxValue; EnableImages = true; + EnableUserData = true; + AddCurrentProgram = true; Fields = Enum.GetNames(typeof (ItemFields)) .Select(i => (ItemFields) Enum.Parse(typeof (ItemFields), i, true)) diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index e4aa466dfd..a6f807ce9d 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,9 +1,9 @@ -using System; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using System.Collections.Generic; using System.Threading.Tasks; +using MediaBrowser.Controller.Sync; namespace MediaBrowser.Controller.Dto { @@ -43,14 +43,6 @@ namespace MediaBrowser.Controller.Dto /// Task{BaseItemDto}. BaseItemDto GetBaseItemDto(BaseItem item, List fields, User user = null, BaseItem owner = null); - /// - /// Fills the synchronize information. - /// - /// The tuples. - /// The options. - /// The user. - void FillSyncInfo(IEnumerable> tuples, DtoOptions options, User user); - /// /// Gets the base item dto. /// @@ -89,11 +81,8 @@ namespace MediaBrowser.Controller.Dto /// /// Gets the item by name dto. /// - /// The item. - /// The options. - /// The tagged items. - /// The user. - /// BaseItemDto. - BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null); + BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, Dictionary syncProgress, User user = null); + + Dictionary GetSyncedItemProgress(DtoOptions options); } } diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 588a65e98d..efc4502481 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -5,6 +5,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; using CommonIO; using MediaBrowser.Controller.Providers; @@ -67,6 +69,31 @@ namespace MediaBrowser.Controller.Entities return CreateResolveArgs(directoryService, true).FileSystemChildren; } + private List _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override IEnumerable LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null || _childrenIds.Count == 0) + { + var list = base.LoadChildren().ToList(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + private bool _requiresRefresh; public override bool RequiresRefresh() { @@ -76,7 +103,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations.ToList(); - var newLocations = CreateResolveArgs(new DirectoryService(BaseItem.FileSystem), false).PhysicalLocations.ToList(); + var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations.ToList(); if (!locations.SequenceEqual(newLocations)) { @@ -89,6 +116,8 @@ namespace MediaBrowser.Controller.Entities public override bool BeforeMetadataRefresh() { + ClearCache(); + var changed = base.BeforeMetadataRefresh() || _requiresRefresh; _requiresRefresh = false; return changed; @@ -96,9 +125,11 @@ namespace MediaBrowser.Controller.Entities private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) { + ClearCache(); + var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths , directoryService) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { FileInfo = FileSystem.GetDirectoryInfo(path), Path = path, @@ -135,7 +166,22 @@ namespace MediaBrowser.Controller.Entities return args; } - + + protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) + { + return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); + } + + protected override async Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + ClearCache(); + + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + .ConfigureAwait(false); + + ClearCache(); + } + /// /// Adds the virtual child. /// @@ -151,15 +197,6 @@ namespace MediaBrowser.Controller.Entities _virtualChildren.Add(child); } - /// - /// Get the children of this folder from the actual file system - /// - /// IEnumerable{BaseItem}. - protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) - { - return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); - } - /// /// Finds the virtual child. /// diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 1897511af7..1af55a389f 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -5,9 +5,11 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Runtime.Serialization; using System.Threading; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; namespace MediaBrowser.Controller.Entities.Audio @@ -47,7 +49,7 @@ namespace MediaBrowser.Controller.Entities.Audio } [IgnoreDataMember] - public override bool EnableForceSaveOnDateModifiedChange + public override bool EnableRefreshOnDateModifiedChange { get { return true; } } @@ -266,6 +268,11 @@ namespace MediaBrowser.Controller.Entities.Audio Size = i.Size }; + if (info.Protocol == MediaProtocol.File) + { + info.ETag = i.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + } + if (string.IsNullOrEmpty(info.Container)) { if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 6790a1bcf1..81d1deaa23 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -169,13 +169,9 @@ namespace MediaBrowser.Controller.Entities.Audio list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); - } + return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); } protected override bool GetBlockUnratedValue(UserPolicy config) { @@ -274,5 +270,54 @@ namespace MediaBrowser.Controller.Entities.Audio return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 798bc79fbb..bd991d9f47 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -18,13 +18,9 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return GetUserDataKeys()[0]; - } + return GetUserDataKeys()[0]; } [IgnoreDataMember] @@ -96,5 +92,48 @@ namespace MediaBrowser.Controller.Entities.Audio return LibraryManager.GetItemList(query); } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index c7a6b75ff2..55aaf04ffc 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -281,6 +281,20 @@ namespace MediaBrowser.Controller.Entities } } + public Task UpdateIsOffline(bool newValue) + { + var item = this; + + if (item.IsOffline != newValue) + { + item.IsOffline = newValue; + // this is creating too many repeated db updates + //return item.UpdateToRepository(ItemUpdateType.None, CancellationToken.None); + } + + return Task.FromResult(true); + } + /// /// Gets or sets the type of the location. /// @@ -290,10 +304,10 @@ namespace MediaBrowser.Controller.Entities { get { - if (IsOffline) - { - return LocationType.Offline; - } + //if (IsOffline) + //{ + // return LocationType.Offline; + //} if (string.IsNullOrWhiteSpace(Path)) { @@ -455,7 +469,7 @@ namespace MediaBrowser.Controller.Entities public DateTime DateLastRefreshed { get; set; } [IgnoreDataMember] - public virtual bool EnableForceSaveOnDateModifiedChange + public virtual bool EnableRefreshOnDateModifiedChange { get { return false; } } @@ -767,6 +781,9 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public string OfficialRating { get; set; } + [IgnoreDataMember] + public int InheritedParentalRatingValue { get; set; } + /// /// Gets or sets the critic rating. /// @@ -951,7 +968,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) ); - return LibraryManager.ResolvePaths(files, directoryService, null) + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType() .Select(audio => { @@ -981,7 +998,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => directoryService.GetFiles(i.FullName)); - return LibraryManager.ResolvePaths(files, directoryService, null) + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType protected virtual IEnumerable LoadChildren() { + //Logger.Debug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); //just load our children from the repo - the library will be validated and maintained in other processes return GetCachedChildren(); } public Task ValidateChildren(IProgress progress, CancellationToken cancellationToken) { - return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem))); + return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); } /// @@ -373,7 +375,7 @@ namespace MediaBrowser.Controller.Entities if (currentChildren.TryGetValue(child.Id, out currentChild) && IsValidFromResolver(currentChild, child)) { - await UpdateIsOffline(currentChild, false).ConfigureAwait(false); + await currentChild.UpdateIsOffline(false).ConfigureAwait(false); validChildren.Add(currentChild); continue; @@ -402,7 +404,7 @@ namespace MediaBrowser.Controller.Entities else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path)) { - await UpdateIsOffline(item, true).ConfigureAwait(false); + await item.UpdateIsOffline(true).ConfigureAwait(false); } else { @@ -459,17 +461,6 @@ namespace MediaBrowser.Controller.Entities progress.Report(100); } - private Task UpdateIsOffline(BaseItem item, bool newValue) - { - if (item.IsOffline != newValue) - { - item.IsOffline = newValue; - return item.UpdateToRepository(ItemUpdateType.None, CancellationToken.None); - } - - return Task.FromResult(true); - } - private async Task RefreshMetadataRecursive(MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { var children = ActualChildren.ToList(); @@ -643,8 +634,9 @@ namespace MediaBrowser.Controller.Entities protected virtual IEnumerable GetNonCachedChildren(IDirectoryService directoryService) { var collectionType = LibraryManager.GetContentType(this); + var libraryOptions = LibraryManager.GetLibraryOptions(this); - return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, collectionType); + return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType); } /// @@ -699,7 +691,7 @@ namespace MediaBrowser.Controller.Entities items = GetRecursiveChildren(user, query); } - return PostFilterAndSort(items, query); + return PostFilterAndSort(items, query, true, true); } if (!(this is UserRootFolder) && !(this is AggregateFolder)) @@ -883,6 +875,15 @@ namespace MediaBrowser.Controller.Entities return true; } + if (query.IsPlayed.HasValue) + { + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name)) + { + Logger.Debug("Query requires post-filtering due to IsPlayed"); + return true; + } + } + return false; } @@ -890,8 +891,16 @@ namespace MediaBrowser.Controller.Entities { if (query.ItemIds.Length > 0) { - var specificItems = query.ItemIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); - return Task.FromResult(PostFilterAndSort(specificItems, query)); + var result = LibraryManager.GetItemsResult(query); + + if (query.SortBy.Length == 0) + { + var ids = query.ItemIds.ToList(); + + // Try to preserve order + result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray(); + } + return Task.FromResult(result); } return GetItemsInternal(query); @@ -919,10 +928,7 @@ namespace MediaBrowser.Controller.Entities catch { // Already logged at lower levels - return new QueryResult - { - - }; + return new QueryResult(); } } @@ -950,12 +956,12 @@ namespace MediaBrowser.Controller.Entities : GetChildren(user, true).Where(filter); } - return PostFilterAndSort(items, query); + return PostFilterAndSort(items, query, true, true); } - protected QueryResult PostFilterAndSort(IEnumerable items, InternalItemsQuery query) + protected QueryResult PostFilterAndSort(IEnumerable items, InternalItemsQuery query, bool collapseBoxSetItems, bool enableSorting) { - return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager, ConfigurationManager); + return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager, ConfigurationManager, collapseBoxSetItems, enableSorting); } public virtual IEnumerable GetChildren(User user, bool includeLinkedChildren) @@ -1419,7 +1425,7 @@ namespace MediaBrowser.Controller.Entities itemDto.RecursiveItemCount = allItemsQueryResult.TotalRecordCount; } - double recursiveItemCount = allItemsQueryResult.TotalRecordCount; + var recursiveItemCount = allItemsQueryResult.TotalRecordCount; double unplayedCount = unplayedQueryResult.TotalRecordCount; if (recursiveItemCount > 0) @@ -1429,6 +1435,14 @@ namespace MediaBrowser.Controller.Entities dto.Played = dto.PlayedPercentage.Value >= 100; dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; } + + if (itemDto != null) + { + if (this is Season || this is MusicAlbum) + { + itemDto.ChildCount = recursiveItemCount; + } + } } } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs index 54386a1795..24910498f5 100644 --- a/MediaBrowser.Controller/Entities/Game.cs +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities } [IgnoreDataMember] - public override bool EnableForceSaveOnDateModifiedChange + public override bool EnableRefreshOnDateModifiedChange { get { return true; } } diff --git a/MediaBrowser.Controller/Entities/GameGenre.cs b/MediaBrowser.Controller/Entities/GameGenre.cs index 45e766c0f0..6448828fb3 100644 --- a/MediaBrowser.Controller/Entities/GameGenre.cs +++ b/MediaBrowser.Controller/Entities/GameGenre.cs @@ -16,12 +16,9 @@ namespace MediaBrowser.Controller.Entities return list; } - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return GetUserDataKeys()[0]; - } + return GetUserDataKeys()[0]; } /// @@ -87,5 +84,48 @@ namespace MediaBrowser.Controller.Entities return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GameGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index cc5aebb2a4..1736ba8c76 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -19,13 +19,9 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return GetUserDataKeys()[0]; - } + return GetUserDataKeys()[0]; } /// @@ -91,5 +87,48 @@ namespace MediaBrowser.Controller.Entities return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs index 2267480985..a38b7394dd 100644 --- a/MediaBrowser.Controller/Entities/IHasImages.cs +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -1,5 +1,4 @@ -using System; -using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/Entities/IHasMetadata.cs b/MediaBrowser.Controller/Entities/IHasMetadata.cs index 378c4a390b..cf2f7db64b 100644 --- a/MediaBrowser.Controller/Entities/IHasMetadata.cs +++ b/MediaBrowser.Controller/Entities/IHasMetadata.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Entities /// /// The date last refreshed. DateTime DateLastRefreshed { get; set; } - + /// /// This is called before any metadata refresh and returns true or false indicating if changes were made /// @@ -52,6 +52,15 @@ namespace MediaBrowser.Controller.Entities bool RequiresRefresh(); - bool EnableForceSaveOnDateModifiedChange { get; } + bool EnableRefreshOnDateModifiedChange { get; } + + string PresentationUniqueKey { get; set; } + + string GetPresentationUniqueKey(); + string CreatePresentationUniqueKey(); + bool StopRefreshIfLocalMetadataFound { get; } + + int? GetInheritedParentalRatingValue(); + int InheritedParentalRatingValue { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 2ac4af1af7..b69c8bdfcc 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 69cab5ec53..deea631127 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -37,6 +37,7 @@ namespace MediaBrowser.Controller.Entities public string[] Genres { get; set; } public string[] Keywords { get; set; } + public bool? IsSpecialSeason { get; set; } public bool? IsMissing { get; set; } public bool? IsUnaired { get; set; } public bool? IsVirtualUnaired { get; set; } @@ -50,6 +51,7 @@ namespace MediaBrowser.Controller.Entities public string PresentationUniqueKey { get; set; } public string Path { get; set; } + public string PathNotStartsWith { get; set; } public string Name { get; set; } public string SlugName { get; set; } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 05d23d9866..2ba6842ca3 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -11,11 +11,13 @@ namespace MediaBrowser.Controller.Entities public int? MaxListOrder { get; set; } public Guid AppearsInItemId { get; set; } public string NameContains { get; set; } + public SourceType[] SourceTypes { get; set; } public InternalPeopleQuery() { PersonTypes = new List(); ExcludePersonTypes = new List(); + SourceTypes = new SourceType[] { }; } } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 4effc162e4..6d5278f1fe 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -62,6 +62,26 @@ namespace MediaBrowser.Controller.Entities.Movies return UnratedItem.Movie; } + protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) + { + if (IsLegacyBoxSet) + { + return base.GetNonCachedChildren(directoryService); + } + return new List(); + } + + protected override IEnumerable LoadChildren() + { + if (IsLegacyBoxSet) + { + return base.LoadChildren(); + } + + // Save a trip to the database + return new List(); + } + [IgnoreDataMember] public override bool IsPreSorted { @@ -76,7 +96,21 @@ namespace MediaBrowser.Controller.Entities.Movies { get { - return true; + if (IsLegacyBoxSet) + { + return true; + } + + return false; + } + } + + [IgnoreDataMember] + private bool IsLegacyBoxSet + { + get + { + return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); } } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index c7a833c58f..f0270497c0 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -179,5 +179,15 @@ namespace MediaBrowser.Controller.Entities.Movies return list; } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 7119828e26..8b749b7a5d 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,7 +1,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; using System.Collections.Generic; using System.Runtime.Serialization; diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 8ef0d70bf9..f21bc0a71e 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -26,13 +26,9 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return GetUserDataKeys()[0]; - } + return GetUserDataKeys()[0]; } public PersonLookupInfo GetLookupInfo() @@ -126,6 +122,64 @@ namespace MediaBrowser.Controller.Entities return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validFilename = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + string subFolderPrefix = null; + + foreach (char c in validFilename) + { + if (char.IsLetterOrDigit(c)) + { + subFolderPrefix = c.ToString(); + break; + } + } + + var path = ConfigurationManager.ApplicationPaths.PeoplePath; + + return string.IsNullOrEmpty(subFolderPrefix) ? + System.IO.Path.Combine(path, validFilename) : + System.IO.Path.Combine(path, subFolderPrefix, validFilename); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } /// diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 804ea04a59..965616eb53 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities } [IgnoreDataMember] - public override bool EnableForceSaveOnDateModifiedChange + public override bool EnableRefreshOnDateModifiedChange { get { return true; } } diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 762798b553..04b09b7442 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -18,13 +18,9 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return GetUserDataKeys()[0]; - } + return GetUserDataKeys()[0]; } /// @@ -89,5 +85,48 @@ namespace MediaBrowser.Controller.Entities return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 726390f65e..1a02043d6f 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -135,7 +135,11 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public Series Series { - get { return FindParent(); } + get + { + var seriesId = SeriesId ?? FindSeriesId(); + return seriesId.HasValue ? (LibraryManager.GetItemById(seriesId.Value) as Series) : null; + } } [IgnoreDataMember] @@ -143,24 +147,8 @@ namespace MediaBrowser.Controller.Entities.TV { get { - var season = FindParent(); - - // Episodes directly in series folder - if (season == null) - { - var series = Series; - - if (series != null && ParentIndexNumber.HasValue) - { - var findNumber = ParentIndexNumber.Value; - - season = series.Children - .OfType() - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber); - } - } - - return season; + var seasonId = SeasonId ?? FindSeasonId(); + return seasonId.HasValue ? (LibraryManager.GetItemById(seasonId.Value) as Season) : null; } } @@ -193,7 +181,23 @@ namespace MediaBrowser.Controller.Entities.TV public Guid? FindSeasonId() { - var season = Season; + var season = FindParent(); + + // Episodes directly in series folder + if (season == null) + { + var series = Series; + + if (series != null && ParentIndexNumber.HasValue) + { + var findNumber = ParentIndexNumber.Value; + + season = series.Children + .OfType() + .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber); + } + } + return season == null ? (Guid?)null : season.Id; } @@ -263,7 +267,7 @@ namespace MediaBrowser.Controller.Entities.TV public Guid? FindSeriesId() { - var series = Series; + var series = FindParent(); return series == null ? (Guid?)null : series.Id; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index ee01c60b1b..65b7c9955b 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,6 +1,5 @@ using System; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Users; using MoreLinq; @@ -86,7 +85,11 @@ namespace MediaBrowser.Controller.Entities.TV public override int GetChildCount(User user) { - return GetChildren(user, true).Count(); + Logger.Debug("Season {0} getting child cound", (Path ?? Name)); + var result = GetChildren(user, true).Count(); + Logger.Debug("Season {0} child cound: ", result); + + return result; } /// @@ -96,7 +99,11 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public Series Series { - get { return FindParent(); } + get + { + var seriesId = SeriesId ?? FindSeriesId(); + return seriesId.HasValue ? (LibraryManager.GetItemById(seriesId.Value) as Series) : null; + } } [IgnoreDataMember] @@ -115,22 +122,18 @@ namespace MediaBrowser.Controller.Entities.TV } } - [IgnoreDataMember] - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get + if (IndexNumber.HasValue) { - if (IndexNumber.HasValue) + var series = Series; + if (series != null) { - var series = Series; - if (series != null) - { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); - } + return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); } - - return base.PresentationUniqueKey; } + + return base.CreatePresentationUniqueKey(); } /// @@ -142,24 +145,6 @@ namespace MediaBrowser.Controller.Entities.TV return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name; } - [IgnoreDataMember] - public bool IsMissingSeason - { - get { return (IsVirtualItem) && !IsUnaired; } - } - - [IgnoreDataMember] - public bool IsVirtualUnaired - { - get { return (IsVirtualItem) && IsUnaired; } - } - - [IgnoreDataMember] - public bool IsSpecialSeason - { - get { return (IndexNumber ?? -1) == 0; } - } - protected override Task> GetItemsInternal(InternalItemsQuery query) { if (query.User == null) @@ -171,10 +156,15 @@ namespace MediaBrowser.Controller.Entities.TV Func filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); + var id = Guid.NewGuid().ToString("N"); + + Logger.Debug("Season.GetItemsInternal entering GetEpisodes. Request id: " + id); var items = GetEpisodes(user).Where(filter); - var result = PostFilterAndSort(items, query); + Logger.Debug("Season.GetItemsInternal entering PostFilterAndSort. Request id: " + id); + var result = PostFilterAndSort(items, query, false, false); + Logger.Debug("Season.GetItemsInternal complete. Request id: " + id); return Task.FromResult(result); } @@ -185,19 +175,17 @@ namespace MediaBrowser.Controller.Entities.TV /// IEnumerable{Episode}. public IEnumerable GetEpisodes(User user) { - var config = user.Configuration; - - return GetEpisodes(Series, user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); + return GetEpisodes(Series, user); } - public IEnumerable GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes) + public IEnumerable GetEpisodes(Series series, User user) { - return GetEpisodes(series, user, includeMissingEpisodes, includeVirtualUnairedEpisodes, null); + return GetEpisodes(series, user, null); } - public IEnumerable GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable allSeriesEpisodes) + public IEnumerable GetEpisodes(Series series, User user, IEnumerable allSeriesEpisodes) { - return series.GetEpisodes(user, this, includeMissingEpisodes, includeVirtualUnairedEpisodes, allSeriesEpisodes); + return series.GetSeasonEpisodes(user, this, allSeriesEpisodes); } public IEnumerable GetEpisodes() @@ -257,7 +245,7 @@ namespace MediaBrowser.Controller.Entities.TV public Guid? FindSeriesId() { - var series = Series; + var series = FindParent(); return series == null ? (Guid?)null : series.Id; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index ad35b3d369..4915cfedc7 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -96,19 +96,29 @@ namespace MediaBrowser.Controller.Entities.TV } } - [IgnoreDataMember] - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get + var userdatakeys = GetUserDataKeys(); + + if (userdatakeys.Count > 1) { - var userdatakeys = GetUserDataKeys(); + return AddLibrariesToPresentationUniqueKey(userdatakeys[0]); + } + return base.CreatePresentationUniqueKey(); + } - if (userdatakeys.Count > 1) - { - return userdatakeys[0]; - } - return base.PresentationUniqueKey; + private string AddLibrariesToPresentationUniqueKey(string key) + { + var folders = LibraryManager.GetCollectionFolders(this) + .Select(i => i.Id.ToString("N")) + .ToArray(); + + if (folders.Length == 0) + { + return key; } + + return key + "-" + string.Join("-", folders); } private static string GetUniqueSeriesKey(BaseItem series) @@ -117,7 +127,7 @@ namespace MediaBrowser.Controller.Entities.TV { return series.Id.ToString("N"); } - return series.PresentationUniqueKey; + return series.GetPresentationUniqueKey(); } public override int GetChildCount(User user) @@ -197,7 +207,30 @@ namespace MediaBrowser.Controller.Entities.TV { var config = user.Configuration; - return GetSeasons(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); + var seriesKey = GetUniqueSeriesKey(this); + + Logger.Debug("GetSeasons SeriesKey: {0}", seriesKey); + var query = new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] {typeof (Season).Name}, + SortBy = new[] {ItemSortBy.SortName} + }; + + if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes) + { + query.IsVirtualItem = false; + } + else if (!config.DisplayMissingEpisodes) + { + query.IsMissing = false; + } + else if (!config.DisplayUnairedEpisodes) + { + query.IsVirtualUnaired = false; + } + + return LibraryManager.GetItemList(query).Cast(); } protected override Task> GetItemsInternal(InternalItemsQuery query) @@ -227,55 +260,43 @@ namespace MediaBrowser.Controller.Entities.TV Func filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); var items = GetSeasons(user).Where(filter); - var result = PostFilterAndSort(items, query); + var result = PostFilterAndSort(items, query, false, true); return Task.FromResult(result); } - public IEnumerable GetSeasons(User user, bool includeMissingSeasons, bool includeVirtualUnaired) + public IEnumerable GetEpisodes(User user) { - IEnumerable seasons; + var seriesKey = GetUniqueSeriesKey(this); + Logger.Debug("GetEpisodes seriesKey: {0}", seriesKey); - seasons = LibraryManager.GetItemList(new InternalItemsQuery(user) + var query = new InternalItemsQuery(user) { - AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(this), - IncludeItemTypes = new[] { typeof(Season).Name }, - SortBy = new[] { ItemSortBy.SortName } - - }).Cast(); - - if (!includeMissingSeasons) + AncestorWithPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] {typeof (Episode).Name, typeof (Season).Name}, + SortBy = new[] {ItemSortBy.SortName} + }; + var config = user.Configuration; + if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes) { - seasons = seasons.Where(i => !(i.IsMissingSeason)); + query.IsVirtualItem = false; } - if (!includeVirtualUnaired) + else if (!config.DisplayMissingEpisodes) { - seasons = seasons.Where(i => !i.IsVirtualUnaired); + query.IsMissing = false; } - - return seasons; - } - - public IEnumerable GetEpisodes(User user) - { - var config = user.Configuration; - - return GetEpisodes(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); - } - - public IEnumerable GetEpisodes(User user, bool includeMissing, bool includeVirtualUnaired) - { - var allItems = LibraryManager.GetItemList(new InternalItemsQuery(user) + else if (!config.DisplayUnairedEpisodes) { - AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(this), - IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }, - SortBy = new[] { ItemSortBy.SortName } + query.IsVirtualUnaired = false; + } - }).ToList(); + var allItems = LibraryManager.GetItemList(query).ToList(); + + Logger.Debug("GetEpisodes return {0} items from database", allItems.Count); var allSeriesEpisodes = allItems.OfType().ToList(); var allEpisodes = allItems.OfType() - .SelectMany(i => i.GetEpisodes(this, user, includeMissing, includeVirtualUnaired, allSeriesEpisodes)) + .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes)) .Reverse() .ToList(); @@ -352,78 +373,68 @@ namespace MediaBrowser.Controller.Entities.TV progress.Report(100); } - public IEnumerable GetEpisodes(User user, Season season) - { - var config = user.Configuration; - - return GetEpisodes(user, season, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes); - } - private IEnumerable GetAllEpisodes(User user) { - return LibraryManager.GetItemList(new InternalItemsQuery(user) + Logger.Debug("Series.GetAllEpisodes entering GetItemList"); + + var result = LibraryManager.GetItemList(new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(this), IncludeItemTypes = new[] { typeof(Episode).Name }, SortBy = new[] { ItemSortBy.SortName } - }).Cast(); - } + }).Cast().ToList(); - public IEnumerable GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes) - { - IEnumerable episodes = GetAllEpisodes(user); + Logger.Debug("Series.GetAllEpisodes returning {0} episodes", result.Count); - return GetEpisodes(user, parentSeason, includeMissingEpisodes, includeVirtualUnairedEpisodes, episodes); + return result; } - public IEnumerable GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable allSeriesEpisodes) + public IEnumerable GetSeasonEpisodes(User user, Season parentSeason) { - if (allSeriesEpisodes == null) + var seriesKey = GetUniqueSeriesKey(this); + Logger.Debug("GetSeasonEpisodes seriesKey: {0}", seriesKey); + + var query = new InternalItemsQuery(user) { - return GetEpisodes(user, parentSeason, includeMissingEpisodes, includeVirtualUnairedEpisodes); + AncestorWithPresentationUniqueKey = seriesKey, + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.SortName } + }; + var config = user.Configuration; + if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes) + { + query.IsVirtualItem = false; } - - var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons); - - if (!includeMissingEpisodes) + else if (!config.DisplayMissingEpisodes) { - episodes = episodes.Where(i => !i.IsMissingEpisode); + query.IsMissing = false; } - if (!includeVirtualUnairedEpisodes) + else if (!config.DisplayUnairedEpisodes) { - episodes = episodes.Where(i => !i.IsVirtualUnaired); + query.IsVirtualUnaired = false; } - var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder; + var allItems = LibraryManager.GetItemList(query).OfType(); - return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending) - .Cast(); + return GetSeasonEpisodes(user, parentSeason, allItems); } - /// - /// Filters the episodes by season. - /// - public static IEnumerable FilterEpisodesBySeason(IEnumerable episodes, int seasonNumber, bool includeSpecials) + public IEnumerable GetSeasonEpisodes(User user, Season parentSeason, IEnumerable allSeriesEpisodes) { - if (!includeSpecials || seasonNumber < 1) + if (allSeriesEpisodes == null) { - return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber); + Logger.Debug("GetSeasonEpisodes allSeriesEpisodes is null"); + return GetSeasonEpisodes(user, parentSeason); } - return episodes.Where(i => - { - var episode = i; - - if (episode != null) - { - var currentSeasonNumber = episode.AiredSeasonNumber; + Logger.Debug("GetSeasonEpisodes FilterEpisodesBySeason"); + var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons); - return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber; - } + var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder; - return false; - }); + return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending) + .Cast(); } /// @@ -454,6 +465,32 @@ namespace MediaBrowser.Controller.Entities.TV }); } + /// + /// Filters the episodes by season. + /// + public static IEnumerable FilterEpisodesBySeason(IEnumerable episodes, int seasonNumber, bool includeSpecials) + { + if (!includeSpecials || seasonNumber < 1) + { + return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber); + } + + return episodes.Where(i => + { + var episode = i; + + if (episode != null) + { + var currentSeasonNumber = episode.AiredSeasonNumber; + + return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber; + } + + return false; + }); + } + + protected override bool GetBlockUnratedValue(UserPolicy config) { return config.BlockUnratedItems.Contains(UnratedItem.Series); @@ -509,5 +546,15 @@ namespace MediaBrowser.Controller.Entities.TV return list; } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } } } diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index eab5ab6794..306ce35ece 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -2,9 +2,7 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System.Collections.Generic; -using System.Globalization; using System.Runtime.Serialization; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Entities @@ -126,5 +124,15 @@ namespace MediaBrowser.Controller.Entities return list; } + + [IgnoreDataMember] + public override bool StopRefreshIfLocalMetadataFound + { + get + { + // Need people id's from internet metadata + return false; + } + } } } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 5c68308f5c..46da469fa5 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -213,7 +213,7 @@ namespace MediaBrowser.Controller.Entities Name = newName; - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)) + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) { ReplaceAllMetadata = true, ImageRefreshMode = ImageRefreshMode.FullRefresh, diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 8e6f11c2ce..aff1f5e286 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,6 +1,5 @@ using System.Runtime.Serialization; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using System; @@ -8,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Entities { @@ -18,6 +16,31 @@ namespace MediaBrowser.Controller.Entities /// public class UserRootFolder : Folder { + private List _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override IEnumerable LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null) + { + var list = base.LoadChildren().ToList(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + protected override async Task> GetItemsInternal(InternalItemsQuery query) { if (query.Recursive) @@ -35,7 +58,7 @@ namespace MediaBrowser.Controller.Entities var user = query.User; Func filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); - return PostFilterAndSort(result.Where(filter), query); + return PostFilterAndSort(result.Where(filter), query, true, true); } public override int GetChildCount(User user) @@ -71,6 +94,8 @@ namespace MediaBrowser.Controller.Entities public override bool BeforeMetadataRefresh() { + ClearCache(); + var hasChanges = base.BeforeMetadataRefresh(); if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase)) @@ -82,11 +107,22 @@ namespace MediaBrowser.Controller.Entities return hasChanges; } + protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) + { + ClearCache(); + + return base.GetNonCachedChildren(directoryService); + } + protected override async Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { + ClearCache(); + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) .ConfigureAwait(false); + ClearCache(); + // Not the best way to handle this, but it solves an issue // CollectionFolders aren't always getting saved after changes // This means that grabbing the item by Id may end up returning the old one diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index e2228bcaf6..9f3acc3fc3 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; @@ -14,12 +13,10 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Configuration; using MoreLinq; namespace MediaBrowser.Controller.Entities @@ -427,7 +424,7 @@ namespace MediaBrowser.Controller.Entities query.SortBy = new string[] { }; - return PostFilterAndSort(items, parent, null, query); + return PostFilterAndSort(items, parent, null, query, false, true); } private QueryResult GetFavoriteSongs(Folder parent, User user, InternalItemsQuery query) @@ -783,7 +780,7 @@ namespace MediaBrowser.Controller.Entities { items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager)); - return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config); + return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config, true, true); } public static bool FilterItem(BaseItem item, InternalItemsQuery query) @@ -794,9 +791,11 @@ namespace MediaBrowser.Controller.Entities private QueryResult PostFilterAndSort(IEnumerable items, BaseItem queryParent, int? totalRecordLimit, - InternalItemsQuery query) + InternalItemsQuery query, + bool collapseBoxSetItems, + bool enableSorting) { - return PostFilterAndSort(items, queryParent, totalRecordLimit, query, _libraryManager, _config); + return PostFilterAndSort(items, queryParent, totalRecordLimit, query, _libraryManager, _config, collapseBoxSetItems, enableSorting); } public static QueryResult PostFilterAndSort(IEnumerable items, @@ -804,7 +803,9 @@ namespace MediaBrowser.Controller.Entities int? totalRecordLimit, InternalItemsQuery query, ILibraryManager libraryManager, - IServerConfigurationManager configurationManager) + IServerConfigurationManager configurationManager, + bool collapseBoxSetItems, + bool enableSorting) { var user = query.User; @@ -813,7 +814,10 @@ namespace MediaBrowser.Controller.Entities query.IsVirtualUnaired, query.IsUnaired); - items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user, configurationManager); + if (collapseBoxSetItems) + { + items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user, configurationManager); + } // This must be the last filter if (!string.IsNullOrEmpty(query.AdjacentTo)) @@ -821,7 +825,7 @@ namespace MediaBrowser.Controller.Entities items = FilterForAdjacency(items, query.AdjacentTo); } - return Sort(items, totalRecordLimit, query, libraryManager); + return SortAndPage(items, totalRecordLimit, query, libraryManager, enableSorting); } public static IEnumerable CollapseBoxSetItemsIfNeeded(IEnumerable items, @@ -1096,8 +1100,6 @@ namespace MediaBrowser.Controller.Entities bool? isVirtualUnaired, bool? isUnaired) { - items = FilterVirtualSeasons(items, isMissing, isVirtualUnaired, isUnaired); - if (isMissing.HasValue) { var val = isMissing.Value; @@ -1143,65 +1145,14 @@ namespace MediaBrowser.Controller.Entities return items; } - private static IEnumerable FilterVirtualSeasons( - IEnumerable items, - bool? isMissing, - bool? isVirtualUnaired, - bool? isUnaired) - { - if (isMissing.HasValue) - { - var val = isMissing.Value; - items = items.Where(i => - { - var e = i as Season; - if (e != null) - { - return (e.IsMissingSeason) == val; - } - return true; - }); - } - - if (isUnaired.HasValue) - { - var val = isUnaired.Value; - items = items.Where(i => - { - var e = i as Season; - if (e != null) - { - return e.IsUnaired == val; - } - return true; - }); - } - - if (isVirtualUnaired.HasValue) - { - var val = isVirtualUnaired.Value; - items = items.Where(i => - { - var e = i as Season; - if (e != null) - { - return e.IsVirtualUnaired == val; - } - return true; - }); - } - - return items; - } - - public static QueryResult Sort(IEnumerable items, + public static QueryResult SortAndPage(IEnumerable items, int? totalRecordLimit, InternalItemsQuery query, - ILibraryManager libraryManager) + ILibraryManager libraryManager, bool enableSorting) { var user = query.User; - items = items.DistinctBy(i => i.PresentationUniqueKey, StringComparer.OrdinalIgnoreCase); + items = items.DistinctBy(i => i.GetPresentationUniqueKey(), StringComparer.OrdinalIgnoreCase); if (query.SortBy.Length > 0) { diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index eba1e466a2..8809f155c3 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -12,6 +12,7 @@ using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; namespace MediaBrowser.Controller.Entities @@ -44,24 +45,23 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get + if (!string.IsNullOrWhiteSpace(PrimaryVersionId)) { - if (!string.IsNullOrWhiteSpace(PrimaryVersionId)) - { - return PrimaryVersionId; - } - - return base.PresentationUniqueKey; + return PrimaryVersionId; } + + return base.CreatePresentationUniqueKey(); } [IgnoreDataMember] - public override bool EnableForceSaveOnDateModifiedChange + public override bool EnableRefreshOnDateModifiedChange { - get { return true; } + get + { + return VideoType == VideoType.VideoFile || VideoType == VideoType.Iso; + } } public int? TotalBitrate { get; set; } @@ -612,6 +612,11 @@ namespace MediaBrowser.Controller.Entities SupportsDirectStream = i.VideoType == VideoType.VideoFile }; + if (info.Protocol == MediaProtocol.File) + { + info.ETag = i.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + } + if (i.IsShortcut) { info.Path = i.ShortcutPath; diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index db896f1fc7..4197ea93e5 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -112,5 +112,48 @@ namespace MediaBrowser.Controller.Entities return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs b/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs index daa670d836..9a5b96a241 100644 --- a/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs +++ b/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs @@ -1,5 +1,7 @@ -using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.FileOrganization; using MediaBrowser.Model.Querying; +using System; using System.Threading; using System.Threading.Tasks; @@ -7,6 +9,11 @@ namespace MediaBrowser.Controller.FileOrganization { public interface IFileOrganizationService { + event EventHandler> ItemAdded; + event EventHandler> ItemUpdated; + event EventHandler> ItemRemoved; + event EventHandler LogReset; + /// /// Processes the new files. /// @@ -81,5 +88,20 @@ namespace MediaBrowser.Controller.FileOrganization /// Item name. /// The match string to delete. void DeleteSmartMatchEntry(string ItemName, string matchString); + + /// + /// Attempts to add a an item to the list of currently processed items. + /// + /// The result item. + /// Passing true will notify the client to reload all items, otherwise only a single item will be refreshed. + /// True if the item was added, False if the item is already contained in the list. + bool AddToInProgressList(FileOrganizationResult result, bool fullClientRefresh); + + /// + /// Removes an item from the list of currently processed items. + /// + /// The result item. + /// True if the item was removed, False if the item was not contained in the list. + bool RemoveFromInprogressList(FileOrganizationResult result); } } diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index c07934d0b1..c89a60a6f8 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -106,5 +106,7 @@ namespace MediaBrowser.Controller /// /// The internal metadata path. string InternalMetadataPath { get; } + + string ArtistsPath { get; } } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index ad38b9ea5e..0862e3eaf9 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -11,6 +11,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Library @@ -32,15 +34,11 @@ namespace MediaBrowser.Controller.Library /// /// Resolves a set of files into a list of BaseItem /// - /// The files. - /// The directory service. - /// The parent. - /// Type of the collection. - /// List{``0}. IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, - Folder parent, string - collectionType = null); + Folder parent, + LibraryOptions libraryOptions, + string collectionType = null); /// /// Gets the root folder. @@ -397,6 +395,9 @@ namespace MediaBrowser.Controller.Library /// true if [is audio file] [the specified path]; otherwise, false. bool IsAudioFile(string path); + bool IsAudioFile(string path, LibraryOptions libraryOptions); + bool IsVideoFile(string path, LibraryOptions libraryOptions); + /// /// Gets the season number from path. /// @@ -453,6 +454,8 @@ namespace MediaBrowser.Controller.Library /// IEnumerable<Folder>. IEnumerable GetCollectionFolders(BaseItem item); + LibraryOptions GetLibraryOptions(BaseItem item); + /// /// Gets the people. /// @@ -474,12 +477,6 @@ namespace MediaBrowser.Controller.Library /// List<Person>. List GetPeopleItems(InternalPeopleQuery query); - /// - /// Gets all people names. - /// - /// List<System.String>. - List GetAllPeople(); - /// /// Updates the people. /// @@ -557,7 +554,7 @@ namespace MediaBrowser.Controller.Library /// true if XXXX, false otherwise. bool IgnoreFile(FileSystemMetadata file, BaseItem parent); - void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, bool refreshLibrary); + void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, LibraryOptions options, bool refreshLibrary); void RemoveVirtualFolder(string name, bool refreshLibrary); void AddMediaPath(string virtualFolderName, string path); void RemoveMediaPath(string virtualFolderName, string path); @@ -568,5 +565,6 @@ namespace MediaBrowser.Controller.Library QueryResult> GetStudios(InternalItemsQuery query); QueryResult> GetArtists(InternalItemsQuery query); QueryResult> GetAlbumArtists(InternalItemsQuery query); + QueryResult> GetAllArtists(InternalItemsQuery query); } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index ea3199b318..ec0ac325bc 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using CommonIO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Library { @@ -51,6 +53,13 @@ namespace MediaBrowser.Controller.Library } } + public LibraryOptions LibraryOptions { get; set; } + + public LibraryOptions GetLibraryOptions() + { + return LibraryOptions ?? (LibraryOptions = (Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent))); + } + /// /// Gets or sets the file system dictionary. /// diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index fe69b38cb0..ed64127c38 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -331,12 +331,11 @@ namespace MediaBrowser.Controller.LiveTv /// The user. /// Task. Task AddInfoToProgramDto(List> programs, List fields, User user = null); + /// /// Saves the tuner host. /// - /// The information. - /// Task. - Task SaveTunerHost(TunerHostInfo info); + Task SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); /// /// Saves the listing provider. /// diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 74c9932480..c7f47b825b 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs b/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs index a8c7376734..f4dba070dc 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs index 0e1a054754..642dee3af8 100644 --- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.LiveTv +namespace MediaBrowser.Controller.LiveTv { public class TimerEventInfo { diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs index da0527eeaf..c1d1c413e8 100644 --- a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs +++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.LiveTv +namespace MediaBrowser.Controller.LiveTv { public class TunerChannelMapping { diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 0462117cb5..e7eaa1dc0b 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -236,6 +236,7 @@ + diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index 44b7417553..844fafcdec 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -1,5 +1,4 @@ -using System.Linq; -using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dlna; namespace MediaBrowser.Controller.MediaEncoding { diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index c8a8caa917..7fdbf020ce 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -1,7 +1,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using System; -using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; @@ -134,5 +133,7 @@ namespace MediaBrowser.Controller.MediaEncoding Task Init(); Task UpdateEncoderPath(string path, string pathType); + bool SupportsEncoder(string encoder); + bool IsDefaultEncoderPath { get; } } } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 66a9fa60b8..96a3753e19 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -36,8 +36,13 @@ namespace MediaBrowser.Controller.MediaEncoding return new[] {videoPath}; } - public static List GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, IEnumerable filenames) + private static List GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, List filenames) { + if (filenames.Count == 0) + { + return new List(); + } + var allFiles = fileSystem .GetFilePaths(rootPath, true) .ToList(); diff --git a/MediaBrowser.Controller/Net/IAsyncStreamSource.cs b/MediaBrowser.Controller/Net/IAsyncStreamSource.cs new file mode 100644 index 0000000000..0f41f60a55 --- /dev/null +++ b/MediaBrowser.Controller/Net/IAsyncStreamSource.cs @@ -0,0 +1,18 @@ +using ServiceStack.Web; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Interface IAsyncStreamSource + /// Enables asynchronous writing to http resonse streams + /// + public interface IAsyncStreamSource + { + /// + /// Asynchronously write to the response stream. + /// + Task WriteToAsync(Stream responseStream); + } +} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs index 49d4614d81..8fdb1ce37e 100644 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Net /// System.Object. object GetResult(object content, string contentType, IDictionary responseHeaders = null); - object GetAsyncStreamWriter(Func streamWriter, IDictionary responseHeaders = null); + object GetAsyncStreamWriter(IAsyncStreamSource streamSource); /// /// Gets the optimized result. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 78138999c5..87937869d5 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -169,6 +169,13 @@ namespace MediaBrowser.Controller.Persistence QueryResult> GetStudios(InternalItemsQuery query); QueryResult> GetArtists(InternalItemsQuery query); QueryResult> GetAlbumArtists(InternalItemsQuery query); + QueryResult> GetAllArtists(InternalItemsQuery query); + + List GetGameGenreNames(); + List GetMusicGenreNames(); + List GetStudioNames(); + List GetGenreNames(); + List GetAllArtistNames(); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 5ffe3d5daf..654b97d7d4 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.Threading.Tasks; +using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Playlists { @@ -58,11 +59,22 @@ namespace MediaBrowser.Controller.Playlists return true; } + protected override IEnumerable LoadChildren() + { + // Save a trip to the database + return new List(); + } + public override IEnumerable GetChildren(User user, bool includeLinkedChildren) { return GetPlayableItems(user).Result; } + protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) + { + return new List(); + } + public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) { var items = GetPlayableItems(user).Result; diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index a783910e34..c0912708c3 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -724,6 +724,15 @@ namespace MediaBrowser.Controller.Providers } break; } + case "TvMazeId": + { + var id = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(id)) + { + item.SetProviderId(MetadataProviders.TvMaze, id); + } + break; + } case "AudioDbArtistId": { var id = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index e7e3323c2b..ee2b28c60a 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -16,13 +16,16 @@ namespace MediaBrowser.Controller.Providers private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); - public DirectoryService(ILogger logger, IFileSystem fileSystem) + private readonly ConcurrentDictionary _fileCache = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + public DirectoryService(ILogger logger, IFileSystem fileSystem) { _logger = logger; _fileSystem = fileSystem; } - public DirectoryService(IFileSystem fileSystem) + public DirectoryService(IFileSystem fileSystem) : this(new NullLogger(), fileSystem) { } @@ -100,20 +103,19 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata GetFile(string path) { - var directory = Path.GetDirectoryName(path); - - if (string.IsNullOrWhiteSpace(directory)) + FileSystemMetadata file; + if (!_fileCache.TryGetValue(path, out file)) { - _logger.Debug("Parent path is null for {0}", path); - return null; - } - - var dict = GetFileSystemDictionary(directory, false); + file = _fileSystem.GetFileInfo(path); - FileSystemMetadata entry; - dict.TryGetValue(path, out entry); + if (file != null) + { + _fileCache.TryAdd(path, file); + } + } - return entry; + return file; + //return _fileSystem.GetFileInfo(path); } public IEnumerable GetDirectories(string path) diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 9427b2afd7..87c3b36a27 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -1,5 +1,6 @@ using System.Linq; using CommonIO; +using MediaBrowser.Model.Logging; using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers @@ -19,7 +20,7 @@ namespace MediaBrowser.Controller.Providers public bool ForceSave { get; set; } public MetadataRefreshOptions(IFileSystem fileSystem) - : this(new DirectoryService(fileSystem)) + : this(new DirectoryService(new NullLogger(), fileSystem)) { } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 6659d15530..3871952450 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -172,15 +172,21 @@ namespace MediaBrowser.Controller.Session Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken); /// - /// Sends the message to user sessions. + /// Sends the message to admin sessions. /// /// - /// The user identifier. /// The name. /// The data. /// The cancellation token. /// Task. - Task SendMessageToUserSessions(string userId, string name, T data, CancellationToken cancellationToken); + Task SendMessageToAdminSessions(string name, T data, CancellationToken cancellationToken); + + /// + /// Sends the message to user sessions. + /// + /// + /// Task. + Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken); /// /// Sends the message to user device sessions. diff --git a/MediaBrowser.Controller/Sync/ISyncManager.cs b/MediaBrowser.Controller/Sync/ISyncManager.cs index 8eb934eaa8..fbcb549ad8 100644 --- a/MediaBrowser.Controller/Sync/ISyncManager.cs +++ b/MediaBrowser.Controller/Sync/ISyncManager.cs @@ -151,7 +151,7 @@ namespace MediaBrowser.Controller.Sync /// /// The query. /// QueryResult<System.String>. - QueryResult GetSyncedItemProgresses(SyncJobItemQuery query); + Dictionary GetSyncedItemProgresses(SyncJobItemQuery query); /// /// Reports the synchronize job item transfer beginning. diff --git a/MediaBrowser.Controller/Sync/ISyncRepository.cs b/MediaBrowser.Controller/Sync/ISyncRepository.cs index 2af09dbaad..8e9b2bf774 100644 --- a/MediaBrowser.Controller/Sync/ISyncRepository.cs +++ b/MediaBrowser.Controller/Sync/ISyncRepository.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Querying; +using System.Collections.Generic; +using MediaBrowser.Model.Querying; using MediaBrowser.Model.Sync; using System.Threading.Tasks; @@ -74,6 +75,6 @@ namespace MediaBrowser.Controller.Sync /// /// The query. /// QueryResult<System.String>. - QueryResult GetSyncedItemProgresses(SyncJobItemQuery query); + Dictionary GetSyncedItemProgresses(SyncJobItemQuery query); } } diff --git a/MediaBrowser.Controller/Sync/SyncedItemProgress.cs b/MediaBrowser.Controller/Sync/SyncedItemProgress.cs index edb42eb0f1..0fd929eb18 100644 --- a/MediaBrowser.Controller/Sync/SyncedItemProgress.cs +++ b/MediaBrowser.Controller/Sync/SyncedItemProgress.cs @@ -4,7 +4,7 @@ namespace MediaBrowser.Controller.Sync { public class SyncedItemProgress { - public string ItemId { get; set; } + public double Progress { get; set; } public SyncJobItemStatus Status { get; set; } } } diff --git a/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs b/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs index a8e778751d..06856989f3 100644 --- a/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs +++ b/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs @@ -1,20 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Dlna.ContentDirectory; -using MediaBrowser.Dlna.PlayTo; -using MediaBrowser.Model.Channels; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; - -namespace MediaBrowser.Dlna.Channels +namespace MediaBrowser.Dlna.Channels { //public class DlnaChannel : IChannel, IDisposable //{ diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index 233ec9546f..9b5030325a 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs @@ -602,7 +602,7 @@ namespace MediaBrowser.Dlna.ContentDirectory id = id.Substring(paramsIndex + paramsSrch.Length); var parts = id.Split(';'); - id = parts[24]; + id = parts[23]; } if (id.StartsWith("folder_", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index b4127a91f1..aae157e7a8 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Dlna private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; - private readonly Dictionary _profiles = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary> _profiles = new Dictionary>(StringComparer.Ordinal); public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, @@ -45,50 +45,45 @@ namespace MediaBrowser.Dlna _appHost = appHost; } - public IEnumerable GetProfiles() + public void InitProfiles() { - ExtractProfilesIfNeeded(); + try + { + ExtractSystemProfiles(); + LoadProfiles(); + } + catch (Exception ex) + { + _logger.ErrorException("Error extracting DLNA profiles.", ex); + } + } + private void LoadProfiles() + { var list = GetProfiles(UserProfilesPath, DeviceProfileType.User) .OrderBy(i => i.Name) .ToList(); list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System) .OrderBy(i => i.Name)); - - return list; } - private bool _extracted; - private readonly object _syncLock = new object(); - private void ExtractProfilesIfNeeded() + public IEnumerable GetProfiles() { - if (!_extracted) + lock (_profiles) { - lock (_syncLock) - { - if (!_extracted) - { - try - { - ExtractSystemProfiles(); - } - catch (Exception ex) - { - _logger.ErrorException("Error extracting DLNA profiles.", ex); - } - - _extracted = true; - } - - } + var list = _profiles.Values.ToList(); + return list + .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1) + .ThenBy(i => i.Item1.Info.Name) + .Select(i => i.Item2) + .ToList(); } + } public DeviceProfile GetDefaultProfile() { - ExtractProfilesIfNeeded(); - return new DefaultProfile(); } @@ -211,7 +206,6 @@ namespace MediaBrowser.Dlna throw new ArgumentNullException("headers"); } - //_logger.Debug("GetProfile. Headers: " + _jsonSerializer.SerializeToString(headers)); // Convert to case insensitive headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); @@ -223,16 +217,12 @@ namespace MediaBrowser.Dlna } else { - string userAgent = null; - headers.TryGetValue("User-Agent", out userAgent); - - var msg = "No matching device profile via headers found. The default will be used. "; - if (!string.IsNullOrEmpty(userAgent)) + var msg = new StringBuilder(); + foreach (var header in headers) { - msg += "User-agent: " + userAgent + ". "; + msg.AppendLine(header.Key + ": " + header.Value); } - - _logger.Debug(msg); + _logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, msg); } return profile; @@ -258,8 +248,7 @@ namespace MediaBrowser.Dlna //_logger.Debug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); return isMatch; case HeaderMatchType.Regex: - // Reports of IgnoreCase not working on linux so try it a couple different ways. - return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase) || Regex.IsMatch(value.ToUpper(), header.Value.ToUpper(), RegexOptions.IgnoreCase); + return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase); default: throw new ArgumentException("Unrecognized HeaderMatchType"); } @@ -304,20 +293,20 @@ namespace MediaBrowser.Dlna { lock (_profiles) { - DeviceProfile profile; - if (_profiles.TryGetValue(path, out profile)) + Tuple profileTuple; + if (_profiles.TryGetValue(path, out profileTuple)) { - return profile; + return profileTuple.Item2; } try { - profile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path); + var profile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path); profile.Id = path.ToLower().GetMD5().ToString("N"); profile.ProfileType = type; - _profiles[path] = profile; + _profiles[path] = new Tuple(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); return profile; } @@ -344,12 +333,14 @@ namespace MediaBrowser.Dlna private IEnumerable GetProfileInfosInternal() { - ExtractProfilesIfNeeded(); - - return GetProfileInfos(UserProfilesPath, DeviceProfileType.User) - .Concat(GetProfileInfos(SystemProfilesPath, DeviceProfileType.System)) - .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1) - .ThenBy(i => i.Info.Name); + lock (_profiles) + { + var list = _profiles.Values.ToList(); + return list + .Select(i => i.Item1) + .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1) + .ThenBy(i => i.Info.Name); + } } public IEnumerable GetProfileInfos() @@ -363,17 +354,7 @@ namespace MediaBrowser.Dlna { return _fileSystem.GetFiles(path) .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase)) - .Select(i => new InternalProfileInfo - { - Path = i.FullName, - - Info = new DeviceProfileInfo - { - Id = i.FullName.ToLower().GetMD5().ToString("N"), - Name = _fileSystem.GetFileNameWithoutExtension(i), - Type = type - } - }) + .Select(i => GetInternalProfileInfo(i, type)) .ToList(); } catch (DirectoryNotFoundException) @@ -382,6 +363,21 @@ namespace MediaBrowser.Dlna } } + private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type) + { + return new InternalProfileInfo + { + Path = file.FullName, + + Info = new DeviceProfileInfo + { + Id = file.FullName.ToLower().GetMD5().ToString("N"), + Name = _fileSystem.GetFileNameWithoutExtension(file), + Type = type + } + }; + } + private void ExtractSystemProfiles() { var assembly = GetType().Assembly; @@ -427,6 +423,11 @@ namespace MediaBrowser.Dlna } _fileSystem.DeleteFile(info.Path); + + lock (_profiles) + { + _profiles.Remove(info.Path); + } } public void CreateProfile(DeviceProfile profile) @@ -441,7 +442,7 @@ namespace MediaBrowser.Dlna var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; var path = Path.Combine(UserProfilesPath, newFilename); - SaveProfile(profile, path); + SaveProfile(profile, path, DeviceProfileType.User); } public void UpdateProfile(DeviceProfile profile) @@ -468,14 +469,14 @@ namespace MediaBrowser.Dlna _fileSystem.DeleteFile(current.Path); } - SaveProfile(profile, path); + SaveProfile(profile, path, DeviceProfileType.User); } - private void SaveProfile(DeviceProfile profile, string path) + private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type) { lock (_profiles) { - _profiles[path] = profile; + _profiles[path] = new Tuple(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); } _xmlSerializer.SerializeToFile(profile, path); } @@ -560,7 +561,10 @@ namespace MediaBrowser.Dlna new SonyBravia2012Profile(), new SonyBravia2013Profile(), new SonyBravia2014Profile(), - new SonyBlurayPlayer2013Profile(), + new SonyBlurayPlayer2013(), + new SonyBlurayPlayer2014(), + new SonyBlurayPlayer2015(), + new SonyBlurayPlayer2016(), new SonyBlurayPlayerProfile(), new PanasonicVieraProfile(), new WdtvLiveProfile(), diff --git a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs index b9d9944eca..9f2726b315 100644 --- a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs +++ b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs @@ -14,10 +14,8 @@ using MediaBrowser.Dlna.Ssdp; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Dlna.Channels; namespace MediaBrowser.Dlna.Main { @@ -82,6 +80,8 @@ namespace MediaBrowser.Dlna.Main public void Run() { + ((DlnaManager)_dlnaManager).InitProfiles(); + ReloadComponents(); _config.ConfigurationUpdated += _config_ConfigurationUpdated; @@ -242,9 +242,9 @@ namespace MediaBrowser.Dlna.Main var services = new List { - "upnp:rootdevice", - "urn:schemas-upnp-org:device:MediaServer:1", - "urn:schemas-upnp-org:service:ContentDirectory:1", + "upnp:rootdevice", + "urn:schemas-upnp-org:device:MediaServer:1", + "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", "uuid:" + udn diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index 9df9c0a252..d10a5f7b5a 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -94,6 +94,9 @@ + + + @@ -137,7 +140,7 @@ - + @@ -244,6 +247,11 @@ + + + + + + + + + + + + + + + +]> + + + + + + + + <xsl:value-of select="StringUsages/@ReportTitle"/> + + + + +

+ +

+
+

Strings

+
+ + + + + + + + + + + + + + +
+
: "
+
"
+
:
+
+
+ + +
+
\ No newline at end of file diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml new file mode 100644 index 0000000000..118ed55aef --- /dev/null +++ b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs b/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs new file mode 100644 index 0000000000..5234f7857e --- /dev/null +++ b/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs @@ -0,0 +1,259 @@ +using MediaBrowser.Tests.ConsistencyTests.TextIndexing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; + +namespace MediaBrowser.Tests.ConsistencyTests +{ + /// + /// This class contains tests for reporting the usage of localization string tokens + /// in the dashboard-ui or similar. + /// + /// + /// Run one of the two tests using Visual Studio's "Test Explorer": + /// + /// + /// + /// + /// + /// + /// + /// On successful run, the bottom section of the test explorer will contain a link "Output". + /// This link will open the test results, displaying the trace and two attachment links. + /// One link will open the output folder, the other link will open the output xml file. + /// + /// + /// The output xml file contains a stylesheet link to render the results as html. + /// How that works depends on the default application configured for XML files: + /// + /// + /// Visual Studio + /// Will open in XML source view. To view the html result, click menu + /// 'XML' => 'Start XSLT without debugging' + /// Internet Explorer + /// XSL transform will be applied automatically. + /// Firefox + /// XSL transform will be applied automatically. + /// Chrome + /// Does not work. Chrome is unable/unwilling to apply xslt transforms from local files. + /// + /// + [TestClass] + public class StringUsageReporter + { + /// + /// Root path of the web application + /// + /// + /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). + /// + public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui"; + + /// + /// Path to the strings file, relative to . + /// + public const string StringsFile = @"strings\en-US.json"; + + /// + /// Path to the output folder + /// + /// + /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). + /// Important: When changing the output path, make sure that "StringCheck.xslt" is present + /// to make the XML transform work. + /// + public const string OutputPath = @"."; + + /// + /// List of file extension to search. + /// + public static string[] TargetExtensions = new[] { ".js", ".html" }; + + /// + /// List of paths to exclude from search. + /// + public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" }; + + private TestContext testContextInstance; + + /// + ///Gets or sets the test context which provides + ///information about and functionality for the current test run. + /// + public TestContext TestContext + { + get + { + return testContextInstance; + } + set + { + testContextInstance = value; + } + } + + //[TestMethod] + //public void ReportStringUsage() + //{ + // this.CheckDashboardStrings(false); + //} + + [TestMethod] + public void ReportUnusedStrings() + { + this.CheckDashboardStrings(true); + } + + private void CheckDashboardStrings(Boolean unusedOnly) + { + // Init Folders + var currentDir = System.IO.Directory.GetCurrentDirectory(); + Trace("CurrentDir: {0}", currentDir); + + var rootFolderInfo = ResolveFolder(currentDir, WebFolder); + Trace("Web Root: {0}", rootFolderInfo.FullName); + + var outputFolderInfo = ResolveFolder(currentDir, OutputPath); + Trace("Output Path: {0}", outputFolderInfo.FullName); + + // Load Strings + var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile); + + if (!File.Exists(stringsFileName)) + { + throw new Exception(string.Format("Strings file not found: {0}", stringsFileName)); + } + + int lineNumbers; + var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers); + + Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers); + + var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories); + + var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => string.Equals(e, f.Extension, StringComparison.OrdinalIgnoreCase))); + var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p))); + + var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList(); + + var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName); + + Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count); + + var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now)); + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = Encoding.UTF8, + WriteEndDocumentOnClose = true + }; + + Trace("Output file: {0}", outputFileName); + + using (XmlWriter writer = XmlWriter.Create(outputFileName, settings)) + { + writer.WriteStartDocument(true); + + // Write the Processing Instruction node. + string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\""; + writer.WriteProcessingInstruction("xml-stylesheet", xslText); + + writer.WriteStartElement("StringUsages"); + writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report"); + writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All"); + + foreach (var kvp in stringsDic) + { + var occurences = wordIndex.Find(kvp.Key); + + if (occurences == null || !unusedOnly) + { + ////Trace("{0}: {1}", kvp.Key, kvp.Value); + writer.WriteStartElement("Dictionary"); + writer.WriteAttributeString("Token", kvp.Key); + writer.WriteAttributeString("Text", kvp.Value); + + if (occurences != null && !unusedOnly) + { + foreach (var occurence in occurences) + { + writer.WriteStartElement("Occurence"); + writer.WriteAttributeString("FileName", occurence.FileName); + writer.WriteAttributeString("FullPath", occurence.FullPath); + writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString()); + writer.WriteEndElement(); + ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber); + } + } + + writer.WriteEndElement(); + } + } + } + + TestContext.AddResultFile(outputFileName); + TestContext.AddResultFile(outputFolderInfo.FullName); + } + + private SortedDictionary CreateStringsDictionary(FileInfo file, out int lineNumbers) + { + var dic = new SortedDictionary(); + lineNumbers = 0; + + using (var reader = file.OpenText()) + { + while (!reader.EndOfStream) + { + lineNumbers++; + var words = reader + .ReadLine() + .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries); + + + if (words.Length == 2) + { + var token = words[0].Replace("\"", string.Empty).Trim(); + var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim(); + + if (dic.Keys.Contains(token)) + { + throw new Exception(string.Format("Double string entry found: {0}", token)); + } + + dic.Add(token, text); + } + } + } + + return dic; + } + + private DirectoryInfo ResolveFolder(string currentDir, string folderPath) + { + if (folderPath.IndexOf(@"\:") != 1) + { + folderPath = Path.Combine(currentDir, folderPath); + } + + var folderInfo = new DirectoryInfo(folderPath); + + if (!folderInfo.Exists) + { + throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName)); + } + + return folderInfo; + } + + + private void Trace(string message, params object[] parameters) + { + var formatted = string.Format(message, parameters); + System.Diagnostics.Trace.WriteLine(formatted); + } + } +} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs new file mode 100644 index 0000000000..e88e3ae292 --- /dev/null +++ b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing +{ + public class IndexBuilder + { + public const int MinumumWordLength = 4; + + public static char[] WordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); + + public static WordIndex BuildIndexFromFiles(IEnumerable wordFiles, string rootFolderPath) + { + var index = new WordIndex(); + + var wordSeparators = Enumerable.Range(32, 127).Select(e => Convert.ToChar(e)).Where(c => !WordChars.Contains(c)).ToArray(); + wordSeparators = wordSeparators.Concat(new[] { '\t' }).ToArray(); // add tab + + foreach (var file in wordFiles) + { + var lineNumber = 1; + var displayFileName = file.FullName.Replace(rootFolderPath, string.Empty); + using (var reader = file.OpenText()) + { + while (!reader.EndOfStream) + { + var words = reader + .ReadLine() + .Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries); + ////.Select(f => f.Trim()); + + var wordIndex = 1; + foreach (var word in words) + { + if (word.Length >= MinumumWordLength) + { + index.AddWordOccurrence(word, displayFileName, file.FullName, lineNumber, wordIndex++); + } + } + + lineNumber++; + } + } + } + + return index; + } + + } +} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs new file mode 100644 index 0000000000..60b88dd5e0 --- /dev/null +++ b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing +{ + public class WordIndex : Dictionary + { + public WordIndex() : base(StringComparer.InvariantCultureIgnoreCase) + { + } + + public void AddWordOccurrence(string word, string fileName, string fullPath, int lineNumber, int wordIndex) + { + WordOccurrences current; + if (!this.TryGetValue(word, out current)) + { + current = new WordOccurrences(); + this[word] = current; + } + + current.AddOccurrence(fileName, fullPath, lineNumber, wordIndex); + } + + public WordOccurrences Find(string word) + { + WordOccurrences found; + if (this.TryGetValue(word, out found)) + { + return found; + } + + return null; + } + + } +} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs new file mode 100644 index 0000000000..7564d90f3c --- /dev/null +++ b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing +{ + public struct WordOccurrence + { + public readonly string FileName; // file containing the word. + public readonly string FullPath; // file containing the word. + public readonly int LineNumber; // line within the file. + public readonly int WordIndex; // index within the line. + + public WordOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) + { + FileName = fileName; + FullPath = fullPath; + LineNumber = lineNumber; + WordIndex = wordIndex; + } + } +} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs new file mode 100644 index 0000000000..821a74d21b --- /dev/null +++ b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing +{ + public class WordOccurrences : List + { + public void AddOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) + { + this.Add(new WordOccurrence(fileName, fullPath, lineNumber, wordIndex)); + } + + } +} diff --git a/MediaBrowser.Tests/MediaBrowser.Tests.csproj b/MediaBrowser.Tests/MediaBrowser.Tests.csproj index 0cfe8182c8..76a1861097 100644 --- a/MediaBrowser.Tests/MediaBrowser.Tests.csproj +++ b/MediaBrowser.Tests/MediaBrowser.Tests.csproj @@ -25,6 +25,7 @@ DEBUG;TRACE prompt 4 + bin\Debug\MediaBrowser.Tests.XML none @@ -36,6 +37,7 @@ + @@ -50,6 +52,11 @@ + + + + + @@ -98,6 +105,14 @@ PreserveNewest + + + Always + StringCheck.xslt + + + + diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 34ad32111b..aec4632ae8 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -17,7 +17,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using CommonIO; -using WebMarkupMin.Core.Minifiers; +using WebMarkupMin.Core; namespace MediaBrowser.WebDashboard.Api { @@ -58,6 +58,11 @@ namespace MediaBrowser.WebDashboard.Api { } + [Route("/web/staticfiles", "GET")] + public class GetCacheFiles + { + } + /// /// Class GetDashboardResource /// @@ -140,6 +145,37 @@ namespace MediaBrowser.WebDashboard.Api return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator().ModifyHtml("dummy.html", page.GetHtmlStream(), null, _appHost.ApplicationVersion.ToString(), null, false)); } + public object Get(GetCacheFiles request) + { + var allFiles = GetCacheFileList(); + + return ResultFactory.GetOptimizedResult(Request, _jsonSerializer.SerializeToString(allFiles)); + } + + private List GetCacheFileList() + { + var creator = GetPackageCreator(); + var directory = creator.DashboardUIPath; + + var skipExtensions = GetDeployIgnoreExtensions(); + var skipNames = GetDeployIgnoreFilenames(); + + return + Directory.GetFiles(directory, "*", SearchOption.AllDirectories) + .Where(i => !skipExtensions.Contains(Path.GetExtension(i) ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + .Where(i => !skipNames.Any(s => + { + if (s.Item2) + { + return string.Equals(s.Item1, Path.GetFileName(i), StringComparison.OrdinalIgnoreCase); + } + + return (Path.GetFileName(i) ?? string.Empty).IndexOf(s.Item1, StringComparison.OrdinalIgnoreCase) != -1; + })) + .Select(i => i.Replace(directory, string.Empty, StringComparison.OrdinalIgnoreCase).Replace("\\", "/").TrimStart('/') + "?v=" + _appHost.ApplicationVersion.ToString()) + .ToList(); + } + /// /// Gets the specified request. /// @@ -274,6 +310,43 @@ namespace MediaBrowser.WebDashboard.Api return new PackageCreator(_fileSystem, _localization, Logger, _serverConfigurationManager, _jsonSerializer); } + private List GetDeployIgnoreExtensions() + { + var list = new List(); + + list.Add(".log"); + list.Add(".txt"); + list.Add(".map"); + list.Add(".md"); + list.Add(".gz"); + list.Add(".bat"); + list.Add(".sh"); + + return list; + } + + private List> GetDeployIgnoreFilenames() + { + var list = new List>(); + + list.Add(new Tuple("copying", true)); + list.Add(new Tuple("license", true)); + list.Add(new Tuple("license-mit", true)); + list.Add(new Tuple("gitignore", false)); + list.Add(new Tuple("npmignore", false)); + list.Add(new Tuple("jshintrc", false)); + list.Add(new Tuple("gruntfile", false)); + list.Add(new Tuple("bowerrc", false)); + list.Add(new Tuple("jscsrc", false)); + list.Add(new Tuple("hero.svg", false)); + list.Add(new Tuple("travis.yml", false)); + list.Add(new Tuple("build.js", false)); + list.Add(new Tuple("editorconfig", false)); + list.Add(new Tuple("gitattributes", false)); + + return list; + } + public async Task Get(GetDashboardPackage request) { var path = Path.Combine(_serverConfigurationManager.ApplicationPaths.ProgramDataPath, @@ -296,45 +369,19 @@ namespace MediaBrowser.WebDashboard.Api var appVersion = _appHost.ApplicationVersion.ToString(); - var mode = request.Mode; + File.WriteAllText(Path.Combine(path, "staticfiles"), _jsonSerializer.SerializeToString(GetCacheFileList())); - if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) - { - _fileSystem.DeleteFile(Path.Combine(path, "scripts", "registrationservices.js")); - } + var mode = request.Mode; // Try to trim the output size a bit var bowerPath = Path.Combine(path, "bower_components"); - if (!string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) - { - //var versionedBowerPath = Path.Combine(Path.GetDirectoryName(bowerPath), "bower_components" + _appHost.ApplicationVersion); - //Directory.Move(bowerPath, versionedBowerPath); - //bowerPath = versionedBowerPath; - } + GetDeployIgnoreExtensions().ForEach(i => DeleteFilesByExtension(bowerPath, i)); - DeleteFilesByExtension(bowerPath, ".log"); - DeleteFilesByExtension(bowerPath, ".txt"); - DeleteFilesByExtension(bowerPath, ".map"); - DeleteFilesByExtension(bowerPath, ".md"); DeleteFilesByExtension(bowerPath, ".json", "strings\\"); - DeleteFilesByExtension(bowerPath, ".gz"); - DeleteFilesByExtension(bowerPath, ".bat"); - DeleteFilesByExtension(bowerPath, ".sh"); - DeleteFilesByName(bowerPath, "copying", true); - DeleteFilesByName(bowerPath, "license", true); - DeleteFilesByName(bowerPath, "license-mit", true); - DeleteFilesByName(bowerPath, "gitignore"); - DeleteFilesByName(bowerPath, "npmignore"); - DeleteFilesByName(bowerPath, "jshintrc"); - DeleteFilesByName(bowerPath, "gruntfile"); - DeleteFilesByName(bowerPath, "bowerrc"); - DeleteFilesByName(bowerPath, "jscsrc"); - DeleteFilesByName(bowerPath, "hero.svg"); - DeleteFilesByName(bowerPath, "travis.yml"); - DeleteFilesByName(bowerPath, "build.js"); - DeleteFilesByName(bowerPath, "editorconfig"); - DeleteFilesByName(bowerPath, "gitattributes"); + + GetDeployIgnoreFilenames().ForEach(i => DeleteFilesByName(bowerPath, i.Item1, i.Item2)); + DeleteFoldersByName(bowerPath, "demo"); DeleteFoldersByName(bowerPath, "test"); DeleteFoldersByName(bowerPath, "guides"); @@ -349,8 +396,6 @@ namespace MediaBrowser.WebDashboard.Api } _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "jquery", "src"), true); - //_fileSystem.DeleteDirectory(Path.Combine(bowerPath, "fingerprintjs2", "flash"), true); - //_fileSystem.DeleteDirectory(Path.Combine(bowerPath, "fingerprintjs2", "specs"), true); DeleteCryptoFiles(Path.Combine(bowerPath, "cryptojslib", "components")); @@ -359,13 +404,11 @@ namespace MediaBrowser.WebDashboard.Api //DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "meteor"); //DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "st"); //DeleteFoldersByName(Path.Combine(bowerPath, "Swiper"), "src"); - + if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) { // Delete things that are unneeded in an attempt to keep the output as trim as possible _fileSystem.DeleteDirectory(Path.Combine(path, "css", "images", "tour"), true); - - _fileSystem.DeleteFile(Path.Combine(path, "thirdparty", "jquerymobile-1.4.5", "jquery.mobile-1.4.5.min.map")); } else { diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index b7b1f1dfd7..65e7fa5d39 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -11,8 +11,6 @@ using System.Threading.Tasks; using CommonIO; using MediaBrowser.Controller.Net; using WebMarkupMin.Core; -using WebMarkupMin.Core.Minifiers; -using WebMarkupMin.Core.Settings; namespace MediaBrowser.WebDashboard.Api { @@ -372,12 +370,12 @@ namespace MediaBrowser.WebDashboard.Api sb.Append(""); // http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html - sb.Append(""); - sb.Append(""); - sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); @@ -431,7 +429,7 @@ namespace MediaBrowser.WebDashboard.Api var files = new List(); - files.Add("bower_components/requirejs/require.js"); + files.Add("bower_components/requirejs/require.js" + versionString); files.Add("scripts/site.js" + versionString); diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index e4150d85cf..5b66c27f48 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -63,9 +63,9 @@ ..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll - - False - ..\packages\WebMarkupMin.Core.1.0.1\lib\net40\WebMarkupMin.Core.dll + + ..\packages\WebMarkupMin.Core.2.1.0\lib\net40-client\WebMarkupMin.Core.dll + True @@ -101,6 +101,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -116,6 +125,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -143,6 +158,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -155,12 +176,21 @@ PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -170,10 +200,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest @@ -197,12 +227,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -242,16 +266,7 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest @@ -317,7 +332,7 @@ PreserveNewest - + PreserveNewest @@ -329,7 +344,7 @@ PreserveNewest - + PreserveNewest @@ -347,7 +362,7 @@ PreserveNewest - + PreserveNewest @@ -368,7 +383,7 @@ PreserveNewest - + PreserveNewest @@ -389,6 +404,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -491,7 +509,7 @@ PreserveNewest - + PreserveNewest @@ -677,9 +695,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -788,9 +803,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -818,9 +830,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -860,13 +869,13 @@ PreserveNewest - + PreserveNewest PreserveNewest - + PreserveNewest @@ -878,7 +887,7 @@ PreserveNewest - + PreserveNewest @@ -893,10 +902,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest @@ -992,12 +1001,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -1089,7 +1092,7 @@ PreserveNewest - + PreserveNewest @@ -1217,13 +1220,13 @@ - + PreserveNewest - + PreserveNewest - + PreserveNewest @@ -1350,18 +1353,13 @@ PreserveNewest - - - PreserveNewest - - PreserveNewest - + PreserveNewest @@ -1408,16 +1406,6 @@ PreserveNewest - - - PreserveNewest - - - - - PreserveNewest - - PreserveNewest @@ -1433,24 +1421,6 @@ PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - PreserveNewest - - - - - PreserveNewest - - PreserveNewest @@ -1462,7 +1432,7 @@ - + PreserveNewest @@ -1474,7 +1444,7 @@ PreserveNewest - + PreserveNewest @@ -1495,9 +1465,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1611,6 +1578,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index a2d13fdf5f..3637c6c84e 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 2e34135a62..f777ae16bc 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -231,7 +231,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers DateTime added; if (DateTime.TryParseExact(val, BaseNfoSaver.DateAddedFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out added)) { - item.EndDate = added.ToUniversalTime(); + item.DateCreated = added.ToUniversalTime(); } else if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out added)) { @@ -827,6 +827,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers } break; } + case "tvmazeid": + { + var id = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(id)) + { + item.SetProviderId(MetadataProviders.TvMaze, id); + } + break; + } case "audiodbartistid": { var id = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 6e3114fa15..dfa5c1b71a 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -199,10 +199,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers // int.TryParse is local aware, so it can be probamatic, force us culture if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) { - if ((item.ParentIndexNumber ?? 0) == 0) - { - item.AirsBeforeSeasonNumber = rval; - } + item.AirsBeforeSeasonNumber = rval; } } @@ -220,10 +217,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers // int.TryParse is local aware, so it can be probamatic, force us culture if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) { - if ((item.ParentIndexNumber ?? 0) == 0) - { - item.AirsBeforeEpisodeNumber = rval; - } + item.AirsBeforeEpisodeNumber = rval; } } diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index c1dd929877..f3c40a1696 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -1,7 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.XbmcMetadata.Savers; -using System; using System.IO; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 954fb2a474..c28a15a939 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -822,6 +822,12 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("tvrageid", externalId); } + externalId = item.GetProviderId(MetadataProviders.TvMaze); + if (!string.IsNullOrEmpty(externalId)) + { + writer.WriteElementString("tvmazeid", externalId); + } + if (options.SaveImagePathsInNfo) { AddImages(item, writer, libraryManager, config); diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index 7523ce6bf1..bf6df2347f 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -64,25 +64,25 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("aired", episode.PremiereDate.Value.ToLocalTime().ToString(formatString)); } - if (episode.AirsAfterSeasonNumber.HasValue) + if (episode.AirsAfterSeasonNumber.HasValue && episode.AirsAfterSeasonNumber.Value != -1) { writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(UsCulture)); } - if (episode.AirsBeforeEpisodeNumber.HasValue) + if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1) { writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(UsCulture)); } - if (episode.AirsBeforeEpisodeNumber.HasValue) + if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1) { writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(UsCulture)); } - if (episode.AirsBeforeSeasonNumber.HasValue) + if (episode.AirsBeforeSeasonNumber.HasValue && episode.AirsBeforeSeasonNumber.Value != -1) { writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(UsCulture)); } var season = episode.AiredSeasonNumber; - if (season.HasValue) + if (season.HasValue && season.Value != -1) { writer.WriteElementString("displayseason", season.Value.ToString(UsCulture)); } @@ -118,7 +118,9 @@ namespace MediaBrowser.XbmcMetadata.Savers "airsbefore_season", "DVD_episodenumber", "DVD_season", - "absolute_number" + "absolute_number", + "displayseason", + "displayepisode" }; return list; diff --git a/MediaBrowser.sln b/MediaBrowser.sln index a55ae200a8..c6068f5364 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Performance3.psess = Performance3.psess Performance4.psess = Performance4.psess Performance5.psess = Performance5.psess + Performance6.psess = Performance6.psess + Performance7.psess = Performance7.psess EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget (2)", ".nuget (2)", "{E60FB157-87E2-4A41-8B04-27EA49B63B4D}" @@ -63,6 +65,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" EndProject Global + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 21173feaad..d1c4c0b449 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common.Internal - 3.0.653 + 3.0.655 MediaBrowser.Common.Internal Luke ebr,Luke,scottisafool @@ -12,8 +12,8 @@ Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption. Copyright © Emby 2013 - - + + diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 2946858472..ffee84de2e 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.653 + 3.0.655 MediaBrowser.Common Emby Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index 18e8a5c13b..73c51ca355 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.653 + 3.0.655 Media Browser.Server.Core Emby Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Emby Server. Copyright © Emby 2013 - + diff --git a/SharedVersion.cs b/SharedVersion.cs index d996b5e97d..ded2038b03 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; [assembly: AssemblyVersion("3.1.*")] -//[assembly: AssemblyVersion("3.0.5986")] +//[assembly: AssemblyVersion("3.0.6060")]