mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	Merge branch 'master' into register-services-correctly
This commit is contained in:
		
						commit
						9728aa8b0a
					
				@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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"]
 | 
			
		||||
 | 
			
		||||
@ -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<IProcessFactory, ProcessFactory>();
 | 
			
		||||
 | 
			
		||||
            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
 | 
			
		||||
 | 
			
		||||
            serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
 | 
			
		||||
@ -1462,15 +1458,17 @@ namespace Emby.Server.Implementations
 | 
			
		||||
                throw new NotSupportedException();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var process = Resolve<IProcessFactory>().Create(new ProcessOptions
 | 
			
		||||
            var process = new Process
 | 
			
		||||
            {
 | 
			
		||||
                StartInfo = new ProcessStartInfo
 | 
			
		||||
                {
 | 
			
		||||
                    FileName = url,
 | 
			
		||||
                EnableRaisingEvents = true,
 | 
			
		||||
                    UseShellExecute = true,
 | 
			
		||||
                    ErrorDialog = false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            process.Exited += ProcessExited;
 | 
			
		||||
                },
 | 
			
		||||
                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)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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<bool> 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<bool>();
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                {
 | 
			
		||||
                    StartInfo = new ProcessStartInfo
 | 
			
		||||
                    {
 | 
			
		||||
                        Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
 | 
			
		||||
                        CreateNoWindow = true,
 | 
			
		||||
                    EnableRaisingEvents = true,
 | 
			
		||||
                        ErrorDialog = false,
 | 
			
		||||
                        FileName = options.RecordingPostProcessor,
 | 
			
		||||
                    IsHidden = true,
 | 
			
		||||
                        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();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
 | 
			
		||||
        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,7 +291,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Processes the exited.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private void OnFfMpegProcessExited(IProcess process, string inputFile)
 | 
			
		||||
        private void OnFfMpegProcessExited(Process process, string inputFile)
 | 
			
		||||
        {
 | 
			
		||||
            using (process)
 | 
			
		||||
            {
 | 
			
		||||
                _hasExited = true;
 | 
			
		||||
 | 
			
		||||
@ -318,6 +319,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 | 
			
		||||
                                exitCode)));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void StartStreamingLog(Stream source, Stream target)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -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."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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": "تعمیر"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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}",
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -86,12 +86,9 @@ namespace MediaBrowser.Api
 | 
			
		||||
                return Array.Empty<string>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
        /// <returns>Task.</returns>
 | 
			
		||||
        internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> 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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -58,12 +58,9 @@ namespace MediaBrowser.Api
 | 
			
		||||
 | 
			
		||||
        public static string[] SplitValue(string value, char delim)
 | 
			
		||||
        {
 | 
			
		||||
            if (value == null)
 | 
			
		||||
            {
 | 
			
		||||
                return Array.Empty<string>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            return value == null
 | 
			
		||||
                ? Array.Empty<string>()
 | 
			
		||||
                : value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static Guid[] GetGuids(string value)
 | 
			
		||||
@ -97,21 +94,12 @@ 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 (!authenticatedUser.Policy.IsAdministrator)
 | 
			
		||||
            if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator)
 | 
			
		||||
                || (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess))
 | 
			
		||||
            {
 | 
			
		||||
                throw new SecurityException("Unauthorized access.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
            else if (restrictUserPreferences)
 | 
			
		||||
            {
 | 
			
		||||
                if (!authenticatedUser.Policy.EnableUserPreferenceAccess)
 | 
			
		||||
                {
 | 
			
		||||
                    throw new SecurityException("Unauthorized access.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the session.
 | 
			
		||||
@ -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<T>().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
 | 
			
		||||
 | 
			
		||||
            }).OfType<T>().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
 | 
			
		||||
 | 
			
		||||
            }).OfType<T>().FirstOrDefault();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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<ItemFilter>()
 | 
			
		||||
                : val.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -173,14 +170,9 @@ namespace MediaBrowser.Api
 | 
			
		||||
        /// <returns>IEnumerable{ItemFilter}.</returns>
 | 
			
		||||
        public IEnumerable<ItemFilter> 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<ItemFilter>()
 | 
			
		||||
                : Filters.Split(',').Select(v => Enum.Parse<ItemFilter>(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
 | 
			
		||||
 | 
			
		||||
@ -155,8 +155,7 @@ namespace MediaBrowser.Api.Devices
 | 
			
		||||
                    Id = id
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
            return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
 | 
			
		||||
            {
 | 
			
		||||
                MimeType = Request.ContentType,
 | 
			
		||||
@ -167,4 +166,3 @@ namespace MediaBrowser.Api.Devices
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<Guid>() : new Guid[] { parentItem.Id };
 | 
			
		||||
                genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : 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
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -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,8 +750,7 @@ namespace MediaBrowser.Api.Images
 | 
			
		||||
        /// <returns>Task.</returns>
 | 
			
		||||
        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);
 | 
			
		||||
@ -770,4 +769,3 @@ namespace MediaBrowser.Api.Images
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -261,28 +261,26 @@ namespace MediaBrowser.Api.Images
 | 
			
		||||
        /// <returns>Task.</returns>
 | 
			
		||||
        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)
 | 
			
		||||
                using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
 | 
			
		||||
            {
 | 
			
		||||
                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);
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the full cache path.
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<BaseItem> itemsResult;
 | 
			
		||||
@ -689,7 +612,6 @@ namespace MediaBrowser.Api.Library
 | 
			
		||||
            var result = new QueryResult<BaseItemDto>
 | 
			
		||||
            {
 | 
			
		||||
                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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -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<BaseItem>();
 | 
			
		||||
            IEnumerable<BaseItem> 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
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        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<BaseItem>();
 | 
			
		||||
            IEnumerable<BaseItem> 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
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -327,15 +327,11 @@ namespace MediaBrowser.Api.Library
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var mediaPath = request.PathInfo;
 | 
			
		||||
 | 
			
		||||
                if (mediaPath == null)
 | 
			
		||||
                {
 | 
			
		||||
                    mediaPath = new MediaPathInfo
 | 
			
		||||
                var mediaPath = request.PathInfo ?? new MediaPathInfo
 | 
			
		||||
                {
 | 
			
		||||
                    Path = request.Path
 | 
			
		||||
                };
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _libraryManager.AddMediaPath(request.Name, mediaPath);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
 | 
			
		||||
@ -885,12 +885,11 @@ 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())
 | 
			
		||||
            {
 | 
			
		||||
            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;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -394,7 +394,7 @@ namespace MediaBrowser.Api.Movies
 | 
			
		||||
        {
 | 
			
		||||
            var people = _libraryManager.GetPeople(new InternalPeopleQuery
 | 
			
		||||
            {
 | 
			
		||||
                PersonTypes = new string[]
 | 
			
		||||
                PersonTypes = new[]
 | 
			
		||||
                {
 | 
			
		||||
                    PersonType.Director
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -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,144 +380,132 @@ namespace MediaBrowser.Api.Playback
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (i == 0)
 | 
			
		||||
                switch (i)
 | 
			
		||||
                {
 | 
			
		||||
                    case 0:
 | 
			
		||||
                        request.DeviceProfileId = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 1)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 1:
 | 
			
		||||
                        request.DeviceId = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 2)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 2:
 | 
			
		||||
                        request.MediaSourceId = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 3)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 3:
 | 
			
		||||
                        request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 4)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 4:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.VideoCodec = val;
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 5)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 5:
 | 
			
		||||
                        request.AudioCodec = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 6)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 6:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 7)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 7:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 8)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 8:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 9)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 9:
 | 
			
		||||
                        request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 10)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 10:
 | 
			
		||||
                        request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 11)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 11:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 12)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 12:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 13)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 13:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 14)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 14:
 | 
			
		||||
                        request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 15)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 15:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.Level = val;
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 16)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 16:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 17)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 17:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 18)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 18:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.Profile = val;
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 19)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 19:
 | 
			
		||||
                        // cabac no longer used
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 20)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 20:
 | 
			
		||||
                        request.PlaySessionId = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 21)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 21:
 | 
			
		||||
                        // api_key
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 22)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 22:
 | 
			
		||||
                        request.LiveStreamId = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 23)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 23:
 | 
			
		||||
                        // Duplicating ItemId because of MediaMonkey
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 24)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 24:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 25)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 25:
 | 
			
		||||
                        if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
 | 
			
		||||
@ -534,50 +513,48 @@ namespace MediaBrowser.Api.Playback
 | 
			
		||||
                                videoRequest.SubtitleMethod = method;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 26)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 26:
 | 
			
		||||
                        request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 27)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 27:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 28)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 28:
 | 
			
		||||
                        request.Tag = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 29)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 29:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 30)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 30:
 | 
			
		||||
                        request.SubtitleCodec = val;
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 31)
 | 
			
		||||
                {
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 31:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 32)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 32:
 | 
			
		||||
                        if (videoRequest != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else if (i == 33)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                        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
 | 
			
		||||
            {
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
 | 
			
		||||
            else if (!string.IsNullOrWhiteSpace(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;
 | 
			
		||||
 | 
			
		||||
@ -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,10 +168,9 @@ 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))
 | 
			
		||||
                {
 | 
			
		||||
            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 | 
			
		||||
            using var reader = new StreamReader(stream);
 | 
			
		||||
 | 
			
		||||
            var text = reader.ReadToEnd();
 | 
			
		||||
 | 
			
		||||
            text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
 | 
			
		||||
@ -183,8 +182,6 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
			
		||||
 | 
			
		||||
            return text;
 | 
			
		||||
        }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
 | 
			
		||||
        {
 | 
			
		||||
@ -212,10 +209,8 @@ 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 reader = new StreamReader(fileStream))
 | 
			
		||||
                        {
 | 
			
		||||
                    using var fileStream = GetPlaylistFileStream(playlist);
 | 
			
		||||
                    using var reader = new StreamReader(fileStream);
 | 
			
		||||
                    var count = 0;
 | 
			
		||||
 | 
			
		||||
                    while (!reader.EndOfStream)
 | 
			
		||||
@ -234,8 +229,6 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
			
		||||
                    }
 | 
			
		||||
                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (IOException)
 | 
			
		||||
                {
 | 
			
		||||
                    // May get an error if the file is locked
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -234,59 +234,48 @@ namespace MediaBrowser.Api
 | 
			
		||||
            SetThumbImageInfo(result, item);
 | 
			
		||||
            SetBackdropImageInfo(result, item);
 | 
			
		||||
 | 
			
		||||
            var program = item as LiveTvProgram;
 | 
			
		||||
            if (program != null)
 | 
			
		||||
            {
 | 
			
		||||
                result.StartDate = program.StartDate;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var hasSeries = item as IHasSeries;
 | 
			
		||||
            if (hasSeries != null)
 | 
			
		||||
            switch (item)
 | 
			
		||||
            {
 | 
			
		||||
                case IHasSeries hasSeries:
 | 
			
		||||
                    result.Series = hasSeries.SeriesName;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var series = item as Series;
 | 
			
		||||
            if (series != null)
 | 
			
		||||
            {
 | 
			
		||||
                    break;
 | 
			
		||||
                case LiveTvProgram program:
 | 
			
		||||
                    result.StartDate = program.StartDate;
 | 
			
		||||
                    break;
 | 
			
		||||
                case Series series:
 | 
			
		||||
                    if (series.Status.HasValue)
 | 
			
		||||
                    {
 | 
			
		||||
                        result.Status = series.Status.Value.ToString();
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var album = item as MusicAlbum;
 | 
			
		||||
 | 
			
		||||
            if (album != null)
 | 
			
		||||
            {
 | 
			
		||||
                    break;
 | 
			
		||||
                case MusicAlbum album:
 | 
			
		||||
                    result.Artists = album.Artists;
 | 
			
		||||
                    result.AlbumArtist = album.AlbumArtist;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var song = item as Audio;
 | 
			
		||||
 | 
			
		||||
            if (song != null)
 | 
			
		||||
            {
 | 
			
		||||
                    break;
 | 
			
		||||
                case Audio song:
 | 
			
		||||
                    result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
 | 
			
		||||
                    result.Artists = song.Artists;
 | 
			
		||||
 | 
			
		||||
                album = song.AlbumEntity;
 | 
			
		||||
                    MusicAlbum musicAlbum = song.AlbumEntity;
 | 
			
		||||
 | 
			
		||||
                if (album != null)
 | 
			
		||||
                    if (musicAlbum != null)
 | 
			
		||||
                    {
 | 
			
		||||
                    result.Album = album.Name;
 | 
			
		||||
                    result.AlbumId = album.Id;
 | 
			
		||||
                        result.Album = musicAlbum.Name;
 | 
			
		||||
                        result.AlbumId = musicAlbum.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,13 +285,10 @@ namespace MediaBrowser.Api
 | 
			
		||||
        {
 | 
			
		||||
            var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
 | 
			
		||||
 | 
			
		||||
            if (itemWithImage == null)
 | 
			
		||||
            {
 | 
			
		||||
                if (item is Episode)
 | 
			
		||||
            if (itemWithImage == null && item is Episode)
 | 
			
		||||
            {
 | 
			
		||||
                itemWithImage = GetParentWithImage<Series>(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<BaseItem>(item, ImageType.Backdrop);
 | 
			
		||||
            }
 | 
			
		||||
            var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
 | 
			
		||||
                ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
 | 
			
		||||
 | 
			
		||||
            if (itemWithImage != null)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -230,18 +230,15 @@ 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))
 | 
			
		||||
                    {
 | 
			
		||||
                using var stream = await GetSubtitles(request).ConfigureAwait(false);
 | 
			
		||||
                using var reader = new StreamReader(stream);
 | 
			
		||||
 | 
			
		||||
                var text = reader.ReadToEnd();
 | 
			
		||||
 | 
			
		||||
                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));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
 | 
			
		||||
@ -92,10 +92,7 @@ namespace MediaBrowser.Api
 | 
			
		||||
        {
 | 
			
		||||
            lock (_timerLock)
 | 
			
		||||
            {
 | 
			
		||||
                if (KillTimer != null)
 | 
			
		||||
                {
 | 
			
		||||
                    KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                }
 | 
			
		||||
                KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<BaseItem>();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    episodes = ((Season)season).GetEpisodes(user, dtoOptions);
 | 
			
		||||
                }
 | 
			
		||||
                episodes = season == null ? new List<BaseItem>() : ((Season)season).GetEpisodes(user, dtoOptions);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
 | 
			
		||||
@ -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<BaseItem, bool> 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,31 +345,22 @@ 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.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Include item types
 | 
			
		||||
            if (includeItemTypes.Length > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (!includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Include MediaTypes
 | 
			
		||||
            if (mediaTypes.Length > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (!mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -396,12 +396,10 @@ namespace MediaBrowser.Api.UserLibrary
 | 
			
		||||
 | 
			
		||||
        public VideoType[] GetVideoTypes()
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(VideoTypes))
 | 
			
		||||
            {
 | 
			
		||||
                return Array.Empty<VideoType>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray();
 | 
			
		||||
            return string.IsNullOrEmpty(VideoTypes)
 | 
			
		||||
                ? Array.Empty<VideoType>()
 | 
			
		||||
                : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
                    .Select(v => Enum.Parse<VideoType>(v, true)).ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -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<ItemFilter>()
 | 
			
		||||
                : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
                    .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -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<ImageType>()
 | 
			
		||||
                : val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -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<string, SortOrder>(vals[i], sortOrder);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										80
									
								
								MediaBrowser.Common/Extensions/ProcessExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								MediaBrowser.Common/Extensions/ProcessExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Common.Extensions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Extension methods for <see cref="Process"/>.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class ProcessExtensions
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Asynchronously wait for the process to exit.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="process">The process to wait for.</param>
 | 
			
		||||
        /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
 | 
			
		||||
        /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
 | 
			
		||||
        /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
 | 
			
		||||
        public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
 | 
			
		||||
        {
 | 
			
		||||
            using (var cancelTokenSource = new CancellationTokenSource(timeout))
 | 
			
		||||
            {
 | 
			
		||||
                return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Asynchronously wait for the process to exit.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="process">The process to wait for.</param>
 | 
			
		||||
        /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
 | 
			
		||||
        /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
 | 
			
		||||
        public static async Task<bool> 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<bool>();
 | 
			
		||||
            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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a value indicating whether the associated process has been terminated using
 | 
			
		||||
        /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
 | 
			
		||||
        /// associated with the <see cref="Process"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="process">The process to check the exit status for.</param>
 | 
			
		||||
        /// <returns>
 | 
			
		||||
        /// True if the operating system process referenced by the <see cref="Process"/> component has
 | 
			
		||||
        /// terminated, or if there is no associated operating system process; otherwise, false.
 | 
			
		||||
        /// </returns>
 | 
			
		||||
        private static bool HasExitedSafe(this Process process)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                return process.HasExited;
 | 
			
		||||
            }
 | 
			
		||||
            catch (InvalidOperationException)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
                // 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))))
 | 
			
		||||
                    || 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");
 | 
			
		||||
 | 
			
		||||
@ -155,7 +155,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
 | 
			
		||||
                inputPath,
 | 
			
		||||
                attachmentStreamIndex,
 | 
			
		||||
                outputPath);
 | 
			
		||||
            var startInfo = new ProcessStartInfo
 | 
			
		||||
 | 
			
		||||
            int exitCode;
 | 
			
		||||
 | 
			
		||||
            using (var process = new Process
 | 
			
		||||
                {
 | 
			
		||||
                    StartInfo = new ProcessStartInfo
 | 
			
		||||
                    {
 | 
			
		||||
                        Arguments = processArgs,
 | 
			
		||||
                        FileName = _mediaEncoder.EncoderPath,
 | 
			
		||||
@ -163,22 +168,15 @@ namespace MediaBrowser.MediaEncoding.Attachments
 | 
			
		||||
                        CreateNoWindow = true,
 | 
			
		||||
                        WindowStyle = ProcessWindowStyle.Hidden,
 | 
			
		||||
                        ErrorDialog = false
 | 
			
		||||
            };
 | 
			
		||||
            var process = new Process
 | 
			
		||||
                    },
 | 
			
		||||
                    EnableRaisingEvents = true
 | 
			
		||||
                })
 | 
			
		||||
            {
 | 
			
		||||
                StartInfo = startInfo
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 | 
			
		||||
 | 
			
		||||
                process.Start();
 | 
			
		||||
 | 
			
		||||
            var processTcs = new TaskCompletionSource<bool>();
 | 
			
		||||
            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();
 | 
			
		||||
                var ranToCompletion = await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (!ranToCompletion)
 | 
			
		||||
                {
 | 
			
		||||
@ -193,9 +191,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            var exitCode = ranToCompletion ? process.ExitCode : -1;
 | 
			
		||||
 | 
			
		||||
            process.Dispose();
 | 
			
		||||
                exitCode = ranToCompletion ? process.ExitCode : -1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var failed = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 | 
			
		||||
            "libvpx",
 | 
			
		||||
            "libvpx-vp9",
 | 
			
		||||
            "aac",
 | 
			
		||||
            "libfdk_aac",
 | 
			
		||||
            "libmp3lame",
 | 
			
		||||
            "libopus",
 | 
			
		||||
            "libvorbis",
 | 
			
		||||
 | 
			
		||||
@ -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<EncodingHelper> _encodingHelperFactory;
 | 
			
		||||
        private readonly string _startupOptionFFmpegPath;
 | 
			
		||||
@ -55,7 +54,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
 | 
			
		||||
            ILogger<MediaEncoder> logger,
 | 
			
		||||
            IServerConfigurationManager configurationManager,
 | 
			
		||||
            IFileSystem fileSystem,
 | 
			
		||||
            IProcessFactory processFactory,
 | 
			
		||||
            ILocalizationManager localization,
 | 
			
		||||
            Lazy<EncodingHelper> 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,7 +351,9 @@ 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
 | 
			
		||||
            {
 | 
			
		||||
                StartInfo = new ProcessStartInfo
 | 
			
		||||
                {
 | 
			
		||||
                    CreateNoWindow = true,
 | 
			
		||||
                    UseShellExecute = false,
 | 
			
		||||
@ -366,18 +365,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
 | 
			
		||||
                    Arguments = args,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                IsHidden = true,
 | 
			
		||||
                    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
 | 
			
		||||
            {
 | 
			
		||||
                StartInfo = new ProcessStartInfo
 | 
			
		||||
                {
 | 
			
		||||
                    CreateNoWindow = true,
 | 
			
		||||
                    UseShellExecute = false,
 | 
			
		||||
                    FileName = _ffmpegPath,
 | 
			
		||||
                    Arguments = args,
 | 
			
		||||
                IsHidden = true,
 | 
			
		||||
                    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)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -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,17 +426,22 @@ 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),
 | 
			
		||||
                EnableRaisingEvents = true,
 | 
			
		||||
                IsHidden = true,
 | 
			
		||||
                        WindowStyle = ProcessWindowStyle.Hidden,
 | 
			
		||||
                        ErrorDialog = false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
                    },
 | 
			
		||||
                    EnableRaisingEvents = true
 | 
			
		||||
                })
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
@ -453,7 +455,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 | 
			
		||||
                    throw;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
 | 
			
		||||
                var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (!ranToCompletion)
 | 
			
		||||
                {
 | 
			
		||||
@ -469,9 +471,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            var exitCode = ranToCompletion ? process.ExitCode : -1;
 | 
			
		||||
 | 
			
		||||
            process.Dispose();
 | 
			
		||||
                exitCode = ranToCompletion ? process.ExitCode : -1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var failed = false;
 | 
			
		||||
 | 
			
		||||
@ -578,17 +579,22 @@ 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,
 | 
			
		||||
                EnableRaisingEvents = true,
 | 
			
		||||
                        FileName = _mediaEncoder.EncoderPath,
 | 
			
		||||
                        Arguments = processArgs,
 | 
			
		||||
                IsHidden = true,
 | 
			
		||||
                        WindowStyle = ProcessWindowStyle.Hidden,
 | 
			
		||||
                        ErrorDialog = false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
                    },
 | 
			
		||||
                    EnableRaisingEvents = true
 | 
			
		||||
                })
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
@ -602,7 +608,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 | 
			
		||||
                    throw;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
 | 
			
		||||
                var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (!ranToCompletion)
 | 
			
		||||
                {
 | 
			
		||||
@ -618,9 +624,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            var exitCode = ranToCompletion ? process.ExitCode : -1;
 | 
			
		||||
 | 
			
		||||
            process.Dispose();
 | 
			
		||||
                exitCode = ranToCompletion ? process.ExitCode : -1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var failed = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<bool> WaitForExitAsync(int timeMs);
 | 
			
		||||
        int ExitCode { get; }
 | 
			
		||||
        void Start();
 | 
			
		||||
        StreamWriter StandardInput { get; }
 | 
			
		||||
        StreamReader StandardError { get; }
 | 
			
		||||
        StreamReader StandardOutput { get; }
 | 
			
		||||
        ProcessOptions StartInfo { get; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								nuget.config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								nuget.config
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<configuration>
 | 
			
		||||
    <packageSources>
 | 
			
		||||
        <add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
 | 
			
		||||
    </packageSources>
 | 
			
		||||
</configuration>
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user