diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index dc93cb7304..3288229a2f 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -163,7 +163,7 @@ namespace Emby.Drawing return _imageEncoder.SupportedOutputFormats; } - public async Task> ProcessImage(ImageProcessingOptions options) + public async Task> ProcessImage(ImageProcessingOptions options) { if (options == null) { @@ -178,14 +178,13 @@ namespace Emby.Drawing } var originalImagePath = originalImage.Path; + var dateModified = originalImage.DateModified; if (!_imageEncoder.SupportsImageEncoding) { - return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath)); + return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - var dateModified = originalImage.DateModified; - if (options.CropWhiteSpace && _imageEncoder.SupportsImageEncoding) { var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false); @@ -211,7 +210,7 @@ namespace Emby.Drawing if (options.HasDefaultOptions(originalImagePath)) { // Just spit out the original file if all the options are default - return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath)); + return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } ImageSize? originalImageSize; @@ -221,7 +220,7 @@ namespace Emby.Drawing if (options.HasDefaultOptions(originalImagePath, originalImageSize.Value)) { // Just spit out the original file if all the options are default - return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath)); + return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } } catch @@ -235,10 +234,6 @@ namespace Emby.Drawing var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]); var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.BackgroundColor, options.ForegroundLayer); - var semaphore = GetLock(cacheFilePath); - - await semaphore.WaitAsync().ConfigureAwait(false); - var imageProcessingLockTaken = false; try @@ -251,15 +246,20 @@ namespace Emby.Drawing var newHeight = Convert.ToInt32(newSize.Height); _fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); + var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")); + _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath)); await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); imageProcessingLockTaken = true; - _imageEncoder.EncodeImage(originalImagePath, cacheFilePath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat); + _imageEncoder.EncodeImage(originalImagePath, tmpPath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat); + CopyFile(tmpPath, cacheFilePath); + + return new Tuple(tmpPath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(tmpPath)); } - return new Tuple(cacheFilePath, GetMimeType(outputFormat, cacheFilePath)); + return new Tuple(cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); } catch (Exception ex) { @@ -267,7 +267,7 @@ namespace Emby.Drawing _logger.ErrorException("Error encoding image", ex); // Just spit out the original file if all the options are default - return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath)); + return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } finally { @@ -275,8 +275,18 @@ namespace Emby.Drawing { _imageProcessingSemaphore.Release(); } + } + } + + private void CopyFile(string src, string destination) + { + try + { + File.Copy(src, destination, true); + } + catch + { - semaphore.Release(); } } @@ -412,14 +422,9 @@ namespace Emby.Drawing var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath)); - var semaphore = GetLock(croppedImagePath); - - await semaphore.WaitAsync().ConfigureAwait(false); - // Check again in case of contention if (_fileSystem.FileExists(croppedImagePath)) { - semaphore.Release(); return GetResult(croppedImagePath); } @@ -428,11 +433,15 @@ namespace Emby.Drawing try { _fileSystem.CreateDirectory(Path.GetDirectoryName(croppedImagePath)); + var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")); + _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath)); await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); imageProcessingLockTaken = true; - _imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath); + _imageEncoder.CropWhiteSpace(originalImagePath, tmpPath); + CopyFile(tmpPath, croppedImagePath); + return GetResult(tmpPath); } catch (NotImplementedException) { @@ -452,11 +461,7 @@ namespace Emby.Drawing { _imageProcessingSemaphore.Release(); } - - semaphore.Release(); } - - return GetResult(croppedImagePath); } private Tuple GetResult(string path) diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index a511f8c728..5866ad15bc 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -273,7 +273,9 @@ namespace MediaBrowser.Api.Images { var list = new List(); - foreach (var image in item.ImageInfos.Where(i => !item.AllowsMultipleImages(i.Type))) + var itemImages = item.ImageInfos; + + foreach (var image in itemImages.Where(i => !item.AllowsMultipleImages(i.Type))) { var info = GetImageInfo(item, image, null); @@ -283,14 +285,14 @@ namespace MediaBrowser.Api.Images } } - foreach (var imageType in item.ImageInfos.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) + foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) { var index = 0; // Prevent implicitly captured closure var currentImageType = imageType; - foreach (var image in item.ImageInfos.Where(i => i.Type == currentImageType)) + foreach (var image in itemImages.Where(i => i.Type == currentImageType)) { var info = GetImageInfo(item, image, index); @@ -636,6 +638,7 @@ namespace MediaBrowser.Api.Images CacheDuration = cacheDuration, ResponseHeaders = headers, ContentType = imageResult.Item2, + DateLastModified = imageResult.Item3, IsHeadRequest = isHeadRequest, Path = imageResult.Item1, diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 0b4e5efe87..f60a106dac 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -286,19 +286,25 @@ namespace MediaBrowser.Api.Playback protected string GetH264Encoder(StreamState state) { - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) || - string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + // 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) { - return "h264_qsv"; - } + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + return "h264_qsv"; + } - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) - { - return "h264_nvenc"; - } - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase)) - { - return "h264_omx"; + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + return "h264_nvenc"; + } + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase)) + { + return "h264_omx"; + } } return "libx264"; @@ -843,6 +849,14 @@ namespace MediaBrowser.Api.Playback return null; } + // 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) + { + return null; + } + if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec)) { if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 868d8d4889..d8b7ce2ef3 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -13,6 +13,7 @@ using ServiceStack.Web; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Threading; using System.Threading.Tasks; using CommonIO; @@ -336,17 +337,19 @@ namespace MediaBrowser.Api.Playback.Progressive state.Dispose(); } - var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - result.Options["Content-Type"] = contentType; + outputHeaders["Content-Type"] = contentType; // Add the response headers to the result object foreach (var item in responseHeaders) { - result.Options[item.Key] = item.Value; + outputHeaders[item.Key] = item.Value; } - return result; + Func streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream); + + return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders); } finally { diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 9f02c51cd6..bf543579f7 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -48,21 +48,19 @@ namespace MediaBrowser.Api.Playback.Progressive /// The response stream. public void WriteTo(Stream responseStream) { - WriteToInternal(responseStream); + var task = WriteToAsync(responseStream); + Task.WaitAll(task); } /// - /// Writes to async. + /// Writes to. /// /// The response stream. - /// Task. - private void WriteToInternal(Stream responseStream) + public async Task WriteToAsync(Stream responseStream) { try { - var task = new ProgressiveFileCopier(_fileSystem, _job, Logger).StreamFile(Path, responseStream); - - Task.WaitAll(task); + await new ProgressiveFileCopier(_fileSystem, _job, Logger).StreamFile(Path, responseStream).ConfigureAwait(false); } catch (IOException) { @@ -110,11 +108,11 @@ namespace MediaBrowser.Api.Playback.Progressive var eofCount = 0; long position = 0; - using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, false)) + using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) { while (eofCount < 15) { - CopyToInternal(fs, outputStream, BufferSize); + await CopyToInternal(fs, outputStream, BufferSize).ConfigureAwait(false); var fsPosition = fs.Position; @@ -140,11 +138,11 @@ namespace MediaBrowser.Api.Playback.Progressive } } - private void CopyToInternal(Stream source, Stream destination, int bufferSize) + private async Task CopyToInternal(Stream source, Stream destination, int bufferSize) { var array = new byte[bufferSize]; int count; - while ((count = source.Read(array, 0, array.Length)) != 0) + while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0) { //if (_job != null) //{ @@ -170,7 +168,7 @@ namespace MediaBrowser.Api.Playback.Progressive // } //} - destination.Write(array, 0, count); + await destination.WriteAsync(array, 0, count).ConfigureAwait(false); _bytesWritten += count; diff --git a/MediaBrowser.Api/Reports/ReportsService.cs b/MediaBrowser.Api/Reports/ReportsService.cs index 36a2a4b619..d0b6d6e787 100644 --- a/MediaBrowser.Api/Reports/ReportsService.cs +++ b/MediaBrowser.Api/Reports/ReportsService.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Api.Reports /// Manager for library. private readonly ILibraryManager _libraryManager; ///< Manager for library - /// The localization. + /// The localization. private readonly ILocalizationManager _localization; ///< The localization @@ -58,10 +58,10 @@ namespace MediaBrowser.Api.Reports /// Gets the given request. /// The request. /// A Task<object> - public async Task Get(GetActivityLogs request) + public object Get(GetActivityLogs request) { request.DisplayType = "Screen"; - ReportResult result = await GetReportActivities(request).ConfigureAwait(false); + ReportResult result = GetReportActivities(request); return ToOptimizedResult(result); } @@ -104,7 +104,8 @@ namespace MediaBrowser.Api.Reports return null; request.DisplayType = "Screen"; - var reportResult = await GetReportResult(request); + var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; + var reportResult = await GetReportResult(request, user); return ToOptimizedResult(reportResult); } @@ -117,7 +118,8 @@ namespace MediaBrowser.Api.Reports if (string.IsNullOrEmpty(request.IncludeItemTypes)) return null; request.DisplayType = "Screen"; - var reportResult = await GetReportStatistic(request); + var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; + var reportResult = await GetReportStatistic(request, user); return ToOptimizedResult(reportResult); } @@ -150,6 +152,7 @@ namespace MediaBrowser.Api.Reports headers["Content-Disposition"] = string.Format("attachment; filename=\"{0}\"", filename); headers["Content-Encoding"] = "UTF-8"; + var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; ReportResult result = null; switch (reportViewType) { @@ -157,12 +160,12 @@ namespace MediaBrowser.Api.Reports case ReportViewType.ReportData: ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); ReportBuilder dataBuilder = new ReportBuilder(_libraryManager); - QueryResult queryResult = await GetQueryResult(request).ConfigureAwait(false); + QueryResult queryResult = await GetQueryResult(request, user).ConfigureAwait(false); result = dataBuilder.GetResult(queryResult.Items, request); result.TotalRecordCount = queryResult.TotalRecordCount; break; case ReportViewType.ReportActivities: - result = await GetReportActivities(request).ConfigureAwait(false); + result = GetReportActivities(request); break; } @@ -177,23 +180,15 @@ namespace MediaBrowser.Api.Reports break; } - object ro = ResultFactory.GetResult(returnResult, contentType, headers); - return ro; + return ResultFactory.GetResult(returnResult, contentType, headers); } #endregion - #region [Private Methods] - - /// Gets items query. - /// The request. - /// The user. - /// The items query. private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user) { - var query = new InternalItemsQuery + var query = new InternalItemsQuery(user) { - User = user, IsPlayed = request.IsPlayed, MediaTypes = request.GetMediaTypes(), IncludeItemTypes = request.GetIncludeItemTypes(), @@ -231,6 +226,7 @@ namespace MediaBrowser.Api.Reports Tags = request.GetTags(), OfficialRatings = request.GetOfficialRatings(), Genres = request.GetGenres(), + GenreIds = request.GetGenreIds(), Studios = request.GetStudios(), StudioIds = request.GetStudioIds(), Person = request.Person, @@ -245,9 +241,11 @@ namespace MediaBrowser.Api.Reports MaxPlayers = request.MaxPlayers, MinCommunityRating = request.MinCommunityRating, MinCriticRating = request.MinCriticRating, + ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId), ParentIndexNumber = request.ParentIndexNumber, AiredDuringSeason = request.AiredDuringSeason, - AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater + AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater, + EnableTotalRecordCount = request.EnableTotalRecordCount }; if (!string.IsNullOrWhiteSpace(request.Ids)) @@ -357,98 +355,111 @@ namespace MediaBrowser.Api.Reports query.AlbumNames = request.Albums.Split('|'); } - if (request.HasQueryLimit == false) - { - query.StartIndex = null; - query.Limit = null; - } - return query; } - /// Gets query result. - /// The request. - /// The query result. - private async Task> GetQueryResult(BaseReportRequest request) + private async Task> GetQueryResult(BaseReportRequest request, User user) { - // Placeholder in case needed later + // all report queries currently need this because it's not being specified request.Recursive = true; - var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; - request.Fields = "MediaSources,DateCreated,Settings,Studios,SyncInfo,ItemCounts"; - - var parentItem = string.IsNullOrEmpty(request.ParentId) ? - (user == null ? _libraryManager.RootFolder : user.RootFolder) : - _libraryManager.GetItemById(request.ParentId); var item = string.IsNullOrEmpty(request.ParentId) ? user == null ? _libraryManager.RootFolder : user.RootFolder : - parentItem; + _libraryManager.GetItemById(request.ParentId); - IEnumerable items; + if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)) + { + //item = user == null ? _libraryManager.RootFolder : user.RootFolder; + } + else if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) + { + item = user == null ? _libraryManager.RootFolder : user.RootFolder; + } - if (request.Recursive) + // Default list type = children + + var folder = item as Folder; + if (folder == null) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); - return result; + folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); } - else + + if (!string.IsNullOrEmpty(request.Ids)) { - if (user == null) + request.Recursive = true; + var query = GetItemsQuery(request, user); + var result = await folder.GetItems(query).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(request.SortBy)) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); - return result; + var ids = query.ItemIds.ToList(); + + // Try to preserve order + result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray(); } - var userRoot = item as UserRootFolder; + return result; + } - if (userRoot == null) - { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); + if (request.Recursive) + { + return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); + } - return result; - } + if (user == null) + { + return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); + } + + var userRoot = item as UserRootFolder; - items = ((Folder)item).GetChildren(user, true); + if (userRoot == null) + { + return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); } - return new QueryResult { Items = items.ToArray() }; + IEnumerable items = folder.GetChildren(user, true); + var itemsArray = items.ToArray(); + + return new QueryResult + { + Items = itemsArray, + TotalRecordCount = itemsArray.Length + }; } + #region [Private Methods] + /// Gets report activities. /// The request. /// The report activities. - private Task GetReportActivities(IReportsDownload request) + private ReportResult GetReportActivities(IReportsDownload request) { - return Task.Run(() => - { - DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? - (DateTime?)null : - DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - - QueryResult queryResult; - if (request.HasQueryLimit) - queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); - else - queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, null); - //var queryResult = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); - - ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager); - var result = builder.GetResult(queryResult, request); - result.TotalRecordCount = queryResult.TotalRecordCount; - return result; + DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? + (DateTime?)null : + DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - }); + QueryResult queryResult; + if (request.HasQueryLimit) + queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); + else + queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, null); + //var queryResult = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); + ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager); + var result = builder.GetResult(queryResult, request); + result.TotalRecordCount = queryResult.TotalRecordCount; + return result; } /// Gets report result. /// The request. /// The report result. - private async Task GetReportResult(GetItemReport request) + private async Task GetReportResult(GetItemReport request, User user) { ReportBuilder reportBuilder = new ReportBuilder(_libraryManager); - QueryResult queryResult = await GetQueryResult(request).ConfigureAwait(false); + QueryResult queryResult = await GetQueryResult(request, user).ConfigureAwait(false); ReportResult reportResult = reportBuilder.GetResult(queryResult.Items, request); reportResult.TotalRecordCount = queryResult.TotalRecordCount; @@ -458,10 +469,10 @@ namespace MediaBrowser.Api.Reports /// Gets report statistic. /// The request. /// The report statistic. - private async Task GetReportStatistic(GetReportStatistics request) + private async Task GetReportStatistic(GetReportStatistics request, User user) { ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); - QueryResult queryResult = await GetQueryResult(request).ConfigureAwait(false); + QueryResult queryResult = await GetQueryResult(request, user).ConfigureAwait(false); ReportStatBuilder reportBuilder = new ReportStatBuilder(_libraryManager); ReportStatResult reportResult = reportBuilder.GetResult(queryResult.Items, ReportHelper.GetRowType(request.IncludeItemTypes), request.TopItems ?? 5); diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index dfb82438e9..1bebd42eb1 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -114,11 +114,10 @@ namespace MediaBrowser.Api private void SetWizardFinishValues(ServerConfiguration config) { config.EnableLocalizedGuids = true; - config.EnableCustomPathSubFolders = true; config.EnableStandaloneMusicKeys = true; config.EnableCaseSensitiveItemIds = true; //config.EnableFolderView = true; - config.SchemaVersion = 97; + config.SchemaVersion = 108; } public void Post(UpdateStartupConfiguration request) diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index d27a560ba5..5a66f872f2 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -103,6 +103,8 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "IsInBoxSet", Description = "Optional filter by items that are in boxsets, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? IsInBoxSet { get; set; } + public string ExcludeItemIds { get; set; } + public bool EnableTotalRecordCount { get; set; } /// @@ -367,6 +369,11 @@ namespace MediaBrowser.Api.UserLibrary return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } + public string[] GetExcludeItemIds() + { + return (ExcludeItemIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + public string[] GetExcludeItemTypes() { return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index a6f1d8b98f..74b33fbb7b 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -101,7 +101,7 @@ namespace MediaBrowser.Api.UserLibrary { var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; - var result = await GetItemsToSerialize(request, user).ConfigureAwait(false); + var result = await GetQueryResult(request, user).ConfigureAwait(false); if (result == null) { @@ -135,7 +135,7 @@ namespace MediaBrowser.Api.UserLibrary /// The request. /// The user. /// IEnumerable{BaseItem}. - private async Task> GetItemsToSerialize(GetItems request, User user) + private async Task> GetQueryResult(GetItems request, User user) { var item = string.IsNullOrEmpty(request.ParentId) ? user == null ? _libraryManager.RootFolder : user.RootFolder : @@ -263,7 +263,8 @@ namespace MediaBrowser.Api.UserLibrary ParentIndexNumber = request.ParentIndexNumber, AiredDuringSeason = request.AiredDuringSeason, AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater, - EnableTotalRecordCount = request.EnableTotalRecordCount + EnableTotalRecordCount = request.EnableTotalRecordCount, + ExcludeItemIds = request.GetExcludeItemIds() }; if (!string.IsNullOrWhiteSpace(request.Ids)) diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs index 2b7cc10aab..371757f6ce 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -140,7 +140,17 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager private WebRequest GetRequest(HttpRequestOptions options, string method) { - var request = CreateWebRequest(options.Url); + var url = options.Url; + + var uriAddress = new Uri(url); + var userInfo = uriAddress.UserInfo; + if (!string.IsNullOrWhiteSpace(userInfo)) + { + _logger.Info("Found userInfo in url: {0} ... url: {1}", userInfo, url); + url = url.Replace(userInfo + "@", string.Empty); + } + + var request = CreateWebRequest(url); var httpWebRequest = request as HttpWebRequest; if (httpWebRequest != null) @@ -183,9 +193,27 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } } + if (!string.IsNullOrWhiteSpace(userInfo)) + { + var parts = userInfo.Split(':'); + if (parts.Length == 2) + { + request.Credentials = GetCredential(url, parts[0], parts[1]); + request.PreAuthenticate = true; + } + } + return request; } + private CredentialCache GetCredential(string url, string username, string password) + { + //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3; + CredentialCache credentialCache = new CredentialCache(); + credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password)); + return credentialCache; + } + private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options) { foreach (var header in options.RequestHeaders.ToList()) diff --git a/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs b/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs index 2ffaedc4be..d1ec30210e 100644 --- a/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs +++ b/MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs @@ -54,7 +54,9 @@ namespace MediaBrowser.Common.Implementations.Updates { if (updateLevel == PackageVersionClass.Release) { - obj = obj.Where(i => !i.prerelease).ToArray(); + // 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. + obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); } else if (updateLevel == PackageVersionClass.Beta) { @@ -70,7 +72,7 @@ namespace MediaBrowser.Common.Implementations.Updates .Where(i => i != null) .OrderByDescending(i => Version.Parse(i.AvailableVersion)) .FirstOrDefault(); - + return availableUpdate ?? new CheckForUpdateResult { IsUpdateAvailable = false diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index d42a04f2ee..19f391b4a6 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -84,7 +84,7 @@ namespace MediaBrowser.Controller.Drawing /// /// The options. /// Task. - Task> ProcessImage(ImageProcessingOptions options); + Task> ProcessImage(ImageProcessingOptions options); /// /// Gets the enhanced image. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9d1a45689f..0860cb61cb 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1878,7 +1878,7 @@ namespace MediaBrowser.Controller.Entities return new ItemImageInfo { Path = path, - DateModified = FileSystem.GetLastWriteTimeUtc(path), + DateModified = chapter.ImageDateModified, Type = imageType }; } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 96fad670e1..2ca56bc706 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Providers; +using System; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using System.Linq; using System.Runtime.Serialization; @@ -17,7 +18,26 @@ namespace MediaBrowser.Controller.Entities } } + [IgnoreDataMember] public string SeriesName { get; set; } + [IgnoreDataMember] + public Guid? SeriesId { get; set; } + [IgnoreDataMember] + public string SeriesSortName { get; set; } + + public string FindSeriesSortName() + { + return SeriesSortName; + } + public string FindSeriesName() + { + return SeriesName; + } + + public Guid? FindSeriesId() + { + return SeriesId; + } public override bool CanDownload() { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 210c115641..89b9479e7b 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -760,11 +760,6 @@ namespace MediaBrowser.Controller.Entities Logger.Debug("Query requires post-filtering due to ItemSortBy.Revenue"); return true; } - if (query.SortBy.Contains(ItemSortBy.SeriesSortName, StringComparer.OrdinalIgnoreCase)) - { - Logger.Debug("Query requires post-filtering due to ItemSortBy.SeriesSortName"); - return true; - } if (query.SortBy.Contains(ItemSortBy.VideoBitRate, StringComparer.OrdinalIgnoreCase)) { Logger.Debug("Query requires post-filtering due to ItemSortBy.VideoBitRate"); @@ -859,24 +854,6 @@ namespace MediaBrowser.Controller.Entities return true; } - if (query.IsMissing.HasValue) - { - Logger.Debug("Query requires post-filtering due to IsMissing"); - return true; - } - - if (query.IsUnaired.HasValue) - { - Logger.Debug("Query requires post-filtering due to IsUnaired"); - return true; - } - - if (query.IsVirtualUnaired.HasValue) - { - Logger.Debug("Query requires post-filtering due to IsVirtualUnaired"); - return true; - } - if (UserViewBuilder.CollapseBoxSetItems(query, this, query.User, ConfigurationManager)) { Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems"); diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs index 64c33a3766..531f587881 100644 --- a/MediaBrowser.Controller/Entities/IHasSeries.cs +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -1,4 +1,6 @@  +using System; + namespace MediaBrowser.Controller.Entities { public interface IHasSeries @@ -7,6 +9,11 @@ namespace MediaBrowser.Controller.Entities /// Gets the name of the series. /// /// The name of the series. - string SeriesName { get; } + string SeriesName { get; set; } + string FindSeriesName(); + string SeriesSortName { get; set; } + string FindSeriesSortName(); + Guid? SeriesId { get; set; } + Guid? FindSeriesId(); } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 2dc459239d..726390f65e 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -13,7 +13,6 @@ namespace MediaBrowser.Controller.Entities.TV /// public class Episode : Video, IHasTrailers, IHasLookupInfo, IHasSeries { - public Episode() { RemoteTrailers = new List(); @@ -25,11 +24,11 @@ namespace MediaBrowser.Controller.Entities.TV public List RemoteTrailerIds { get; set; } public List RemoteTrailers { get; set; } - /// - /// Gets the season in which it aired. - /// - /// The aired season. - public int? AirsBeforeSeasonNumber { get; set; } + /// + /// Gets the season in which it aired. + /// + /// The aired season. + public int? AirsBeforeSeasonNumber { get; set; } public int? AirsAfterSeasonNumber { get; set; } public int? AirsBeforeEpisodeNumber { get; set; } @@ -54,7 +53,16 @@ namespace MediaBrowser.Controller.Entities.TV /// This is the ending episode number for double episodes. /// /// The index number. - public int? IndexNumberEnd { get; set; } + public int? IndexNumberEnd { get; set; } + + [IgnoreDataMember] + public string SeriesSortName { get; set; } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesSortName : series.SortName; + } [IgnoreDataMember] protected override bool SupportsOwnedItems @@ -166,13 +174,27 @@ namespace MediaBrowser.Controller.Entities.TV } [IgnoreDataMember] - public string SeriesName - { - get - { - var series = Series; - return series == null ? null : series.Name; - } + public string SeriesName { get; set; } + + [IgnoreDataMember] + public string SeasonName { get; set; } + + public string FindSeasonName() + { + var season = Season; + return season == null ? SeasonName : season.Name; + } + + public string FindSeriesName() + { + var series = Series; + return series == null ? SeriesName : series.Name; + } + + public Guid? FindSeasonId() + { + var season = Season; + return season == null ? (Guid?)null : season.Id; } /// @@ -235,20 +257,14 @@ namespace MediaBrowser.Controller.Entities.TV } [IgnoreDataMember] - public Guid? SeasonId - { - get - { - // First see if the parent is a Season - var season = Season; - - if (season != null) - { - return season.Id; - } - - return null; - } + public Guid? SeasonId { get; set; } + [IgnoreDataMember] + public Guid? SeriesId { get; set; } + + public Guid? FindSeriesId() + { + var series = Series; + return series == null ? (Guid?)null : series.Id; } public override IEnumerable GetAncestorIds() diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 9a90148443..ee01c60b1b 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -51,6 +51,15 @@ namespace MediaBrowser.Controller.Entities.TV } } + [IgnoreDataMember] + public string SeriesSortName { get; set; } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesSortName : series.SortName; + } + // Genre, Rating and Stuido will all be the same protected override IEnumerable GetIndexByOptions() { @@ -235,13 +244,21 @@ namespace MediaBrowser.Controller.Entities.TV } [IgnoreDataMember] - public string SeriesName + public string SeriesName { get; set; } + + [IgnoreDataMember] + public Guid? SeriesId { get; set; } + + public string FindSeriesName() { - get - { - var series = Series; - return series == null ? null : series.Name; - } + var series = Series; + return series == null ? SeriesName : series.Name; + } + + public Guid? FindSeriesId() + { + var series = Series; + return series == null ? (Guid?)null : series.Id; } /// diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs index cd7ee603e3..49d4614d81 100644 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -28,6 +28,8 @@ namespace MediaBrowser.Controller.Net /// System.Object. object GetResult(object content, string contentType, IDictionary responseHeaders = null); + object GetAsyncStreamWriter(Func streamWriter, IDictionary responseHeaders = null); + /// /// Gets the optimized result. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index 725f0bc6d0..32cd950afe 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -366,6 +366,14 @@ namespace MediaBrowser.MediaEncoding.Encoder return null; } + // 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) + { + return null; + } + if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec)) { if (string.Equals(GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9a83aba88b..a63aca11b3 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -120,6 +120,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles bool preserveOriginalTimestamps, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(itemId)) + { + throw new ArgumentNullException("itemId"); + } + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + throw new ArgumentNullException("mediaSourceId"); + } + var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) .ConfigureAwait(false); @@ -141,10 +150,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles int subtitleStreamIndex, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(itemId)) + { + throw new ArgumentNullException("itemId"); + } + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + throw new ArgumentNullException("mediaSourceId"); + } + var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(itemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false); var mediaSource = mediaSources - .First(i => string.Equals(i.Id, mediaSourceId)); + .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); var subtitleStream = mediaSource.MediaStreams .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex); @@ -609,7 +627,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw; } - process.StandardError.BaseStream.CopyToAsync(logFileStream); + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback + Task.Run(() => StartStreamingLog(process.StandardError.BaseStream, logFileStream)); var ranToCompletion = process.WaitForExit(300000); @@ -686,6 +705,33 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + private async Task StartStreamingLog(Stream source, Stream target) + { + try + { + using (var reader = new StreamReader(source)) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await target.FlushAsync().ConfigureAwait(false); + } + } + } + catch (ObjectDisposedException) + { + // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux + } + catch (Exception ex) + { + _logger.ErrorException("Error reading ffmpeg log", ex); + } + } + /// /// Sets the ass font. /// diff --git a/MediaBrowser.Model.Portable/FodyWeavers.xml b/MediaBrowser.Model.Portable/FodyWeavers.xml index 7369928101..6e2fa02e64 100644 --- a/MediaBrowser.Model.Portable/FodyWeavers.xml +++ b/MediaBrowser.Model.Portable/FodyWeavers.xml @@ -1,4 +1,3 @@ - + - \ No newline at end of file diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 0de9fb519c..862d95f7ee 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -36,6 +36,8 @@ false true true + + true @@ -617,9 +619,6 @@ Extensions\FloatHelper.cs - - Extensions\IHasPropertyChangedEvent.cs - Extensions\IntHelper.cs @@ -1233,13 +1232,6 @@ - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - -