diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f195c125f1..3d6023b829 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -128,6 +128,7 @@ - [xosdy](https://github.com/xosdy) - [XVicarious](https://github.com/XVicarious) - [YouKnowBlom](https://github.com/YouKnowBlom) + - [KristupasSavickas](https://github.com/KristupasSavickas) # Emby Contributors diff --git a/Dockerfile.arm b/Dockerfile.arm index c5189d6aa3..39beaa4791 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -74,4 +74,4 @@ VOLUME /cache /config /media ENTRYPOINT ["./jellyfin/jellyfin", \ "--datadir", "/config", \ "--cachedir", "/cache", \ - "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"] + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ad0a69b19e..03ce9a9a9f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -30,7 +30,6 @@ using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Devices; -using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; @@ -86,7 +85,6 @@ using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -574,8 +572,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -1462,15 +1458,17 @@ namespace Emby.Server.Implementations throw new NotSupportedException(); } - var process = Resolve().Create(new ProcessOptions + var process = new Process { - FileName = url, - EnableRaisingEvents = true, - UseShellExecute = true, - ErrorDialog = false - }); - - process.Exited += ProcessExited; + StartInfo = new ProcessStartInfo + { + FileName = url, + UseShellExecute = true, + ErrorDialog = false + }, + EnableRaisingEvents = true + }; + process.Exited += (sender, args) => ((Process)sender).Dispose(); try { @@ -1483,11 +1481,6 @@ namespace Emby.Server.Implementations } } - private static void ProcessExited(object sender, EventArgs e) - { - ((IProcess)sender).Dispose(); - } - public virtual void EnableLoopback(string appName) { } diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs deleted file mode 100644 index bfa49ac5ff..0000000000 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ /dev/null @@ -1,152 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Diagnostics; - -namespace Emby.Server.Implementations.Diagnostics -{ - public class CommonProcess : IProcess - { - private readonly Process _process; - - private bool _disposed = false; - private bool _hasExited; - - public CommonProcess(ProcessOptions options) - { - StartInfo = options; - - var startInfo = new ProcessStartInfo - { - Arguments = options.Arguments, - FileName = options.FileName, - WorkingDirectory = options.WorkingDirectory, - UseShellExecute = options.UseShellExecute, - CreateNoWindow = options.CreateNoWindow, - RedirectStandardError = options.RedirectStandardError, - RedirectStandardInput = options.RedirectStandardInput, - RedirectStandardOutput = options.RedirectStandardOutput, - ErrorDialog = options.ErrorDialog - }; - - - if (options.IsHidden) - { - startInfo.WindowStyle = ProcessWindowStyle.Hidden; - } - - _process = new Process - { - StartInfo = startInfo - }; - - if (options.EnableRaisingEvents) - { - _process.EnableRaisingEvents = true; - _process.Exited += OnProcessExited; - } - } - - public event EventHandler Exited; - - public ProcessOptions StartInfo { get; } - - public StreamWriter StandardInput => _process.StandardInput; - - public StreamReader StandardError => _process.StandardError; - - public StreamReader StandardOutput => _process.StandardOutput; - - public int ExitCode => _process.ExitCode; - - private bool HasExited - { - get - { - if (_hasExited) - { - return true; - } - - try - { - _hasExited = _process.HasExited; - } - catch (InvalidOperationException) - { - _hasExited = true; - } - - return _hasExited; - } - } - - public void Start() - { - _process.Start(); - } - - public void Kill() - { - _process.Kill(); - } - - public bool WaitForExit(int timeMs) - { - return _process.WaitForExit(timeMs); - } - - public Task WaitForExitAsync(int timeMs) - { - // Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true. - - if (HasExited) - { - return Task.FromResult(true); - } - - timeMs = Math.Max(0, timeMs); - - var tcs = new TaskCompletionSource(); - - var cancellationToken = new CancellationTokenSource(timeMs).Token; - - _process.Exited += (sender, args) => tcs.TrySetResult(true); - - cancellationToken.Register(() => tcs.TrySetResult(HasExited)); - - return tcs.Task; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _process?.Dispose(); - } - - _disposed = true; - } - - private void OnProcessExited(object sender, EventArgs e) - { - _hasExited = true; - Exited?.Invoke(this, e); - } - } -} diff --git a/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs b/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs deleted file mode 100644 index 02ad3c1a89..0000000000 --- a/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Model.Diagnostics; - -namespace Emby.Server.Implementations.Diagnostics -{ - public class ProcessFactory : IProcessFactory - { - public IProcess Create(ProcessOptions options) - { - return new CommonProcess(options); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 72667a3144..5ae65a4e3d 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -239,7 +239,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace) + private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, string urlToLog) { try { @@ -247,11 +247,11 @@ namespace Emby.Server.Implementations.HttpServer if (logExceptionStackTrace) { - _logger.LogError(ex, "Error processing request"); + _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); } else { - _logger.LogError("Error processing request: {Message}", ex.Message); + _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); } var httpRes = httpReq.Response; @@ -271,7 +271,7 @@ namespace Emby.Server.Implementations.HttpServer } catch (Exception errorEx) { - _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)"); + _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response). URL: {Url}", urlToLog); } } @@ -456,7 +456,7 @@ namespace Emby.Server.Implementations.HttpServer var stopWatch = new Stopwatch(); stopWatch.Start(); var httpRes = httpReq.Response; - string urlToLog = null; + string urlToLog = GetUrlToLog(urlString); string remoteIp = httpReq.RemoteIp; try @@ -502,8 +502,6 @@ namespace Emby.Server.Implementations.HttpServer return; } - urlToLog = GetUrlToLog(urlString); - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) @@ -553,7 +551,7 @@ namespace Emby.Server.Implementations.HttpServer || ex is OperationCanceledException || ex is SecurityException || ex is FileNotFoundException; - await ErrorHandler(ex, httpReq, ignoreStackTrace).ConfigureAwait(false); + await ErrorHandler(ex, httpReq, !ignoreStackTrace, urlToLog).ConfigureAwait(false); } finally { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 139aa19a4b..900f12062f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -25,7 +26,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; @@ -61,7 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IProcessFactory _processFactory; private readonly IMediaSourceManager _mediaSourceManager; private readonly IStreamHelper _streamHelper; @@ -88,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, - IMediaEncoder mediaEncoder, - IProcessFactory processFactory) + IMediaEncoder mediaEncoder) { Current = this; @@ -102,7 +100,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _libraryMonitor = libraryMonitor; _providerManager = providerManager; _mediaEncoder = mediaEncoder; - _processFactory = processFactory; _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; _mediaSourceManager = mediaSourceManager; @@ -1662,7 +1659,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); } return new DirectRecorder(_logger, _httpClient, _streamHelper); @@ -1683,16 +1680,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - var process = _processFactory.Create(new ProcessOptions + var process = new Process { - Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments), - CreateNoWindow = true, - EnableRaisingEvents = true, - ErrorDialog = false, - FileName = options.RecordingPostProcessor, - IsHidden = true, - UseShellExecute = false - }); + StartInfo = new ProcessStartInfo + { + Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments), + CreateNoWindow = true, + ErrorDialog = false, + FileName = options.RecordingPostProcessor, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false + }, + EnableRaisingEvents = true + }; _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -1712,11 +1712,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void Process_Exited(object sender, EventArgs e) { - using (var process = (IProcess)sender) + using (var process = (Process)sender) { _logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode); - - process.Dispose(); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index d24fc67922..bc86cc59a2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; @@ -13,7 +14,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -29,8 +29,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private bool _hasExited; private Stream _logFileStream; private string _targetPath; - private IProcess _process; - private readonly IProcessFactory _processFactory; + private Process _process; private readonly IJsonSerializer _json; private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); private readonly IServerConfigurationManager _config; @@ -40,14 +39,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, - IProcessFactory processFactory, IServerConfigurationManager config) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _processFactory = processFactory; _config = config; } @@ -79,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _targetPath = targetFile; Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - var process = _processFactory.Create(new ProcessOptions + var processStartInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, @@ -90,14 +87,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV FileName = _mediaEncoder.EncoderPath, Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration), - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true - }); + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; - _process = process; - - var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; + var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments; _logger.LogInformation(commandLineLogMessage); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); @@ -109,16 +103,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); - process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile); + _process = new Process + { + StartInfo = processStartInfo, + EnableRaisingEvents = true + }; + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); - process.Start(); + _process.Start(); cancellationToken.Register(Stop); onStarted(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - StartStreamingLog(process.StandardError.BaseStream, _logFileStream); + StartStreamingLog(_process.StandardError.BaseStream, _logFileStream); _logger.LogInformation("ffmpeg recording process started for {0}", _targetPath); @@ -292,30 +291,33 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// /// Processes the exited. /// - private void OnFfMpegProcessExited(IProcess process, string inputFile) + private void OnFfMpegProcessExited(Process process, string inputFile) { - _hasExited = true; - - _logFileStream?.Dispose(); - _logFileStream = null; - - var exitCode = process.ExitCode; - - _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath); - - if (exitCode == 0) + using (process) { - _taskCompletionSource.TrySetResult(true); - } - else - { - _taskCompletionSource.TrySetException( - new Exception( - string.Format( - CultureInfo.InvariantCulture, - "Recording for {0} failed. Exit code {1}", - _targetPath, - exitCode))); + _hasExited = true; + + _logFileStream?.Dispose(); + _logFileStream = null; + + var exitCode = process.ExitCode; + + _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath); + + if (exitCode == 0) + { + _taskCompletionSource.TrySetResult(true); + } + else + { + _taskCompletionSource.TrySetException( + new Exception( + string.Format( + CultureInfo.InvariantCulture, + "Recording for {0} failed. Exit code {1}", + _targetPath, + exitCode))); + } } } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 92719a9fdd..93b8052d3e 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -1,5 +1,5 @@ { - "Albums": "Album", + "Albums": "Albums", "AppDeviceValues": "App: {0}, Enhed: {1}", "Application": "Applikation", "Artists": "Kunstnere", @@ -106,5 +106,7 @@ "TasksChannelsCategory": "Internet Kanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", - "TasksMaintenanceCategory": "Vedligeholdelse" + "TasksMaintenanceCategory": "Vedligeholdelse", + "TaskRefreshChapterImages": "Udtræk Kapitel billeder", + "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler." } diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index dc4f0b212a..544c38cfa9 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.", + "TaskDownloadMissingSubtitles": "Download missing subtitles", + "TaskRefreshChannelsDescription": "Refreshes internet channel information.", + "TaskRefreshChannels": "Refresh Channels", + "TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.", + "TaskCleanTranscode": "Clean Transcode Directory", + "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.", + "TaskUpdatePlugins": "Update Plugins", + "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.", + "TaskRefreshPeople": "Refresh People", + "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.", + "TaskCleanLogs": "Clean Log Directory", + "TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.", + "TaskRefreshLibrary": "Scan Media Library", + "TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.", + "TaskRefreshChapterImages": "Extract Chapter Images", + "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.", + "TaskCleanCache": "Clean Cache Directory", + "TasksChannelsCategory": "Internet Channels", + "TasksApplicationCategory": "Application", + "TasksLibraryCategory": "Library", + "TasksMaintenanceCategory": "Maintenance" } diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 1a7b57c534..0ef16542f4 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -5,7 +5,7 @@ "Collections": "Colecciones", "Artists": "Artistas", "DeviceOnlineWithName": "{0} está conectado", - "DeviceOfflineWithName": "{0} ha desconectado", + "DeviceOfflineWithName": "{0} se ha desconectado", "ChapterNameValue": "Capítulo {0}", "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", "AuthenticationSucceededWithUserName": "{0} autenticado con éxito", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 45e74b8ebe..be6f87ee3f 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه", "HeaderFavoriteShows": "سریال‌های مورد علاقه", "HeaderFavoriteSongs": "آهنگ‌های مورد علاقه", - "HeaderLiveTV": "پخش زنده تلویزیون", + "HeaderLiveTV": "تلویزیون زنده", "HeaderNextUp": "قسمت بعدی", "HeaderRecordingGroups": "گروه‌های ضبط", "HomeVideos": "ویدیوهای خانگی", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} پخش {1} را بر روی {2} به پایان رساند", "ValueHasBeenAddedToLibrary": "{0} به کتابخانه‌ی رسانه‌ی شما افزوده شد", "ValueSpecialEpisodeName": "ویژه - {0}", - "VersionNumber": "نسخه {0}" + "VersionNumber": "نسخه {0}", + "TaskCleanTranscodeDescription": "فایل‌های کدگذاری که قدیمی‌تر از یک روز هستند را حذف می‌کند.", + "TaskCleanTranscode": "پاکسازی مسیر کد گذاری", + "TaskUpdatePluginsDescription": "دانلود و نصب به روز رسانی افزونه‌هایی که برای به روز رسانی خودکار پیکربندی شده‌اند.", + "TaskDownloadMissingSubtitlesDescription": "جستجوی زیرنویس‌های ناموجود در اینترنت بر اساس پیکربندی ابرداده‌ها.", + "TaskDownloadMissingSubtitles": "دانلود زیرنویس‌های ناموجود", + "TaskRefreshChannelsDescription": "اطلاعات کانال اینترنتی را تازه سازی می‌کند.", + "TaskRefreshChannels": "تازه سازی کانال‌ها", + "TaskUpdatePlugins": "به روز رسانی افزونه‌ها", + "TaskRefreshPeopleDescription": "ابرداده‌ها برای بازیگران و کارگردانان در کتابخانه رسانه شما به روزرسانی می شوند.", + "TaskRefreshPeople": "تازه سازی افراد", + "TaskCleanLogsDescription": "واقعه نگارهایی را که قدیمی تر {0} روز هستند را حذف می کند.", + "TaskCleanLogs": "پاکسازی مسیر واقعه نگار", + "TaskRefreshLibraryDescription": "کتابخانه رسانه شما را اسکن می‌کند و ابرداده‌ها را تازه سازی می‌کند.", + "TaskRefreshLibrary": "اسکن کتابخانه رسانه", + "TaskRefreshChapterImagesDescription": "عکس‌های کوچک برای ویدیوهایی که سکانس دارند ایجاد می‌کند.", + "TaskRefreshChapterImages": "استخراج عکس‌های سکانس", + "TaskCleanCacheDescription": "فایل‌های حافظه موقت که توسط سیستم دیگر مورد نیاز نیستند حذف می‌شوند.", + "TaskCleanCache": "پاکسازی مسیر حافظه موقت", + "TasksChannelsCategory": "کانال‌های داخلی", + "TasksApplicationCategory": "برنامه", + "TasksLibraryCategory": "کتابخانه", + "TasksMaintenanceCategory": "تعمیر" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index bf5fc05c4d..b39adefe70 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -1,5 +1,5 @@ { - "HeaderLiveTV": "TV-lähetykset", + "HeaderLiveTV": "Suorat lähetykset", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonNumber": "Kausi {0}", @@ -19,12 +19,12 @@ "ItemAddedWithName": "{0} lisättiin kirjastoon", "Inherit": "Periytyä", "HomeVideos": "Kotivideot", - "HeaderRecordingGroups": "Nauhoitusryhmät", + "HeaderRecordingGroups": "Nauhoiteryhmät", "HeaderNextUp": "Seuraavaksi", "HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteEpisodes": "Lempijaksot", - "HeaderCameraUploads": "Kameralataukset", + "HeaderCameraUploads": "Kamerasta Lähetetyt", "HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteAlbums": "Lempialbumit", "HeaderContinueWatching": "Jatka katsomista", @@ -63,10 +63,10 @@ "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", "UserOfflineFromDevice": "{0} yhteys katkaistu {1}", - "UserLockedOutWithName": "Käyttäjä {0} kirjautui ulos", - "UserDownloadingItemWithValues": "{0} latautumassa {1}", - "UserDeletedWithName": "Poistettiin käyttäjä {0}", - "UserCreatedWithName": "Luotiin käyttäjä {0}", + "UserLockedOutWithName": "Käyttäjä {0} lukittu", + "UserDownloadingItemWithValues": "{0} lataa {1}", + "UserDeletedWithName": "Käyttäjä {0} poistettu", + "UserCreatedWithName": "Käyttäjä {0} luotu", "TvShows": "TV-Ohjelmat", "Sync": "Synkronoi", "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}", @@ -74,22 +74,44 @@ "Songs": "Kappaleet", "Shows": "Ohjelmat", "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen", - "ProviderValue": "Palveluntarjoaja: {0}", + "ProviderValue": "Tarjoaja: {0}", "Plugin": "Liitännäinen", - "NotificationOptionVideoPlaybackStopped": "Videon toistaminen pysäytetty", - "NotificationOptionVideoPlayback": "Videon toistaminen aloitettu", - "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", - "NotificationOptionTaskFailed": "Ajastetun tehtävän ongelma", + "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", + "NotificationOptionVideoPlayback": "Videon toisto aloitettu", + "NotificationOptionUserLockedOut": "Käyttäjä lukittu", + "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan", - "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", + "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu", "NotificationOptionPluginError": "Ongelma liitännäisessä", "NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty", "NotificationOptionInstallationFailed": "Asennus epäonnistui", - "NotificationOptionCameraImageUploaded": "Kuva ladattu kamerasta", - "NotificationOptionAudioPlaybackStopped": "Audion toisto pysäytetty", - "NotificationOptionAudioPlayback": "Audion toisto aloitettu", - "NotificationOptionApplicationUpdateInstalled": "Ohjelmistopäivitys asennettu", - "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla" + "NotificationOptionCameraImageUploaded": "Kameran kuva ladattu", + "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu", + "NotificationOptionAudioPlayback": "Toistetaan ääntä", + "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu", + "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla", + "TasksMaintenanceCategory": "Ylläpito", + "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.", + "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset", + "TaskRefreshChannelsDescription": "Päivittää internet-kanavien tiedot.", + "TaskRefreshChannels": "Päivitä kanavat", + "TaskCleanTranscodeDescription": "Poistaa transkoodatut tiedostot jotka ovat yli päivän vanhoja.", + "TaskCleanTranscode": "Puhdista transkoodaushakemisto", + "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset liitännäisille jotka on asetettu päivittymään automaattisesti.", + "TaskUpdatePlugins": "Päivitä liitännäiset", + "TaskRefreshPeopleDescription": "Päivittää näyttelijöiden ja ohjaajien mediatiedot kirjastossasi.", + "TaskRefreshPeople": "Päivitä henkilöt", + "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.", + "TaskCleanLogs": "Puhdista lokihakemisto", + "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.", + "TaskRefreshLibrary": "Skannaa mediakirjasto", + "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.", + "TaskRefreshChapterImages": "Eristä lukujen kuvat", + "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.", + "TaskCleanCache": "Tyhjennä välimuisti-hakemisto", + "TasksChannelsCategory": "Internet kanavat", + "TasksApplicationCategory": "Sovellus", + "TasksLibraryCategory": "Kirjasto" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 88a7ac1906..d1403c494d 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une nouvelle photo a été chargée depuis {0}", + "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 6f226fe991..c5c3844e3f 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -71,7 +71,7 @@ "ScheduledTaskFailedWithName": "{0} sikertelen", "ScheduledTaskStartedWithName": "{0} elkezdve", "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani", - "Shows": "Műsorok", + "Shows": "Sorozatok", "Songs": "Dalok", "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 3d5f7cab24..661ee8603e 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -91,5 +91,9 @@ "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Application": "Aplicação", - "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}" + "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", + "TaskCleanCache": "Limpar Diretório de Cache", + "TasksApplicationCategory": "Aplicação", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Manutenção" } diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json index 0967ef424b..9a5874e295 100644 --- a/Emby.Server.Implementations/Localization/Core/ur_PK.json +++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json @@ -1 +1,117 @@ -{} +{ + "HeaderFavoriteAlbums": "پسندیدہ البمز", + "HeaderNextUp": "اگلا", + "HeaderFavoriteArtists": "پسندیدہ فنکار", + "HeaderAlbumArtists": "البم کے فنکار", + "Movies": "فلمیں", + "HeaderFavoriteEpisodes": "پسندیدہ اقساط", + "Collections": "مجموعہ", + "Folders": "فولڈرز", + "HeaderLiveTV": "براہ راست ٹی وی", + "Channels": "چینل", + "HeaderContinueWatching": "دیکھنا جاری رکھیں", + "Playlists": "پلے لسٹس", + "ValueSpecialEpisodeName": "خاص - {0}", + "Shows": "شوز", + "Genres": "انواع", + "Artists": "فنکار", + "Sync": "مطابقت", + "Photos": "تصوریں", + "Albums": "البم", + "Favorites": "پسندیدہ", + "Songs": "گانے", + "Books": "کتابیں", + "HeaderFavoriteSongs": "پسندیدہ گانے", + "HeaderFavoriteShows": "پسندیدہ شوز", + "TaskDownloadMissingSubtitlesDescription": "میٹا ڈیٹا کی تشکیل پر مبنی ذیلی عنوانات کے غائب عنوانات انٹرنیٹ پے تلاش کرتا ہے۔", + "TaskDownloadMissingSubtitles": "غائب سب ٹائٹلز ڈاؤن لوڈ کریں", + "TaskRefreshChannelsDescription": "انٹرنیٹ چینل کی معلومات کو تازہ دم کرتا ہے۔", + "TaskRefreshChannels": "چینلز ریفریش کریں", + "TaskCleanTranscodeDescription": "ایک دن سے زیادہ پرانی ٹرانسکوڈ فائلوں کو حذف کرتا ہے۔", + "TaskCleanTranscode": "ٹرانس کوڈ ڈائرکٹری صاف کریں", + "TaskUpdatePluginsDescription": "پلگ انز کے لئے اپ ڈیٹس ڈاؤن لوڈ اور انسٹال کرتے ہیں جو خود بخود اپ ڈیٹ کرنے کیلئے تشکیل شدہ ہیں۔", + "TaskUpdatePlugins": "پلگ انز کو اپ ڈیٹ کریں", + "TaskRefreshPeopleDescription": "آپ کی میڈیا لائبریری میں اداکاروں اور ہدایت کاروں کے لئے میٹا ڈیٹا کی تازہ کاری۔", + "TaskRefreshPeople": "لوگوں کو تروتازہ کریں", + "TaskCleanLogsDescription": "لاگ فائلوں کو حذف کریں جو {0} دن سے زیادہ پرانی ہیں۔", + "TaskCleanLogs": "لاگ ڈائرکٹری کو صاف کریں", + "TaskRefreshLibraryDescription": "میڈیا لائبریری کو اسکین کرتا ھے ہر میٹا دیٹا کہ تازہ دم کرتا ھے.", + "TaskRefreshLibrary": "اسکین میڈیا لائبریری", + "TaskRefreshChapterImagesDescription": "بابوں والی ویڈیوز کے لئے تمبنیل بنایں۔", + "TaskRefreshChapterImages": "باب کی تصاویر نکالیں", + "TaskCleanCacheDescription": "فائلوں کو حذف کریں جنکی ضرورت نھیں ھے۔", + "TaskCleanCache": "کیش ڈائرکٹری کلیر کریں", + "TasksChannelsCategory": "انٹرنیٹ چینلز", + "TasksApplicationCategory": "پروگرام", + "TasksLibraryCategory": "لآیبریری", + "TasksMaintenanceCategory": "مرمت", + "VersionNumber": "ورژن {0}", + "ValueHasBeenAddedToLibrary": "{0} آپ کی میڈیا لائبریری میں شامل کر دیا گیا ہے", + "UserStoppedPlayingItemWithValues": "{0} نے {1} چلانا ختم کر دیا ھے {2} پے", + "UserStartedPlayingItemWithValues": "{0} چلا رہا ہے {1} {2} پے", + "UserPolicyUpdatedWithName": "صارف {0} کی پالیسی کیلئے تازہ کاری کی گئی ہے", + "UserPasswordChangedWithName": "صارف {0} کے لئے پاس ورڈ تبدیل کر دیا گیا ہے", + "UserOnlineFromDevice": "{0} آن لائن ہے {1} سے", + "UserOfflineFromDevice": "{0} سے منقطع ہوگیا ہے {1}", + "UserLockedOutWithName": "صارف {0} کو لاک آؤٹ کردیا گیا ہے", + "UserDownloadingItemWithValues": "{0} ڈاؤن لوڈ کر رھا ھے {1}", + "UserDeletedWithName": "صارف {0} کو ہٹا دیا گیا ہے", + "UserCreatedWithName": "صارف {0} تشکیل دیا گیا ہے", + "User": "صارف", + "TvShows": "ٹی وی کے پروگرام", + "System": "نظام", + "SubtitleDownloadFailureFromForItem": "ذیلی عنوانات {0} سے ڈاؤن لوڈ کرنے میں ناکام {1} کے لیے", + "StartupEmbyServerIsLoading": "جیلیفن سرور لوڈ ہورہا ہے۔ براہ کرم جلد ہی دوبارہ کوشش کریں۔", + "ServerNameNeedsToBeRestarted": "{0} دوبارہ چلانے کرنے کی ضرورت ہے", + "ScheduledTaskStartedWithName": "{0} شروع", + "ScheduledTaskFailedWithName": "{0} ناکام", + "ProviderValue": "فراہم کرنے والا: {0}", + "PluginUpdatedWithName": "{0} تازہ کاری کی گئی تھی", + "PluginUninstalledWithName": "[0} ہٹا دیا گیا تھا", + "PluginInstalledWithName": "{0} انسٹال کیا گیا تھا", + "Plugin": "پلگن", + "NotificationOptionVideoPlaybackStopped": "ویڈیو پلے بیک رک گیا", + "NotificationOptionVideoPlayback": "ویڈیو پلے بیک شروع ہوا", + "NotificationOptionUserLockedOut": "صارف کو لاک آؤٹ کیا گیا", + "NotificationOptionTaskFailed": "طے شدہ کام کی ناکامی", + "NotificationOptionServerRestartRequired": "سرور دوبارہ چلانے کرنے کی ضرورت ہے", + "NotificationOptionPluginUpdateInstalled": "پلگ ان اپ ڈیٹ انسٹال", + "NotificationOptionPluginUninstalled": "پلگ ان ہٹا دیا گیا", + "NotificationOptionPluginInstalled": "پلگ ان انسٹال ہوا", + "NotificationOptionPluginError": "پلگ ان کی ناکامی", + "NotificationOptionNewLibraryContent": "نیا مواد شامل کیا گیا", + "NotificationOptionInstallationFailed": "تنصیب کی ناکامی", + "NotificationOptionCameraImageUploaded": "کیمرے کی تصویر اپ لوڈ ہوگئی", + "NotificationOptionAudioPlaybackStopped": "آڈیو پلے بیک رک گیا", + "NotificationOptionAudioPlayback": "آڈیو پلے بیک شروع ہوا", + "NotificationOptionApplicationUpdateInstalled": "پروگرام اپ ڈیٹ انسٹال ہوچکا ھے", + "NotificationOptionApplicationUpdateAvailable": "پروگرام کی تازہ کاری دستیاب ہے", + "NewVersionIsAvailable": "جیلیفن سرور کا ایک نیا ورژن ڈاؤن لوڈ کے لئے دستیاب ہے۔", + "NameSeasonUnknown": "نامعلوم باب", + "NameSeasonNumber": "باب {0}", + "NameInstallFailed": "{0} تنصیب ناکام ہوگئی", + "MusicVideos": "موسیقی ویڈیو", + "Music": "موسیقی", + "MixedContent": "مخلوط مواد", + "MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے", + "MessageNamedServerConfigurationUpdatedWithValue": "سرور ضمن {0} کو ترتیب دے دیا گیا ھے", + "MessageApplicationUpdatedTo": "جیلیفن سرور کو اپ ڈیٹ کیا ہے {0}", + "MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے", + "Latest": "تازہ ترین", + "LabelRunningTimeValue": "چلانے کی مدت", + "LabelIpAddressValue": "ای پی پتے {0}", + "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے", + "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے", + "Inherit": "وراثت میں", + "HomeVideos": "ہوم ویڈیو", + "HeaderRecordingGroups": "ریکارڈنگ گروپس", + "HeaderCameraUploads": "کیمرہ اپلوڈز", + "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}", + "DeviceOnlineWithName": "{0} متصل ھو چکا ھے", + "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے", + "ChapterNameValue": "باب", + "AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے", + "CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}", + "Application": "پروگرام", + "AppDeviceValues": "پروگرام:{0}, آلہ:{1}" +} diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 4bd13df003..6691080bc8 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -86,12 +86,9 @@ namespace MediaBrowser.Api return Array.Empty(); } - if (removeEmpty) - { - return value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries); - } - - return value.Split(separator); + return removeEmpty + ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) + : value.Split(separator); } public SemaphoreSlim GetTranscodingLock(string outputPath) @@ -258,7 +255,7 @@ namespace MediaBrowser.Api 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; + var ticks = transcodingPosition?.Ticks; if (job != null) { @@ -487,16 +484,9 @@ namespace MediaBrowser.Api /// Task. internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func deleteFiles) { - return KillTranscodingJobs(j => - { - if (!string.IsNullOrWhiteSpace(playSessionId)) - { - return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase); - - }, deleteFiles); + return KillTranscodingJobs(j => string.IsNullOrWhiteSpace(playSessionId) + ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase) + : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles); } /// @@ -561,10 +551,7 @@ namespace MediaBrowser.Api lock (job.ProcessLock) { - if (job.TranscodingThrottler != null) - { - job.TranscodingThrottler.Stop().GetAwaiter().GetResult(); - } + job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 112ee8f79a..1a1d86362a 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -58,12 +58,9 @@ namespace MediaBrowser.Api public static string[] SplitValue(string value, char delim) { - if (value == null) - { - return Array.Empty(); - } - - return value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries); + return value == null + ? Array.Empty() + : value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries); } public static Guid[] GetGuids(string value) @@ -97,19 +94,10 @@ namespace MediaBrowser.Api var authenticatedUser = auth.User; // If they're going to update the record of another user, they must be an administrator - if (!userId.Equals(auth.UserId)) + if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator) + || (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess)) { - if (!authenticatedUser.Policy.IsAdministrator) - { - throw new SecurityException("Unauthorized access."); - } - } - else if (restrictUserPreferences) - { - if (!authenticatedUser.Policy.EnableUserPreferenceAccess) - { - throw new SecurityException("Unauthorized access."); - } + throw new SecurityException("Unauthorized access."); } } @@ -138,8 +126,8 @@ namespace MediaBrowser.Api options.Fields = hasFields.GetItemFields(); } - if (!options.ContainsField(Model.Querying.ItemFields.RecursiveItemCount) - || !options.ContainsField(Model.Querying.ItemFields.ChildCount)) + if (!options.ContainsField(ItemFields.RecursiveItemCount) + || !options.ContainsField(ItemFields.ChildCount)) { var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || @@ -150,7 +138,7 @@ namespace MediaBrowser.Api int oldLen = options.Fields.Length; var arr = new ItemFields[oldLen + 1]; options.Fields.CopyTo(arr, 0); - arr[oldLen] = Model.Querying.ItemFields.RecursiveItemCount; + arr[oldLen] = ItemFields.RecursiveItemCount; options.Fields = arr; } @@ -166,7 +154,7 @@ namespace MediaBrowser.Api int oldLen = options.Fields.Length; var arr = new ItemFields[oldLen + 1]; options.Fields.CopyTo(arr, 0); - arr[oldLen] = Model.Querying.ItemFields.ChildCount; + arr[oldLen] = ItemFields.ChildCount; options.Fields = arr; } } @@ -282,27 +270,21 @@ namespace MediaBrowser.Api }).OfType().FirstOrDefault(); - if (result == null) + result ??= libraryManager.GetItemList(new InternalItemsQuery { - result = libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { typeof(T).Name }, - DtoOptions = dtoOptions + Name = name.Replace(BaseItem.SlugChar, '/'), + IncludeItemTypes = new[] { typeof(T).Name }, + DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - } + }).OfType().FirstOrDefault(); - if (result == null) + result ??= libraryManager.GetItemList(new InternalItemsQuery { - result = libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { typeof(T).Name }, - DtoOptions = dtoOptions + Name = name.Replace(BaseItem.SlugChar, '?'), + IncludeItemTypes = new[] { typeof(T).Name }, + DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - } + }).OfType().FirstOrDefault(); return result; } diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs index 6139ba1560..fd9b8c3968 100644 --- a/MediaBrowser.Api/ChannelService.cs +++ b/MediaBrowser.Api/ChannelService.cs @@ -116,12 +116,9 @@ namespace MediaBrowser.Api { var val = Filters; - if (string.IsNullOrEmpty(val)) - { - return new ItemFilter[] { }; - } - - return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)); + return string.IsNullOrEmpty(val) + ? Array.Empty() + : val.Split(',').Select(v => Enum.Parse(v, true)); } /// @@ -173,14 +170,9 @@ namespace MediaBrowser.Api /// IEnumerable{ItemFilter}. public IEnumerable GetFilters() { - var val = Filters; - - if (string.IsNullOrEmpty(val)) - { - return new ItemFilter[] { }; - } - - return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)); + return string.IsNullOrEmpty(Filters) + ? Array.Empty() + : Filters.Split(',').Select(v => Enum.Parse(v, true)); } } @@ -241,7 +233,7 @@ namespace MediaBrowser.Api { Limit = request.Limit, StartIndex = request.StartIndex, - ChannelIds = new Guid[] { new Guid(request.Id) }, + ChannelIds = new[] { new Guid(request.Id) }, ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId), OrderBy = request.GetOrderBy(), DtoOptions = new Controller.Dto.DtoOptions diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs index 8b63decd22..7004a2559e 100644 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ b/MediaBrowser.Api/Devices/DeviceService.cs @@ -155,16 +155,14 @@ namespace MediaBrowser.Api.Devices Id = id }); } - else + + return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo { - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } + MimeType = Request.ContentType, + Album = album, + Name = name, + Id = id + }); } } } diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs index 36b03f09ce..d199ce1544 100644 --- a/MediaBrowser.Api/EnvironmentService.cs +++ b/MediaBrowser.Api/EnvironmentService.cs @@ -258,12 +258,7 @@ namespace MediaBrowser.Api return false; } - if (!request.IncludeDirectories && isDirectory) - { - return false; - } - - return true; + return request.IncludeDirectories || !isDirectory; }); return entries.Select(f => new FileSystemEntryInfo diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 25f23bcd1e..5eb72cdb19 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -133,7 +133,7 @@ namespace MediaBrowser.Api // Non recursive not yet supported for library folders if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) { - genreQuery.AncestorIds = parentItem == null ? Array.Empty() : new Guid[] { parentItem.Id }; + genreQuery.AncestorIds = parentItem == null ? Array.Empty() : new[] { parentItem.Id }; } else { @@ -231,7 +231,7 @@ namespace MediaBrowser.Api EnableTotalRecordCount = false, DtoOptions = new Controller.Dto.DtoOptions { - Fields = new ItemFields[] { ItemFields.Genres, ItemFields.Tags }, + Fields = new[] { ItemFields.Genres, ItemFields.Tags }, EnableImages = false, EnableUserData = false } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index c11e0c1b7e..2e9b3e6cb4 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -657,7 +657,7 @@ namespace MediaBrowser.Api.Images if (!string.IsNullOrWhiteSpace(request.Format) && Enum.TryParse(request.Format, true, out ImageFormat format)) { - return new ImageFormat[] { format }; + return new[] { format }; } return GetClientSupportedFormats(); @@ -750,24 +750,22 @@ namespace MediaBrowser.Api.Images /// Task. public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType) { - using (var reader = new StreamReader(inputStream)) + using var reader = new StreamReader(inputStream); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + + var bytes = Convert.FromBase64String(text); + + var memoryStream = new MemoryStream(bytes) { - var text = await reader.ReadToEndAsync().ConfigureAwait(false); + Position = 0 + }; - var bytes = Convert.FromBase64String(text); + // Handle image/png; charset=utf-8 + mimeType = mimeType.Split(';').FirstOrDefault(); - var memoryStream = new MemoryStream(bytes) - { - Position = 0 - }; + await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - // Handle image/png; charset=utf-8 - mimeType = mimeType.Split(';').FirstOrDefault(); - - await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - - entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - } + entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } } } diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index f03f5efd8d..222bb34d31 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -261,27 +261,25 @@ namespace MediaBrowser.Api.Images /// Task. private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) { - using (var result = await _httpClient.GetResponse(new HttpRequestOptions + using var result = await _httpClient.GetResponse(new HttpRequestOptions { Url = url, BufferContent = false - }).ConfigureAwait(false)) + }).ConfigureAwait(false); + var ext = result.ContentType.Split('/').Last(); + + var fullCachePath = GetFullCachePath(urlHash + "." + ext); + + Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); + using (var stream = result.Content) { - var ext = result.ContentType.Split('/').Last(); - - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) - using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) - { - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } - - Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); - File.WriteAllText(pointerCachePath, fullCachePath); + using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await stream.CopyToAsync(filestream).ConfigureAwait(false); } + + Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); + File.WriteAllText(pointerCachePath, fullCachePath); } /// diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index a76369a157..0bbe7e1cfa 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -305,9 +305,16 @@ namespace MediaBrowser.Api Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) - using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { - await stream.CopyToAsync(filestream).ConfigureAwait(false); + using var fileStream = new FileStream( + fullCachePath, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + IODefaults.FileStreamBufferSize, + true); + + await stream.CopyToAsync(fileStream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index c81e89ca3c..2db6d717aa 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -263,8 +263,7 @@ namespace MediaBrowser.Api item.Overview = request.Overview; item.Genres = request.Genres; - var episode = item as Episode; - if (episode != null) + if (item is Episode episode) { episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber; episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber; @@ -302,14 +301,12 @@ namespace MediaBrowser.Api item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode; item.PreferredMetadataLanguage = request.PreferredMetadataLanguage; - var hasDisplayOrder = item as IHasDisplayOrder; - if (hasDisplayOrder != null) + if (item is IHasDisplayOrder hasDisplayOrder) { hasDisplayOrder.DisplayOrder = request.DisplayOrder; } - var hasAspectRatio = item as IHasAspectRatio; - if (hasAspectRatio != null) + if (item is IHasAspectRatio hasAspectRatio) { hasAspectRatio.AspectRatio = request.AspectRatio; } @@ -337,16 +334,14 @@ namespace MediaBrowser.Api item.ProviderIds = request.ProviderIds; - var video = item as Video; - if (video != null) + if (item is Video video) { video.Video3DFormat = request.Video3DFormat; } if (request.AlbumArtists != null) { - var hasAlbumArtists = item as IHasAlbumArtist; - if (hasAlbumArtists != null) + if (item is IHasAlbumArtist hasAlbumArtists) { hasAlbumArtists.AlbumArtists = request .AlbumArtists @@ -357,8 +352,7 @@ namespace MediaBrowser.Api if (request.ArtistItems != null) { - var hasArtists = item as IHasArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { hasArtists.Artists = request .ArtistItems @@ -367,20 +361,17 @@ namespace MediaBrowser.Api } } - var song = item as Audio; - if (song != null) + if (item is Audio song) { song.Album = request.Album; } - var musicVideo = item as MusicVideo; - if (musicVideo != null) + if (item is MusicVideo musicVideo) { musicVideo.Album = request.Album; } - var series = item as Series; - if (series != null) + if (item is Series series) { series.Status = GetSeriesStatus(request); @@ -400,7 +391,6 @@ namespace MediaBrowser.Api } return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); - } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 15284958d7..a54640b2fd 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -348,28 +348,19 @@ namespace MediaBrowser.Api.Library private string[] GetRepresentativeItemTypes(string contentType) { - switch (contentType) + return contentType switch { - case CollectionType.BoxSets: - return new string[] { "BoxSet" }; - case CollectionType.Playlists: - return new string[] { "Playlist" }; - case CollectionType.Movies: - return new string[] { "Movie" }; - case CollectionType.TvShows: - return new string[] { "Series", "Season", "Episode" }; - case CollectionType.Books: - return new string[] { "Book" }; - case CollectionType.Music: - return new string[] { "MusicAlbum", "MusicArtist", "Audio", "MusicVideo" }; - case CollectionType.HomeVideos: - case CollectionType.Photos: - return new string[] { "Video", "Photo" }; - case CollectionType.MusicVideos: - return new string[] { "MusicVideo" }; - default: - return new string[] { "Series", "Season", "Episode", "Movie" }; - } + CollectionType.BoxSets => new[] {"BoxSet"}, + CollectionType.Playlists => new[] {"Playlist"}, + CollectionType.Movies => new[] {"Movie"}, + CollectionType.TvShows => new[] {"Series", "Season", "Episode"}, + CollectionType.Books => new[] {"Book"}, + CollectionType.Music => new[] {"MusicAlbum", "MusicArtist", "Audio", "MusicVideo"}, + CollectionType.HomeVideos => new[] {"Video", "Photo"}, + CollectionType.Photos => new[] {"Video", "Photo"}, + CollectionType.MusicVideos => new[] {"MusicVideo"}, + _ => new[] {"Series", "Season", "Episode", "Movie"} + }; } private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) @@ -397,54 +388,22 @@ namespace MediaBrowser.Api.Library { if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - return true; - } - else if (string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "The Open Movie Database", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase)) - { - return true; + return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); } - return false; + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .ToArray(); - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + return metadataOptions.Length == 0 + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -453,50 +412,17 @@ namespace MediaBrowser.Api.Library { if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - return true; - } - else if (string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "The Open Movie Database", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase)) - { - return true; + return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); } - return false; + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); } var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions @@ -561,8 +487,7 @@ namespace MediaBrowser.Api.Library foreach (var type in types) { - ImageOption[] defaultImageOptions = null; - TypeOptions.DefaultImageOptions.TryGetValue(type, out defaultImageOptions); + TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); typeOptions.Add(new LibraryTypeOptions { @@ -609,8 +534,6 @@ namespace MediaBrowser.Api.Library public object Get(GetSimilarItems request) { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - var item = string.IsNullOrEmpty(request.Id) ? (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); @@ -668,7 +591,7 @@ namespace MediaBrowser.Api.Library // ExcludeArtistIds if (!string.IsNullOrEmpty(request.ExcludeArtistIds)) { - query.ExcludeArtistIds = BaseApiService.GetGuids(request.ExcludeArtistIds); + query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds); } List itemsResult; @@ -689,7 +612,6 @@ namespace MediaBrowser.Api.Library var result = new QueryResult { Items = returnList, - TotalRecordCount = itemsResult.Count }; @@ -919,12 +841,10 @@ namespace MediaBrowser.Api.Library private BaseItem TranslateParentItem(BaseItem item, User user) { - if (item.GetParent() is AggregateFolder) - { - return _libraryManager.GetUserRootFolder().GetChildren(user, true).FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)); - } - - return item; + return item.GetParent() is AggregateFolder + ? _libraryManager.GetUserRootFolder().GetChildren(user, true) + .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) + : item; } /// @@ -1086,7 +1006,7 @@ namespace MediaBrowser.Api.Library var item = string.IsNullOrEmpty(request.Id) ? (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() - : (Folder)_libraryManager.RootFolder) + : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); if (item == null) @@ -1094,18 +1014,13 @@ namespace MediaBrowser.Api.Library throw new ResourceNotFoundException("Item not found."); } - BaseItem[] themeItems = Array.Empty(); + IEnumerable themeItems; while (true) { - themeItems = item.GetThemeSongs().ToArray(); + themeItems = item.GetThemeSongs(); - if (themeItems.Length > 0) - { - break; - } - - if (!request.InheritFromParent) + if (themeItems.Any() || !request.InheritFromParent) { break; } @@ -1119,11 +1034,9 @@ namespace MediaBrowser.Api.Library } var dtoOptions = GetDtoOptions(_authContext, request); - - var dtos = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); - - var items = dtos.ToArray(); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); return new ThemeMediaResult { @@ -1140,9 +1053,7 @@ namespace MediaBrowser.Api.Library /// System.Object. public object Get(GetThemeVideos request) { - var result = GetThemeVideos(request); - - return ToOptimizedResult(result); + return ToOptimizedResult(GetThemeVideos(request)); } public ThemeMediaResult GetThemeVideos(GetThemeVideos request) @@ -1152,7 +1063,7 @@ namespace MediaBrowser.Api.Library var item = string.IsNullOrEmpty(request.Id) ? (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() - : (Folder)_libraryManager.RootFolder) + : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); if (item == null) @@ -1160,18 +1071,13 @@ namespace MediaBrowser.Api.Library throw new ResourceNotFoundException("Item not found."); } - BaseItem[] themeItems = Array.Empty(); + IEnumerable themeItems; while (true) { - themeItems = item.GetThemeVideos().ToArray(); + themeItems = item.GetThemeVideos(); - if (themeItems.Length > 0) - { - break; - } - - if (!request.InheritFromParent) + if (themeItems.Any() || !request.InheritFromParent) { break; } @@ -1186,10 +1092,9 @@ namespace MediaBrowser.Api.Library var dtoOptions = GetDtoOptions(_authContext, request); - var dtos = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); - - var items = dtos.ToArray(); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); return new ThemeMediaResult { diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index c071b42f75..1e300814f6 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -327,15 +327,11 @@ namespace MediaBrowser.Api.Library try { - var mediaPath = request.PathInfo; - - if (mediaPath == null) + var mediaPath = request.PathInfo ?? new MediaPathInfo { - mediaPath = new MediaPathInfo - { - Path = request.Path - }; - } + Path = request.Path + }; + _libraryManager.AddMediaPath(request.Name, mediaPath); } finally diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 4b44961392..5fe4c0cca3 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -885,11 +885,10 @@ namespace MediaBrowser.Api.LiveTv { // SchedulesDirect requires a SHA1 hash of the user's password // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token - using (SHA1 sha = SHA1.Create()) - { - return Hex.Encode( - sha.ComputeHash(Encoding.UTF8.GetBytes(str))); - } + using SHA1 sha = SHA1.Create(); + + return Hex.Encode( + sha.ComputeHash(Encoding.UTF8.GetBytes(str))); } public void Delete(DeleteListingProvider request) @@ -1050,8 +1049,7 @@ namespace MediaBrowser.Api.LiveTv { query.IsSeries = true; - var series = _libraryManager.GetItemById(request.LibrarySeriesId) as Series; - if (series != null) + if (_libraryManager.GetItemById(request.LibrarySeriesId) is Series series) { query.Name = series.Name; } diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 889ebc9281..46da8b9099 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -394,7 +394,7 @@ namespace MediaBrowser.Api.Movies { var people = _libraryManager.GetPeople(new InternalPeopleQuery { - PersonTypes = new string[] + PersonTypes = new[] { PersonType.Director } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 5029ce0bb7..eb44cb4266 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -137,12 +137,9 @@ namespace MediaBrowser.Api.Playback var ext = outputFileExtension.ToLowerInvariant(); var folder = ServerConfigurationManager.GetTranscodePath(); - if (EnableOutputInSubFolder) - { - return Path.Combine(folder, filename, filename + ext); - } - - return Path.Combine(folder, filename + ext); + return EnableOutputInSubFolder + ? Path.Combine(folder, filename, filename + ext) + : Path.Combine(folder, filename + ext); } protected virtual string GetDefaultEncoderPreset() @@ -248,14 +245,8 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - logFilePrefix = "ffmpeg-remux"; - } - else - { - logFilePrefix = "ffmpeg-directstream"; - } + logFilePrefix = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase) + ? "ffmpeg-remux" : "ffmpeg-directstream"; } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); @@ -389,195 +380,181 @@ namespace MediaBrowser.Api.Playback continue; } - if (i == 0) + switch (i) { - request.DeviceProfileId = val; - } - else if (i == 1) - { - request.DeviceId = val; - } - else if (i == 2) - { - request.MediaSourceId = val; - } - else if (i == 3) - { - request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - else if (i == 4) - { - if (videoRequest != null) - { - videoRequest.VideoCodec = val; - } - } - else if (i == 5) - { - request.AudioCodec = val; - } - else if (i == 6) - { - if (videoRequest != null) - { - videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 7) - { - if (videoRequest != null) - { - videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 8) - { - if (videoRequest != null) - { - videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 9) - { - request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 10) - { - request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 11) - { - if (videoRequest != null) - { - videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 12) - { - if (videoRequest != null) - { - videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 13) - { - if (videoRequest != null) - { - videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 14) - { - request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 15) - { - if (videoRequest != null) - { - videoRequest.Level = val; - } - } - else if (i == 16) - { - if (videoRequest != null) - { - videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 17) - { - if (videoRequest != null) - { - videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 18) - { - if (videoRequest != null) - { - videoRequest.Profile = val; - } - } - else if (i == 19) - { - // cabac no longer used - } - else if (i == 20) - { - request.PlaySessionId = val; - } - else if (i == 21) - { - // api_key - } - else if (i == 22) - { - request.LiveStreamId = val; - } - else if (i == 23) - { - // Duplicating ItemId because of MediaMonkey - } - else if (i == 24) - { - if (videoRequest != null) - { - videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 25) - { - if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) - { - if (Enum.TryParse(val, out SubtitleDeliveryMethod method)) + case 0: + request.DeviceProfileId = val; + break; + case 1: + request.DeviceId = val; + break; + case 2: + request.MediaSourceId = val; + break; + case 3: + request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + break; + case 4: + if (videoRequest != null) { - videoRequest.SubtitleMethod = method; + videoRequest.VideoCodec = val; } - } - } - else if (i == 26) - { - request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 27) - { - if (videoRequest != null) - { - videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 28) - { - request.Tag = val; - } - else if (i == 29) - { - if (videoRequest != null) - { - videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 30) - { - request.SubtitleCodec = val; - } - else if (i == 31) - { - if (videoRequest != null) - { - videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 32) - { - if (videoRequest != null) - { - videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 33) - { - request.TranscodeReasons = val; + + break; + case 5: + request.AudioCodec = val; + break; + case 6: + if (videoRequest != null) + { + videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 7: + if (videoRequest != null) + { + videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 8: + if (videoRequest != null) + { + videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 9: + request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 10: + request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 11: + if (videoRequest != null) + { + videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 12: + if (videoRequest != null) + { + videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 13: + if (videoRequest != null) + { + videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 14: + request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); + break; + case 15: + if (videoRequest != null) + { + videoRequest.Level = val; + } + + break; + case 16: + if (videoRequest != null) + { + videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 17: + if (videoRequest != null) + { + videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 18: + if (videoRequest != null) + { + videoRequest.Profile = val; + } + + break; + case 19: + // cabac no longer used + break; + case 20: + request.PlaySessionId = val; + break; + case 21: + // api_key + break; + case 22: + request.LiveStreamId = val; + break; + case 23: + // Duplicating ItemId because of MediaMonkey + break; + case 24: + if (videoRequest != null) + { + videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 25: + if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) + { + if (Enum.TryParse(val, out SubtitleDeliveryMethod method)) + { + videoRequest.SubtitleMethod = method; + } + } + + break; + case 26: + request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 27: + if (videoRequest != null) + { + videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 28: + request.Tag = val; + break; + case 29: + if (videoRequest != null) + { + videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 30: + request.SubtitleCodec = val; + break; + case 31: + if (videoRequest != null) + { + videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 32: + if (videoRequest != null) + { + videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 33: + request.TranscodeReasons = val; + break; } } } @@ -630,14 +607,9 @@ namespace MediaBrowser.Api.Playback throw new ArgumentException("Invalid timeseek header"); } int index = value.IndexOf('-'); - if (index == -1) - { - value = value.Substring(Npt.Length); - } - else - { - value = value.Substring(Npt.Length, index - Npt.Length); - } + value = index == -1 + ? value.Substring(Npt.Length) + : value.Substring(Npt.Length, index - Npt.Length); if (value.IndexOf(':') == -1) { @@ -856,21 +828,11 @@ namespace MediaBrowser.Api.Playback { state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId); } - else + else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) { - if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) - { - var caps = DeviceManager.GetCapabilities(state.Request.DeviceId); + var caps = DeviceManager.GetCapabilities(state.Request.DeviceId); - if (caps != null) - { - state.DeviceProfile = caps.DeviceProfile; - } - else - { - state.DeviceProfile = DlnaManager.GetProfile(headers); - } - } + state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile; } var profile = state.DeviceProfile; diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 0cbfe4bdfa..52962366c6 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -140,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Hls if (isLive) { - job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); + job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); if (job != null) { @@ -156,7 +156,7 @@ namespace MediaBrowser.Api.Playback.Hls var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate); - job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); + job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); if (job != null) { @@ -168,22 +168,19 @@ namespace MediaBrowser.Api.Playback.Hls private string GetLivePlaylistText(string path, int segmentLength) { - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - using (var reader = new StreamReader(stream)) - { - var text = reader.ReadToEnd(); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var reader = new StreamReader(stream); - text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT"); + var text = reader.ReadToEnd(); - var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); + text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT"); - text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); - //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); - return text; - } - } + text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + + return text; } private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate) @@ -212,29 +209,25 @@ namespace MediaBrowser.Api.Playback.Hls try { // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (var fileStream = GetPlaylistFileStream(playlist)) + using var fileStream = GetPlaylistFileStream(playlist); + using var reader = new StreamReader(fileStream); + var count = 0; + + while (!reader.EndOfStream) { - using (var reader = new StreamReader(fileStream)) + var line = reader.ReadLine(); + + if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) { - var count = 0; - - while (!reader.EndOfStream) + count++; + if (count >= segmentCount) { - var line = reader.ReadLine(); - - if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) - { - count++; - if (count >= segmentCount) - { - Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); - return; - } - } + Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); + return; } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } catch (IOException) { @@ -247,17 +240,13 @@ namespace MediaBrowser.Api.Playback.Hls protected Stream GetPlaylistFileStream(string path) { - var tmpPath = path + ".tmp"; - tmpPath = path; - - try - { - return new FileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan); - } - catch (IOException) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan); - } + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + IODefaults.FileStreamBufferSize, + FileOptions.SequentialScan); } protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 3348a31875..20e18cc265 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -284,7 +284,7 @@ namespace MediaBrowser.Api.Playback.Hls //} Logger.LogDebug("returning {0} [general case]", segmentPath); - job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -438,8 +438,7 @@ namespace MediaBrowser.Api.Playback.Hls { var segmentId = "0"; - var segmentRequest = request as GetHlsVideoSegment; - if (segmentRequest != null) + if (request is GetHlsVideoSegment segmentRequest) { segmentId = segmentRequest.SegmentId; } @@ -690,8 +689,7 @@ namespace MediaBrowser.Api.Playback.Hls return false; } - var request = state.Request as IMasterHlsRequest; - if (request != null && !request.EnableAdaptiveBitrateStreaming) + if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming) { return false; } @@ -936,7 +934,7 @@ namespace MediaBrowser.Api.Playback.Hls var framerate = state.VideoStream?.RealFrameRate; - if (framerate != null && framerate.HasValue) + if (framerate.HasValue) { // This is to make sure keyframe interval is limited to our segment, // as forcing keyframes is not enough. diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index d74ec3ca63..3638feaa33 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -234,7 +234,7 @@ namespace MediaBrowser.Api.Playback OpenToken = mediaSource.OpenToken }).ConfigureAwait(false); - info.MediaSources = new MediaSourceInfo[] { openStreamResult.MediaSource }; + info.MediaSources = new[] { openStreamResult.MediaSource }; } } @@ -289,7 +289,7 @@ namespace MediaBrowser.Api.Playback { var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); - mediaSources = new MediaSourceInfo[] { mediaSource }; + mediaSources = new[] { mediaSource }; } if (mediaSources.Length == 0) @@ -366,7 +366,7 @@ namespace MediaBrowser.Api.Playback var options = new VideoOptions { - MediaSources = new MediaSourceInfo[] { mediaSource }, + MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, DeviceId = auth.DeviceId, ItemId = item.Id, @@ -572,8 +572,7 @@ namespace MediaBrowser.Api.Playback { attachment.DeliveryUrl = string.Format( CultureInfo.InvariantCulture, - "{0}/Videos/{1}/{2}/Attachments/{3}", - ServerConfigurationManager.Configuration.BaseUrl, + "/Videos/{0}/{1}/Attachments/{2}", item.Id, mediaSource.Id, attachment.Index); @@ -583,7 +582,7 @@ namespace MediaBrowser.Api.Playback private long? GetMaxBitrate(long? clientMaxBitrate, User user) { var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user == null ? 0 : user.Policy.RemoteClientBitrateLimit; + var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0; if (remoteClientMaxBitrate <= 0) { @@ -662,17 +661,9 @@ namespace MediaBrowser.Api.Playback }; }).ThenBy(i => { - if (maxBitrate.HasValue) + if (maxBitrate.HasValue && i.Bitrate.HasValue) { - if (i.Bitrate.HasValue) - { - if (i.Bitrate.Value <= maxBitrate.Value) - { - return 0; - } - - return 2; - } + return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; } return 1; diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index cbf981dfec..cebd4b49a1 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -167,7 +167,7 @@ namespace MediaBrowser.Api.Playback AudioCodec = request.AudioCodec, Protocol = request.TranscodingProtocol, BreakOnNonKeyFrames = request.BreakOnNonKeyFrames, - MaxAudioChannels = request.TranscodingAudioChannels.HasValue ? request.TranscodingAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : null + MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } }; @@ -300,7 +300,7 @@ namespace MediaBrowser.Api.Playback // hls segment container can only be mpegts or fmp4 per ffmpeg documentation // TODO: remove this when we switch back to the segment muxer - var supportedHLSContainers = new string[] { "mpegts", "fmp4" }; + var supportedHLSContainers = new[] { "mpegts", "fmp4" }; var newRequest = new GetMasterHlsAudioPlaylist { diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs index 16d3268b9b..7f74511eec 100644 --- a/MediaBrowser.Api/PluginService.cs +++ b/MediaBrowser.Api/PluginService.cs @@ -243,9 +243,7 @@ namespace MediaBrowser.Api // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs var id = Guid.Parse(GetPathValue(1)); - var plugin = _appHost.Plugins.First(p => p.Id == id) as IHasPluginConfiguration; - - if (plugin == null) + if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin)) { throw new FileNotFoundException(); } diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs index 2bd387229a..e08a8482e0 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs @@ -123,9 +123,7 @@ namespace MediaBrowser.Api.ScheduledTasks { var isHidden = false; - var configurableTask = i.ScheduledTask as IConfigurableScheduledTask; - - if (configurableTask != null) + if (i.ScheduledTask is IConfigurableScheduledTask configurableTask) { isHidden = configurableTask.IsHidden; } @@ -142,9 +140,7 @@ namespace MediaBrowser.Api.ScheduledTasks { var isEnabled = true; - var configurableTask = i.ScheduledTask as IConfigurableScheduledTask; - - if (configurableTask != null) + if (i.ScheduledTask is IConfigurableScheduledTask configurableTask) { isEnabled = configurableTask.IsEnabled; } diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index 0a3dc19dc6..e9d339c6e3 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -234,59 +234,48 @@ namespace MediaBrowser.Api SetThumbImageInfo(result, item); SetBackdropImageInfo(result, item); - var program = item as LiveTvProgram; - if (program != null) + switch (item) { - result.StartDate = program.StartDate; - } + case IHasSeries hasSeries: + result.Series = hasSeries.SeriesName; + break; + case LiveTvProgram program: + result.StartDate = program.StartDate; + break; + case Series series: + if (series.Status.HasValue) + { + result.Status = series.Status.Value.ToString(); + } - var hasSeries = item as IHasSeries; - if (hasSeries != null) - { - result.Series = hasSeries.SeriesName; - } + break; + case MusicAlbum album: + result.Artists = album.Artists; + result.AlbumArtist = album.AlbumArtist; + break; + case Audio song: + result.AlbumArtist = song.AlbumArtists.FirstOrDefault(); + result.Artists = song.Artists; - var series = item as Series; - if (series != null) - { - if (series.Status.HasValue) - { - result.Status = series.Status.Value.ToString(); - } - } + MusicAlbum musicAlbum = song.AlbumEntity; - var album = item as MusicAlbum; + if (musicAlbum != null) + { + result.Album = musicAlbum.Name; + result.AlbumId = musicAlbum.Id; + } + else + { + result.Album = song.Album; + } - if (album != null) - { - result.Artists = album.Artists; - result.AlbumArtist = album.AlbumArtist; - } - - var song = item as Audio; - - if (song != null) - { - result.AlbumArtist = song.AlbumArtists.FirstOrDefault(); - result.Artists = song.Artists; - - album = song.AlbumEntity; - - if (album != null) - { - result.Album = album.Name; - result.AlbumId = album.Id; - } - else - { - result.Album = song.Album; - } + break; } if (!item.ChannelId.Equals(Guid.Empty)) { var channel = _libraryManager.GetItemById(item.ChannelId); - result.ChannelName = channel == null ? null : channel.Name; + result.ChannelName = channel?.Name; } return result; @@ -296,12 +285,9 @@ namespace MediaBrowser.Api { var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null; - if (itemWithImage == null) + if (itemWithImage == null && item is Episode) { - if (item is Episode) - { - itemWithImage = GetParentWithImage(item, ImageType.Thumb); - } + itemWithImage = GetParentWithImage(item, ImageType.Thumb); } if (itemWithImage == null) @@ -323,12 +309,8 @@ namespace MediaBrowser.Api private void SetBackdropImageInfo(SearchHint hint, BaseItem item) { - var itemWithImage = item.HasImage(ImageType.Backdrop) ? item : null; - - if (itemWithImage == null) - { - itemWithImage = GetParentWithImage(item, ImageType.Backdrop); - } + var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null) + ?? GetParentWithImage(item, ImageType.Backdrop); if (itemWithImage != null) { diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index c4a7ae78e7..f2968c6b5c 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -230,17 +230,14 @@ namespace MediaBrowser.Api.Subtitles if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap) { - using (var stream = await GetSubtitles(request).ConfigureAwait(false)) - { - using (var reader = new StreamReader(stream)) - { - var text = reader.ReadToEnd(); + using var stream = await GetSubtitles(request).ConfigureAwait(false); + using var reader = new StreamReader(stream); - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); + var text = reader.ReadToEnd(); - return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format)); - } - } + text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); + + return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format)); } return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format)); diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs index 3a3eeb8b8f..c57cc93d55 100644 --- a/MediaBrowser.Api/System/SystemService.cs +++ b/MediaBrowser.Api/System/SystemService.cs @@ -168,12 +168,9 @@ namespace MediaBrowser.Api.System .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase)); // For older files, assume fully static - if (file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1)) - { - return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.Read); - } + var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; - return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite); + return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare); } /// diff --git a/MediaBrowser.Api/TranscodingJob.cs b/MediaBrowser.Api/TranscodingJob.cs index 6d944d19ea..8c24e3ce18 100644 --- a/MediaBrowser.Api/TranscodingJob.cs +++ b/MediaBrowser.Api/TranscodingJob.cs @@ -92,10 +92,7 @@ namespace MediaBrowser.Api { lock (_timerLock) { - if (KillTimer != null) - { - KillTimer.Change(Timeout.Infinite, Timeout.Infinite); - } + KillTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 334d1db512..cd8e8dfbe4 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -424,9 +424,7 @@ namespace MediaBrowser.Api if (!string.IsNullOrWhiteSpace(request.SeasonId)) { - var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season; - - if (season == null) + if (!(_libraryManager.GetItemById(new Guid(request.SeasonId)) is Season season)) { throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId); } @@ -444,14 +442,7 @@ namespace MediaBrowser.Api var season = series.GetSeasons(user, dtoOptions).FirstOrDefault(i => i.IndexNumber == request.Season.Value); - if (season == null) - { - episodes = new List(); - } - else - { - episodes = ((Season)season).GetEpisodes(user, dtoOptions); - } + episodes = season == null ? new List() : ((Season)season).GetEpisodes(user, dtoOptions); } else { diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index adb0a440f6..3d08d5437c 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -126,12 +126,7 @@ namespace MediaBrowser.Api.UserLibrary protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) { - if (request is GetAlbumArtists) - { - return LibraryManager.GetAlbumArtists(query); - } - - return LibraryManager.GetArtists(query); + return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query); } /// diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 9fa222d324..c4a52d5f52 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -82,8 +82,7 @@ namespace MediaBrowser.Api.UserLibrary { var parent = GetParentItem(request); - var collectionFolder = parent as IHasCollectionType; - if (collectionFolder != null) + if (parent is IHasCollectionType collectionFolder) { return collectionFolder.CollectionType; } @@ -274,7 +273,7 @@ namespace MediaBrowser.Api.UserLibrary DtoOptions = dtoOptions }; - Func filter = i => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes); + bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes); if (parentItem.IsFolder) { @@ -284,18 +283,18 @@ namespace MediaBrowser.Api.UserLibrary { items = request.Recursive ? folder.GetRecursiveChildren(user, query).ToList() : - folder.GetChildren(user, true).Where(filter).ToList(); + folder.GetChildren(user, true).Where(Filter).ToList(); } else { items = request.Recursive ? - folder.GetRecursiveChildren(filter) : - folder.Children.Where(filter).ToList(); + folder.GetRecursiveChildren(Filter) : + folder.Children.Where(Filter).ToList(); } } else { - items = new[] { parentItem }.Where(filter).ToList(); + items = new[] { parentItem }.Where(Filter).ToList(); } var extractedItems = GetAllItems(request, items); @@ -346,30 +345,21 @@ namespace MediaBrowser.Api.UserLibrary private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes) { // Exclude item types - if (excludeItemTypes.Length > 0) + if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) { - if (excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } // Include item types - if (includeItemTypes.Length > 0) + if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) { - if (!includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } // Include MediaTypes - if (mediaTypes.Length > 0) + if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - if (!mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } return true; diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index a26f59573c..7561b5c892 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -396,12 +396,10 @@ namespace MediaBrowser.Api.UserLibrary public VideoType[] GetVideoTypes() { - if (string.IsNullOrEmpty(VideoTypes)) - { - return Array.Empty(); - } - - return VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray(); + return string.IsNullOrEmpty(VideoTypes) + ? Array.Empty() + : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse(v, true)).ToArray(); } /// @@ -412,12 +410,10 @@ namespace MediaBrowser.Api.UserLibrary { var val = Filters; - if (string.IsNullOrEmpty(val)) - { - return new ItemFilter[] { }; - } - - return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)).ToArray(); + return string.IsNullOrEmpty(val) + ? Array.Empty() + : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse(v, true)).ToArray(); } /// @@ -428,12 +424,9 @@ namespace MediaBrowser.Api.UserLibrary { var val = ImageTypes; - if (string.IsNullOrEmpty(val)) - { - return new ImageType[] { }; - } - - return val.Split(',').Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToArray(); + return string.IsNullOrEmpty(val) + ? Array.Empty() + : val.Split(',').Select(v => Enum.Parse(v, true)).ToArray(); } /// @@ -469,7 +462,9 @@ namespace MediaBrowser.Api.UserLibrary var sortOrderIndex = sortOrders.Length > i ? i : 0; var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null; - var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) ? MediaBrowser.Model.Entities.SortOrder.Descending : MediaBrowser.Model.Entities.SortOrder.Ascending; + var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) + ? MediaBrowser.Model.Entities.SortOrder.Descending + : MediaBrowser.Model.Entities.SortOrder.Ascending; result[i] = new ValueTuple(vals[i], sortOrder); } diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 22e4eea0b8..ac59c30301 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -199,14 +199,12 @@ namespace MediaBrowser.Api.UserLibrary item = _libraryManager.GetUserRootFolder(); } - Folder folder = item as Folder; - if (folder == null) + if (!(item is Folder folder)) { folder = _libraryManager.GetUserRootFolder(); } - var hasCollectionType = folder as IHasCollectionType; - if (hasCollectionType != null + if (folder is IHasCollectionType hasCollectionType && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { request.Recursive = true; diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index 46b6d5a947..b11fd48d3c 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/MediaBrowser.Api/VideosService.cs @@ -139,17 +139,11 @@ namespace MediaBrowser.Api .ToList(); var primaryVersion = videosWithVersions.FirstOrDefault(); - if (primaryVersion == null) { primaryVersion = items.OrderBy(i => { - if (i.Video3DFormat.HasValue) - { - return 1; - } - - if (i.VideoType != Model.Entities.VideoType.VideoFile) + if (i.Video3DFormat.HasValue || i.VideoType != Model.Entities.VideoType.VideoFile) { return 1; } @@ -158,10 +152,7 @@ namespace MediaBrowser.Api }) .ThenByDescending(i => { - var stream = i.GetDefaultVideoStream(); - - return stream == null || stream.Width == null ? 0 : stream.Width.Value; - + return i.GetDefaultVideoStream()?.Width ?? 0; }).First(); } diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs new file mode 100644 index 0000000000..c747871222 --- /dev/null +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Extensions +{ + /// + /// Extension methods for . + /// + public static class ProcessExtensions + { + /// + /// Asynchronously wait for the process to exit. + /// + /// The process to wait for. + /// The duration to wait before cancelling waiting for the task. + /// True if the task exited normally, false if the timeout elapsed before the process exited. + /// If is not set to true for the process. + public static async Task WaitForExitAsync(this Process process, TimeSpan timeout) + { + using (var cancelTokenSource = new CancellationTokenSource(timeout)) + { + return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false); + } + } + + /// + /// Asynchronously wait for the process to exit. + /// + /// The process to wait for. + /// A to observe while waiting for the process to exit. + /// True if the task exited normally, false if cancelled before the process exited. + public static async Task WaitForExitAsync(this Process process, CancellationToken cancelToken) + { + if (!process.EnableRaisingEvents) + { + throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit."); + } + + // Add an event handler for the process exit event + var tcs = new TaskCompletionSource(); + process.Exited += (sender, args) => tcs.TrySetResult(true); + + // Return immediately if the process has already exited + if (process.HasExitedSafe()) + { + return true; + } + + // Register with the cancellation token then await + using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe()))) + { + return await tcs.Task.ConfigureAwait(false); + } + } + + /// + /// Gets a value indicating whether the associated process has been terminated using + /// . This is safe to call even if there is no operating system process + /// associated with the . + /// + /// The process to check the exit status for. + /// + /// True if the operating system process referenced by the component has + /// terminated, or if there is no associated operating system process; otherwise, false. + /// + private static bool HasExitedSafe(this Process process) + { + try + { + return process.HasExited; + } + catch (InvalidOperationException) + { + return true; + } + } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 6e5fe97e5d..3552a45a23 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -78,8 +78,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType) - && CheckVaapi(state, hwType, encodingOptions)) + && codecMap.ContainsKey(hwType)) { var preferredEncoder = codecMap[hwType]; @@ -93,23 +92,6 @@ namespace MediaBrowser.Controller.MediaEncoding return defaultEncoder; } - private bool CheckVaapi(EncodingJobInfo state, string hwType, EncodingOptions encodingOptions) - { - if (!string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase)) - { - // No vaapi requested, return OK. - return true; - } - - if (string.IsNullOrEmpty(encodingOptions.VaapiDevice)) - { - // No device specified, return OK. - return true; - } - - return IsVaapiSupported(state); - } - private bool IsVaapiSupported(EncodingJobInfo state) { var videoStream = state.VideoStream; @@ -424,7 +406,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { - return "aac -strict experimental"; + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support + if (_mediaEncoder.SupportsEncoder("libfdk_aac")) + { + return "libfdk_aac"; + } + + return "aac"; } if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) @@ -1605,7 +1593,7 @@ namespace MediaBrowser.Controller.MediaEncoding // For VAAPI and CUVID decoder // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video, // thus needs to be manually adjusted. - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + if ((IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) || (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) { var videoStream = state.VideoStream; @@ -1648,7 +1636,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first - else if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) { /* @@ -2014,19 +2002,29 @@ namespace MediaBrowser.Controller.MediaEncoding } // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first - else if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) { var codec = videoStream.Codec.ToLowerInvariant(); - var pixelFormat = videoStream.PixelFormat.ToLowerInvariant(); + var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)); - // Assert hardware VAAPI decodable (Except h264 10-bit and higher color depth) - // TODO: a propery way to detect hardware capabilities and falling back when transcoding is failed - if ((pixelFormat ?? string.Empty).IndexOf("p10", StringComparison.OrdinalIgnoreCase) == -1 - || ((pixelFormat ?? string.Empty).IndexOf("p10", StringComparison.OrdinalIgnoreCase) != -1 - && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)))) + // Assert 10-bit hardware VAAPI decodable + if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + /* + Download data from GPU to CPU as p010le format. + Colorspace conversion is unnecessary here as libx264 will handle it. + If this step is missing, it will fail on AMD but not on intel. + */ + filters.Add("hwdownload"); + filters.Add("format=p010le"); + } + + // Assert 8-bit hardware VAAPI decodable + else if (!isColorDepth10) { filters.Add("hwdownload"); filters.Add("format=nv12"); diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 43d33c716f..3f177a9fa8 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -155,48 +155,45 @@ namespace MediaBrowser.MediaEncoding.Attachments inputPath, attachmentStreamIndex, outputPath); - var startInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }; - var process = new Process - { - StartInfo = startInfo - }; - _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + int exitCode; - process.Start(); - - var processTcs = new TaskCompletionSource(); - process.EnableRaisingEvents = true; - process.Exited += (sender, args) => processTcs.TrySetResult(true); - var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited)); - var ranToCompletion = await processTcs.Task.ConfigureAwait(false); - unregister.Dispose(); - - if (!ranToCompletion) - { - try + using (var process = new Process { - _logger.LogWarning("Killing ffmpeg attachment extraction process"); - process.Kill(); - } - catch (Exception ex) + StartInfo = new ProcessStartInfo + { + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + + process.Start(); + + var ranToCompletion = await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + + if (!ranToCompletion) { - _logger.LogError(ex, "Error killing attachment extraction process"); + try + { + _logger.LogWarning("Killing ffmpeg attachment extraction process"); + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing attachment extraction process"); + } } + + exitCode = ranToCompletion ? process.ExitCode : -1; } - var exitCode = ranToCompletion ? process.ExitCode : -1; - - process.Dispose(); - var failed = false; if (exitCode != 0) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f5decdc0db..6e036d24c1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -42,6 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "libvpx", "libvpx-vp9", "aac", + "libfdk_aac", "libmp3lame", "libopus", "libvorbis", diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index c5bba87802..f4260afbca 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -22,6 +21,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace MediaBrowser.MediaEncoding.Encoder { @@ -38,7 +38,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly ILogger _logger; private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; - private readonly IProcessFactory _processFactory; private readonly ILocalizationManager _localization; private readonly Lazy _encodingHelperFactory; private readonly string _startupOptionFFmpegPath; @@ -55,7 +54,6 @@ namespace MediaBrowser.MediaEncoding.Encoder ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - IProcessFactory processFactory, ILocalizationManager localization, Lazy encodingHelperFactory, string startupOptionsFFmpegPath) @@ -63,7 +61,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; - _processFactory = processFactory; _localization = localization; _encodingHelperFactory = encodingHelperFactory; _startupOptionFFmpegPath = startupOptionsFFmpegPath; @@ -354,30 +351,33 @@ namespace MediaBrowser.MediaEncoding.Encoder : "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format"; args = string.Format(args, probeSizeArgument, inputPath).Trim(); - var process = _processFactory.Create(new ProcessOptions + var process = new Process { - CreateNoWindow = true, - UseShellExecute = false, + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, - // Must consume both or ffmpeg may hang due to deadlocks. See comments below. - RedirectStandardOutput = true, + // Must consume both or ffmpeg may hang due to deadlocks. See comments below. + RedirectStandardOutput = true, - FileName = _ffprobePath, - Arguments = args, + FileName = _ffprobePath, + Arguments = args, - IsHidden = true, - ErrorDialog = false, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, EnableRaisingEvents = true - }); + }; if (forceEnableLogging) { - _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogInformation("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments); } else { - _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments); } using (var processWrapper = new ProcessWrapper(process, this)) @@ -563,18 +563,21 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - var process = _processFactory.Create(new ProcessOptions + var process = new Process { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _ffmpegPath, - Arguments = args, - IsHidden = true, - ErrorDialog = false, + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _ffmpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, EnableRaisingEvents = true - }); + }; - _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Arguments); using (var processWrapper = new ProcessWrapper(process, this)) { @@ -591,7 +594,7 @@ namespace MediaBrowser.MediaEncoding.Encoder timeoutMs = DefaultImageExtractionTimeout; } - ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); + ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); if (!ranToCompletion) { @@ -692,23 +695,27 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - var process = _processFactory.Create(new ProcessOptions + var processStartInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, FileName = _ffmpegPath, Arguments = args, - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true - }); + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; - _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); + _logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments); await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); bool ranToCompletion = false; + var process = new Process + { + StartInfo = processStartInfo, + EnableRaisingEvents = true + }; using (var processWrapper = new ProcessWrapper(process, this)) { try @@ -724,7 +731,7 @@ namespace MediaBrowser.MediaEncoding.Encoder while (isResponsive) { - if (await process.WaitForExitAsync(30000).ConfigureAwait(false)) + if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) { ranToCompletion = true; break; @@ -941,14 +948,14 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _disposed = false; - public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder) + public ProcessWrapper(Process process, MediaEncoder mediaEncoder) { Process = process; _mediaEncoder = mediaEncoder; Process.Exited += OnProcessExited; } - public IProcess Process { get; } + public Process Process { get; } public bool HasExited { get; private set; } @@ -956,7 +963,7 @@ namespace MediaBrowser.MediaEncoding.Encoder void OnProcessExited(object sender, EventArgs e) { - var process = (IProcess)sender; + var process = (Process)sender; HasExited = true; @@ -971,7 +978,7 @@ namespace MediaBrowser.MediaEncoding.Encoder DisposeProcess(process); } - private void DisposeProcess(IProcess process) + private void DisposeProcess(Process process) { lock (_mediaEncoder._runningProcessesLock) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index b76b52941c..ba171295ea 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -12,7 +13,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -31,7 +31,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IMediaEncoder _mediaEncoder; private readonly IHttpClient _httpClient; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IProcessFactory _processFactory; public SubtitleEncoder( ILibraryManager libraryManager, @@ -40,8 +39,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles IFileSystem fileSystem, IMediaEncoder mediaEncoder, IHttpClient httpClient, - IMediaSourceManager mediaSourceManager, - IProcessFactory processFactory) + IMediaSourceManager mediaSourceManager) { _libraryManager = libraryManager; _logger = logger; @@ -50,7 +48,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles _mediaEncoder = mediaEncoder; _httpClient = httpClient; _mediaSourceManager = mediaSourceManager; - _processFactory = processFactory; } private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); @@ -429,50 +426,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles encodingParam = " -sub_charenc " + encodingParam; } - var process = _processFactory.Create(new ProcessOptions + int exitCode; + + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _mediaEncoder.EncoderPath, - Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), - EnableRaisingEvents = true, - IsHidden = true, - ErrorDialog = false - }); + _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - - try - { - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ffmpeg"); - - throw; - } - - var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false); - - if (!ranToCompletion) - { try { - _logger.LogInformation("Killing ffmpeg subtitle conversion process"); - - process.Kill(); + process.Start(); } catch (Exception ex) { - _logger.LogError(ex, "Error killing subtitle conversion process"); + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; } + + var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false); + + if (!ranToCompletion) + { + try + { + _logger.LogInformation("Killing ffmpeg subtitle conversion process"); + + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing subtitle conversion process"); + } + } + + exitCode = ranToCompletion ? process.ExitCode : -1; } - var exitCode = ranToCompletion ? process.ExitCode : -1; - - process.Dispose(); - var failed = false; if (exitCode == -1) @@ -578,50 +579,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputCodec, outputPath); - var process = _processFactory.Create(new ProcessOptions + int exitCode; + + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = processArgs, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { - CreateNoWindow = true, - UseShellExecute = false, - EnableRaisingEvents = true, - FileName = _mediaEncoder.EncoderPath, - Arguments = processArgs, - IsHidden = true, - ErrorDialog = false - }); + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - - try - { - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ffmpeg"); - - throw; - } - - var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false); - - if (!ranToCompletion) - { try { - _logger.LogWarning("Killing ffmpeg subtitle extraction process"); - - process.Kill(); + process.Start(); } catch (Exception ex) { - _logger.LogError(ex, "Error killing subtitle extraction process"); + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; } + + var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false); + + if (!ranToCompletion) + { + try + { + _logger.LogWarning("Killing ffmpeg subtitle extraction process"); + + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing subtitle extraction process"); + } + } + + exitCode = ranToCompletion ? process.ExitCode : -1; } - var exitCode = ranToCompletion ? process.ExitCode : -1; - - process.Dispose(); - var failed = false; if (exitCode == -1) diff --git a/MediaBrowser.Model/Diagnostics/IProcess.cs b/MediaBrowser.Model/Diagnostics/IProcess.cs deleted file mode 100644 index 514d1e7379..0000000000 --- a/MediaBrowser.Model/Diagnostics/IProcess.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Model.Diagnostics -{ - public interface IProcess : IDisposable - { - event EventHandler Exited; - - void Kill(); - bool WaitForExit(int timeMs); - Task WaitForExitAsync(int timeMs); - int ExitCode { get; } - void Start(); - StreamWriter StandardInput { get; } - StreamReader StandardError { get; } - StreamReader StandardOutput { get; } - ProcessOptions StartInfo { get; } - } -} diff --git a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs b/MediaBrowser.Model/Diagnostics/IProcessFactory.cs deleted file mode 100644 index 57082acc57..0000000000 --- a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Diagnostics -{ - public interface IProcessFactory - { - IProcess Create(ProcessOptions options); - } - - public class ProcessOptions - { - public string FileName { get; set; } - public string Arguments { get; set; } - public string WorkingDirectory { get; set; } - public bool CreateNoWindow { get; set; } - public bool UseShellExecute { get; set; } - public bool EnableRaisingEvents { get; set; } - public bool ErrorDialog { get; set; } - public bool RedirectStandardError { get; set; } - public bool RedirectStandardInput { get; set; } - public bool RedirectStandardOutput { get; set; } - public bool IsHidden { get; set; } - } -} diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000000..326331f322 --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 1646237a0e..a64d173496 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -7,6 +7,8 @@ namespace Jellyfin.Naming.Tests.Video { public class ExtraTests : BaseVideoTest { + private readonly NamingOptions _videoOptions = new NamingOptions(); + // Requirements // movie-deleted = ExtraType deletedscene @@ -15,42 +17,64 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestKodiExtras() { - var videoOptions = new NamingOptions(); + Test("trailer.mp4", ExtraType.Trailer, _videoOptions); + Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions); - Test("trailer.mp4", ExtraType.Trailer, videoOptions); - Test("300-trailer.mp4", ExtraType.Trailer, videoOptions); - - Test("theme.mp3", ExtraType.ThemeSong, videoOptions); + Test("theme.mp3", ExtraType.ThemeSong, _videoOptions); } [Fact] public void TestExpandedExtras() { - var videoOptions = new NamingOptions(); + Test("trailer.mp4", ExtraType.Trailer, _videoOptions); + Test("trailer.mp3", null, _videoOptions); + Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions); - Test("trailer.mp4", ExtraType.Trailer, videoOptions); - Test("trailer.mp3", null, videoOptions); - Test("300-trailer.mp4", ExtraType.Trailer, videoOptions); + Test("theme.mp3", ExtraType.ThemeSong, _videoOptions); + Test("theme.mkv", null, _videoOptions); - Test("theme.mp3", ExtraType.ThemeSong, videoOptions); - Test("theme.mkv", null, videoOptions); + Test("300-scene.mp4", ExtraType.Scene, _videoOptions); + Test("300-scene2.mp4", ExtraType.Scene, _videoOptions); + Test("300-clip.mp4", ExtraType.Clip, _videoOptions); - Test("300-scene.mp4", ExtraType.Scene, videoOptions); - Test("300-scene2.mp4", ExtraType.Scene, videoOptions); - Test("300-clip.mp4", ExtraType.Clip, videoOptions); + Test("300-deleted.mp4", ExtraType.DeletedScene, _videoOptions); + Test("300-deletedscene.mp4", ExtraType.DeletedScene, _videoOptions); + Test("300-interview.mp4", ExtraType.Interview, _videoOptions); + Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, _videoOptions); + } - Test("300-deleted.mp4", ExtraType.DeletedScene, videoOptions); - Test("300-deletedscene.mp4", ExtraType.DeletedScene, videoOptions); - Test("300-interview.mp4", ExtraType.Interview, videoOptions); - Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, videoOptions); + [Theory] + [InlineData(ExtraType.BehindTheScenes, "behind the scenes" )] + [InlineData(ExtraType.DeletedScene, "deleted scenes" )] + [InlineData(ExtraType.Interview, "interviews" )] + [InlineData(ExtraType.Scene, "scenes" )] + [InlineData(ExtraType.Sample, "samples" )] + [InlineData(ExtraType.Clip, "shorts" )] + [InlineData(ExtraType.Clip, "featurettes" )] + [InlineData(ExtraType.Unknown, "extras" )] + public void TestDirectories(ExtraType type, string dirName) + { + Test(dirName + "/300.mp4", type, _videoOptions); + Test("300/" + dirName + "/something.mkv", type, _videoOptions); + Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type, _videoOptions); + } + + [Theory] + [InlineData("gibberish")] + [InlineData("not a scene")] + [InlineData("The Big Short")] + public void TestNonExtraDirectories(string dirName) + { + Test(dirName + "/300.mp4", null, _videoOptions); + Test("300/" + dirName + "/something.mkv", null, _videoOptions); + Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null, _videoOptions); + Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null, _videoOptions); } [Fact] public void TestSample() { - var videoOptions = new NamingOptions(); - - Test("300-sample.mp4", ExtraType.Sample, videoOptions); + Test("300-sample.mp4", ExtraType.Sample, _videoOptions); } private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)