From 369785c184a8e19250c6e3b16b3609c222399552 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 8 Mar 2019 20:17:17 +0100 Subject: [PATCH 01/91] Remove usage of depricated 'WebRequest' Ref: https://docs.microsoft.com/en-us/dotnet/api/system.net.webrequest?view=netframework-4.7.2 --- .../HttpClientManager/HttpClientInfo.cs | 18 - .../HttpClientManager/HttpClientManager.cs | 432 ++++++------------ MediaBrowser.Common/Net/HttpRequestOptions.cs | 1 - MediaBrowser.Common/Net/IHttpClient.cs | 11 + 4 files changed, 161 insertions(+), 301 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs deleted file mode 100644 index f747b01b93..0000000000 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Net.Http; - -namespace Emby.Server.Implementations.HttpClientManager -{ - /// - /// Class HttpClientInfo - /// - public class HttpClientInfo - { - /// - /// Gets or sets the last timeout. - /// - /// The last timeout. - public DateTime LastTimeout { get; set; } - public HttpClient HttpClient { get; set; } - } -} diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 1bebdd1637..581d6bafd1 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Cache; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -55,12 +54,13 @@ namespace Emby.Server.Implementations.HttpClientManager { throw new ArgumentNullException(nameof(appPaths)); } + if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } - _logger = loggerFactory.CreateLogger("HttpClient"); + _logger = loggerFactory.CreateLogger(nameof(HttpClientManager)); _fileSystem = fileSystem; _appPaths = appPaths; _defaultUserAgentFn = defaultUserAgentFn; @@ -74,27 +74,26 @@ namespace Emby.Server.Implementations.HttpClientManager /// DON'T dispose it after use. /// /// The HTTP clients. - private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); /// /// Gets /// - /// The host. + /// The host. /// if set to true [enable HTTP compression]. /// HttpClient. /// host - private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression) + private HttpClient GetHttpClient(string url, bool enableHttpCompression) { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - var key = host + enableHttpCompression; + var key = GetHostFromUrl(url) + enableHttpCompression; if (!_httpClients.TryGetValue(key, out var client)) { - client = new HttpClientInfo(); + + client = new HttpClient() + { + BaseAddress = new Uri(url) + }; _httpClients.TryAdd(key, client); } @@ -102,110 +101,87 @@ namespace Emby.Server.Implementations.HttpClientManager return client; } - private WebRequest GetRequest(HttpRequestOptions options, string method) + private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method) { string url = options.Url; - var uriAddress = new Uri(url); string userInfo = uriAddress.UserInfo; if (!string.IsNullOrWhiteSpace(userInfo)) { - _logger.LogInformation("Found userInfo in url: {0} ... url: {1}", userInfo, url); + _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); url = url.Replace(userInfo + "@", string.Empty); } - var request = WebRequest.Create(url); + var request = new HttpRequestMessage(method, url); - if (request is HttpWebRequest httpWebRequest) - { - AddRequestHeaders(httpWebRequest, options); + AddRequestHeaders(request, options); - if (options.EnableHttpCompression) + if (options.EnableHttpCompression) + { + if (options.DecompressionMethod.HasValue + && options.DecompressionMethod.Value == CompressionMethod.Gzip) { - httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate; - if (options.DecompressionMethod.HasValue - && options.DecompressionMethod.Value == CompressionMethod.Gzip) - { - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip; - } + request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" }); } else { - httpWebRequest.AutomaticDecompression = DecompressionMethods.None; + request.Headers.Add(HeaderNames.AcceptEncoding, "deflate"); } + } - httpWebRequest.KeepAlive = options.EnableKeepAlive; + if (options.EnableKeepAlive) + { + request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); + } - if (!string.IsNullOrEmpty(options.Host)) - { - httpWebRequest.Host = options.Host; - } + if (!string.IsNullOrEmpty(options.Host)) + { + request.Headers.Add(HeaderNames.Host, options.Host); + } - if (!string.IsNullOrEmpty(options.Referer)) - { - httpWebRequest.Referer = options.Referer; - } + if (!string.IsNullOrEmpty(options.Referer)) + { + request.Headers.Add(HeaderNames.Referer, options.Referer); } - request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); + //request.Headers.Add(HeaderNames.CacheControl, "no-cache"); - request.Method = method; - request.Timeout = options.TimeoutMs; + //request.Headers.Add(HeaderNames., options.TimeoutMs; + /* if (!string.IsNullOrWhiteSpace(userInfo)) { var parts = userInfo.Split(':'); if (parts.Length == 2) { - request.Credentials = GetCredential(url, parts[0], parts[1]); - // TODO: .net core ?? - request.PreAuthenticate = true; + request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]); } } + */ return request; } - private static CredentialCache GetCredential(string url, string username, string password) - { - //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3; - var credentialCache = new CredentialCache(); - credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password)); - return credentialCache; - } - - private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options) + private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options) { var hasUserAgent = false; foreach (var header in options.RequestHeaders) { - if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase)) - { - request.Accept = header.Value; - } - else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) { - SetUserAgent(request, header.Value); hasUserAgent = true; } - else - { - request.Headers.Set(header.Key, header.Value); - } + + request.Headers.Add(header.Key, header.Value); } if (!hasUserAgent && options.EnableDefaultUserAgent) { - SetUserAgent(request, _defaultUserAgentFn()); + request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn()); } } - private static void SetUserAgent(HttpWebRequest request, string userAgent) - { - request.UserAgent = userAgent; - } - /// /// Gets the response internal. /// @@ -213,7 +189,7 @@ namespace Emby.Server.Implementations.HttpClientManager /// Task{HttpResponseInfo}. public Task GetResponse(HttpRequestOptions options) { - return SendAsync(options, "GET"); + return SendAsync(options, HttpMethod.Get); } /// @@ -235,7 +211,21 @@ namespace Emby.Server.Implementations.HttpClientManager /// Task{HttpResponseInfo}. /// /// - public async Task SendAsync(HttpRequestOptions options, string httpMethod) + public Task SendAsync(HttpRequestOptions options, string httpMethod) + { + var httpMethod2 = GetHttpMethod(httpMethod); + return SendAsync(options, httpMethod2); + } + + /// + /// send as an asynchronous operation. + /// + /// The options. + /// The HTTP method. + /// Task{HttpResponseInfo}. + /// + /// + public async Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod) { if (options.CacheMode == CacheMode.None) { @@ -263,6 +253,40 @@ namespace Emby.Server.Implementations.HttpClientManager return response; } + private HttpMethod GetHttpMethod(string httpMethod) + { + if (httpMethod.Equals("DELETE", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Delete; + } + else if (httpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Get; + } + else if (httpMethod.Equals("HEAD", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Head; + } + else if (httpMethod.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Options; + } + else if (httpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Post; + } + else if (httpMethod.Equals("PUT", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Put; + } + else if (httpMethod.Equals("TRACE", StringComparison.OrdinalIgnoreCase)) + { + return HttpMethod.Trace; + } + + throw new ArgumentException("Invalid HTTP method", nameof(httpMethod)); + } + private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url) { if (File.Exists(responseCachePath) @@ -294,31 +318,23 @@ namespace Emby.Server.Implementations.HttpClientManager } } - private async Task SendAsyncInternal(HttpRequestOptions options, string httpMethod) + private async Task SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod) { ValidateParams(options); options.CancellationToken.ThrowIfCancellationRequested(); - var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); - - if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds) - { - throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) - { - IsTimedOut = true - }; - } + var client = GetHttpClient(options.Url, options.EnableHttpCompression); - var httpWebRequest = GetRequest(options, httpMethod); + var httpWebRequest = GetRequestMessage(options, httpMethod); if (options.RequestContentBytes != null || !string.IsNullOrEmpty(options.RequestContent) || - string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase)) + httpMethod == HttpMethod.Post) { try { - var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty); + httpWebRequest.Content = new StringContent(Encoding.UTF8.GetString(options.RequestContentBytes) ?? options.RequestContent ?? string.Empty); var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded"; @@ -327,8 +343,8 @@ namespace Emby.Server.Implementations.HttpClientManager contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\""; } - httpWebRequest.ContentType = contentType; - (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); + httpWebRequest.Headers.Add(HeaderNames.ContentType, contentType); + await client.SendAsync(httpWebRequest).ConfigureAwait(false); } catch (Exception ex) { @@ -341,68 +357,45 @@ namespace Emby.Server.Implementations.HttpClientManager await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); } - if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds) - { - options.ResourcePool?.Release(); - - throw new HttpException($"Connection to {options.Url} timed out") { IsTimedOut = true }; - } - if (options.LogRequest) { - if (options.LogRequestAsDebug) - { - _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url); - } - else - { - _logger.LogInformation("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url); - } + _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url); } try { options.CancellationToken.ThrowIfCancellationRequested(); - if (!options.BufferContent) + /*if (!options.BufferContent) { - var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false); - - var httpResponse = (HttpWebResponse)response; + var response = await client.HttpClient.SendAsync(httpWebRequest).ConfigureAwait(false); - EnsureSuccessStatusCode(client, httpResponse, options); + await EnsureSuccessStatusCode(client, response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); - return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse); - } + return GetResponseInfo(response, await response.Content.ReadAsStreamAsync().ConfigureAwait(false), response.Content.Headers.ContentLength, response); + }*/ - using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false)) + using (var response = await client.SendAsync(httpWebRequest).ConfigureAwait(false)) { - var httpResponse = (HttpWebResponse)response; - - EnsureSuccessStatusCode(client, httpResponse, options); + await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); - using (var stream = httpResponse.GetResponseStream()) + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; - return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null); + return GetResponseInfo(response, memoryStream, memoryStream.Length, null); } } } catch (OperationCanceledException ex) { - throw GetCancellationException(options, client, options.CancellationToken, ex); - } - catch (Exception ex) - { - throw GetException(ex, options, client); + throw GetCancellationException(options, options.CancellationToken, ex); } finally { @@ -410,69 +403,54 @@ namespace Emby.Server.Implementations.HttpClientManager } } - private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable) + private HttpResponseInfo GetResponseInfo(HttpResponseMessage httpResponse, Stream content, long? contentLength, IDisposable disposable) { var responseInfo = new HttpResponseInfo(disposable) { Content = content, StatusCode = httpResponse.StatusCode, - ContentType = httpResponse.ContentType, + ContentType = httpResponse.Content.Headers.ContentType?.MediaType, ContentLength = contentLength, - ResponseUrl = httpResponse.ResponseUri.ToString() + ResponseUrl = httpResponse.Content.Headers.ContentLocation?.ToString() }; if (httpResponse.Headers != null) { - SetHeaders(httpResponse.Headers, responseInfo); + SetHeaders(httpResponse.Content.Headers, responseInfo); } return responseInfo; } - private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength) + private HttpResponseInfo GetResponseInfo(HttpResponseMessage httpResponse, string tempFile, long? contentLength) { var responseInfo = new HttpResponseInfo { TempFilePath = tempFile, StatusCode = httpResponse.StatusCode, - ContentType = httpResponse.ContentType, + ContentType = httpResponse.Content.Headers.ContentType?.MediaType, ContentLength = contentLength }; if (httpResponse.Headers != null) { - SetHeaders(httpResponse.Headers, responseInfo); + SetHeaders(httpResponse.Content.Headers, responseInfo); } return responseInfo; } - private static void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo) + private static void SetHeaders(HttpContentHeaders headers, HttpResponseInfo responseInfo) { - foreach (var key in headers.AllKeys) + foreach (var key in headers) { - responseInfo.Headers[key] = headers[key]; + responseInfo.Headers[key.Key] = string.Join(", ", key.Value); } } public Task Post(HttpRequestOptions options) { - return SendAsync(options, "POST"); - } - - /// - /// Performs a POST request - /// - /// The options. - /// Params to add to the POST data. - /// stream on success, null on failure - public async Task Post(HttpRequestOptions options, Dictionary postData) - { - options.SetPostData(postData); - - var response = await Post(options).ConfigureAwait(false); - - return response.Content; + return SendAsync(options, HttpMethod.Post); } /// @@ -482,9 +460,10 @@ namespace Emby.Server.Implementations.HttpClientManager /// Task{System.String}. public async Task GetTempFile(HttpRequestOptions options) { - var response = await GetTempFileResponse(options).ConfigureAwait(false); - - return response.TempFilePath; + using (var response = await GetTempFileResponse(options).ConfigureAwait(false)) + { + return response.TempFilePath; + } } public async Task GetTempFileResponse(HttpRequestOptions options) @@ -502,7 +481,7 @@ namespace Emby.Server.Implementations.HttpClientManager options.CancellationToken.ThrowIfCancellationRequested(); - var httpWebRequest = GetRequest(options, "GET"); + var httpWebRequest = GetRequestMessage(options, HttpMethod.Get); if (options.ResourcePool != null) { @@ -513,33 +492,22 @@ namespace Emby.Server.Implementations.HttpClientManager if (options.LogRequest) { - if (options.LogRequestAsDebug) - { - _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url); - } - else - { - _logger.LogInformation("HttpClientManager.GetTempFileResponse url: {0}", options.Url); - } + _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url); } - var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); + var client = GetHttpClient(options.Url, options.EnableHttpCompression); try { options.CancellationToken.ThrowIfCancellationRequested(); - using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false)) + using (var response = (await client.SendAsync(httpWebRequest).ConfigureAwait(false))) { - var httpResponse = (HttpWebResponse)response; - - EnsureSuccessStatusCode(client, httpResponse, options); + await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); - var contentLength = GetContentLength(httpResponse); - - using (var stream = httpResponse.GetResponseStream()) + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); @@ -547,13 +515,18 @@ namespace Emby.Server.Implementations.HttpClientManager options.Progress.Report(100); - return GetResponseInfo(httpResponse, tempFile, contentLength); + var contentLength = response.Content.Headers.ContentLength; + return GetResponseInfo(response, tempFile, contentLength); } } catch (Exception ex) { - DeleteTempFile(tempFile); - throw GetException(ex, options, client); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + throw GetException(ex, options); } finally { @@ -561,21 +534,7 @@ namespace Emby.Server.Implementations.HttpClientManager } } - private static long? GetContentLength(HttpWebResponse response) - { - var length = response.ContentLength; - - if (length == 0) - { - return null; - } - - return length; - } - - protected static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client) + private Exception GetException(Exception ex, HttpRequestOptions options) { if (ex is HttpException) { @@ -599,11 +558,6 @@ namespace Emby.Server.Implementations.HttpClientManager if (response != null) { exception.StatusCode = response.StatusCode; - - if ((int)response.StatusCode == 429) - { - client.LastTimeout = DateTime.UtcNow; - } } } @@ -624,7 +578,7 @@ namespace Emby.Server.Implementations.HttpClientManager if (operationCanceledException != null) { - return GetCancellationException(options, client, options.CancellationToken, operationCanceledException); + return GetCancellationException(options, options.CancellationToken, operationCanceledException); } if (options.LogErrors) @@ -635,18 +589,6 @@ namespace Emby.Server.Implementations.HttpClientManager return ex; } - private void DeleteTempFile(string file) - { - try - { - _fileSystem.DeleteFile(file); - } - catch (IOException) - { - // Might not have been created at all. No need to worry. - } - } - private void ValidateParams(HttpRequestOptions options) { if (string.IsNullOrEmpty(options.Url)) @@ -682,11 +624,10 @@ namespace Emby.Server.Implementations.HttpClientManager /// Throws the cancellation exception. /// /// The options. - /// The client. /// The cancellation token. /// The exception. /// Exception. - private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception) + private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception) { // If the HttpClient's timeout is reached, it will cancel the Task internally if (!cancellationToken.IsCancellationRequested) @@ -698,8 +639,6 @@ namespace Emby.Server.Implementations.HttpClientManager _logger.LogError(msg); } - client.LastTimeout = DateTime.UtcNow; - // Throw an HttpException so that the caller doesn't think it was cancelled by user code return new HttpException(msg, exception) { @@ -710,91 +649,20 @@ namespace Emby.Server.Implementations.HttpClientManager return exception; } - private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options) + private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) { - var statusCode = response.StatusCode; - - var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299; - - if (isSuccessful) + if (response.IsSuccessStatusCode) { return; } - if (options.LogErrorResponseBody) - { - try - { - using (var stream = response.GetResponseStream()) - { - if (stream != null) - { - using (var reader = new StreamReader(stream)) - { - var msg = reader.ReadToEnd(); - - _logger.LogError(msg); - } - } - } - } - catch - { + var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + _logger.LogError(msg); - } - } - - throw new HttpException(response.StatusDescription) + throw new HttpException(response.ReasonPhrase) { StatusCode = response.StatusCode }; } - - private static Task GetResponseAsync(WebRequest request, TimeSpan timeout) - { - var taskCompletion = new TaskCompletionSource(); - - var asyncTask = Task.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null); - - ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true); - var callback = new TaskCallback { taskCompletion = taskCompletion }; - asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted); - - // Handle errors - asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted); - - return taskCompletion.Task; - } - - private static void TimeoutCallback(object state, bool timedOut) - { - if (timedOut && state != null) - { - var request = (WebRequest)state; - request.Abort(); - } - } - - private class TaskCallback - { - public TaskCompletionSource taskCompletion; - - public void OnSuccess(Task task) - { - taskCompletion.TrySetResult(task.Result); - } - - public void OnError(Task task) - { - if (task.Exception == null) - { - taskCompletion.TrySetException(Enumerable.Empty()); - } - else - { - taskCompletion.TrySetException(task.Exception); - } - } - } } } diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index bea178517b..874383fedf 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -99,7 +99,6 @@ namespace MediaBrowser.Common.Net public bool EnableDefaultUserAgent { get; set; } public bool AppendCharsetToMimeType { get; set; } - public string DownloadFilePath { get; set; } private string GetHeaderValue(string name) { diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index 5aaf7e0be6..d332ab2000 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -1,5 +1,6 @@ using System.IO; using System.Threading.Tasks; +using System.Net.Http; namespace MediaBrowser.Common.Net { @@ -23,6 +24,8 @@ namespace MediaBrowser.Common.Net Task Get(HttpRequestOptions options); /// + /// Warning: Depricated function, + /// use 'Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead /// Sends the asynchronous. /// /// The options. @@ -30,6 +33,14 @@ namespace MediaBrowser.Common.Net /// Task{HttpResponseInfo}. Task SendAsync(HttpRequestOptions options, string httpMethod); + /// + /// Sends the asynchronous. + /// + /// The options. + /// The HTTP method. + /// Task{HttpResponseInfo}. + Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod); + /// /// Posts the specified options. /// From 7f42dcc60fd3aaf30f2408f6bddeb2b8bf921cdf Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 8 Mar 2019 20:32:14 +0100 Subject: [PATCH 02/91] Remove more unused stuff --- .../ApplicationHost.cs | 11 +++++----- .../HttpClientManager/HttpClientManager.cs | 18 ---------------- .../LiveTv/Listings/SchedulesDirect.cs | 10 +-------- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 2 -- .../LiveTv/TunerHosts/SharedHttpStream.cs | 6 ------ MediaBrowser.Common/Net/HttpRequestOptions.cs | 21 +------------------ 6 files changed, 7 insertions(+), 61 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f7d9bad1b0..2f396f8146 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; @@ -34,7 +35,6 @@ using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; -using Emby.Server.Implementations.Middleware; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Reflection; @@ -1479,12 +1479,12 @@ namespace Emby.Server.Implementations LogErrorResponseBody = false, LogErrors = false, LogRequest = false, - TimeoutMs = 10000, BufferContent = false, CancellationToken = cancellationToken })) { - return GetLocalApiUrl(response.ReadToEnd().Trim()); + string res = await response.ReadToEndAsync().ConfigureAwait(false); + return GetLocalApiUrl(res.Trim()); } } catch (Exception ex) @@ -1604,16 +1604,15 @@ namespace Emby.Server.Implementations LogErrorResponseBody = false, LogErrors = logPing, LogRequest = logPing, - TimeoutMs = 5000, BufferContent = false, CancellationToken = cancellationToken - }, "POST").ConfigureAwait(false)) + }, HttpMethod.Post).ConfigureAwait(false)) { using (var reader = new StreamReader(response.Content)) { - var result = reader.ReadToEnd(); + var result = await reader.ReadToEndAsync().ConfigureAwait(false); var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 581d6bafd1..b82d55d0e6 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -352,11 +352,6 @@ namespace Emby.Server.Implementations.HttpClientManager } } - if (options.ResourcePool != null) - { - await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); - } - if (options.LogRequest) { _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url); @@ -397,10 +392,6 @@ namespace Emby.Server.Implementations.HttpClientManager { throw GetCancellationException(options, options.CancellationToken, ex); } - finally - { - options.ResourcePool?.Release(); - } } private HttpResponseInfo GetResponseInfo(HttpResponseMessage httpResponse, Stream content, long? contentLength, IDisposable disposable) @@ -483,11 +474,6 @@ namespace Emby.Server.Implementations.HttpClientManager var httpWebRequest = GetRequestMessage(options, HttpMethod.Get); - if (options.ResourcePool != null) - { - await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false); - } - options.Progress.Report(0); if (options.LogRequest) @@ -528,10 +514,6 @@ namespace Emby.Server.Implementations.HttpClientManager throw GetException(ex, options); } - finally - { - options.ResourcePool?.Release(); - } } private Exception GetException(Exception ex, HttpRequestOptions options) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 4137760d07..f3f7477180 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -96,8 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings Url = ApiUrl + "/schedules", UserAgent = UserAgent, CancellationToken = cancellationToken, - // The data can be large so give it some extra time - TimeoutMs = 60000, LogErrorResponseBody = true, RequestContent = requestString }; @@ -115,9 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings Url = ApiUrl + "/programs", UserAgent = UserAgent, CancellationToken = cancellationToken, - LogErrorResponseBody = true, - // The data can be large so give it some extra time - TimeoutMs = 60000 + LogErrorResponseBody = true }; httpOptions.RequestHeaders["token"] = token; @@ -483,8 +479,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings CancellationToken = cancellationToken, RequestContent = imageIdString, LogErrorResponseBody = true, - // The data can be large so give it some extra time - TimeoutMs = 60000 }; try @@ -871,8 +865,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings UserAgent = UserAgent, CancellationToken = cancellationToken, LogErrorResponseBody = true, - // The data can be large so give it some extra time - TimeoutMs = 60000 }; httpOptions.RequestHeaders["token"] = token; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 24b100eddc..761275f8f3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/discover.json", GetApiUrl(info)), CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(10).TotalMilliseconds), BufferContent = false }, "GET").ConfigureAwait(false)) @@ -191,7 +190,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/tuners.html", GetApiUrl(info)), CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), BufferContent = false })) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index d74cf3be2d..e8b34da0ca 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -47,13 +47,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts CancellationToken = CancellationToken.None, BufferContent = false, - // Increase a little bit - TimeoutMs = 30000, - EnableHttpCompression = false, - - LogResponse = true, - LogResponseHeaders = true }; foreach (var header in mediaSource.RequiredHttpHeaders) diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 874383fedf..38e0ff0f55 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -28,18 +28,13 @@ namespace MediaBrowser.Common.Net get => GetHeaderValue(HeaderNames.Accept); set => RequestHeaders[HeaderNames.Accept] = value; } + /// /// Gets or sets the cancellation token. /// /// The cancellation token. public CancellationToken CancellationToken { get; set; } - /// - /// Gets or sets the resource pool. - /// - /// The resource pool. - public SemaphoreSlim ResourcePool { get; set; } - /// /// Gets or sets the user agent. /// @@ -86,8 +81,6 @@ namespace MediaBrowser.Common.Net public bool LogRequest { get; set; } public bool LogRequestAsDebug { get; set; } public bool LogErrors { get; set; } - public bool LogResponse { get; set; } - public bool LogResponseHeaders { get; set; } public bool LogErrorResponseBody { get; set; } public bool EnableKeepAlive { get; set; } @@ -95,7 +88,6 @@ namespace MediaBrowser.Common.Net public CacheMode CacheMode { get; set; } public TimeSpan CacheLength { get; set; } - public int TimeoutMs { get; set; } public bool EnableDefaultUserAgent { get; set; } public bool AppendCharsetToMimeType { get; set; } @@ -119,17 +111,6 @@ namespace MediaBrowser.Common.Net LogRequest = true; LogErrors = true; CacheMode = CacheMode.None; - - TimeoutMs = 20000; - } - - public void SetPostData(IDictionary values) - { - var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key])); - var postContent = string.Join("&", strings.ToArray()); - - RequestContent = postContent; - RequestContentType = "application/x-www-form-urlencoded"; } } From 6566c9136057f7b272b0d35501ed85034034d11e Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 25 Mar 2019 21:27:03 +0100 Subject: [PATCH 03/91] Seperate changes from #1023 The unrelated changes from #1023 (and more) --- .../Activity/ActivityLogEntryPoint.cs | 33 +++++--- .../ApplicationHost.cs | 5 +- .../Channels/ChannelManager.cs | 9 --- Emby.Server.Implementations/Dto/DtoService.cs | 28 +++---- .../Security/AuthenticationRepository.cs | 4 - .../Services/StringMapTypeDeserializer.cs | 6 +- .../Session/SessionManager.cs | 78 ++++++------------- MediaBrowser.Controller/Entities/UserView.cs | 5 +- .../Entities/UserViewBuilder.cs | 12 ++- 9 files changed, 72 insertions(+), 108 deletions(-) diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 98cd97c318..f32ad7196e 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -29,31 +27,39 @@ namespace Emby.Server.Implementations.Activity { public class ActivityLogEntryPoint : IServerEntryPoint { + private readonly ILogger _logger; private readonly IInstallationManager _installationManager; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; - private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; - private readonly IServerConfigurationManager _config; private readonly IServerApplicationHost _appHost; private readonly IDeviceManager _deviceManager; - public ActivityLogEntryPoint(ISessionManager sessionManager, IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, IInstallationManager installationManager, ILibraryManager libraryManager, ISubtitleManager subManager, IUserManager userManager, IServerConfigurationManager config, IServerApplicationHost appHost) + public ActivityLogEntryPoint( + ILogger logger, + ISessionManager sessionManager, + IDeviceManager deviceManager, + ITaskManager taskManager, + IActivityManager activityManager, + ILocalizationManager localization, + IInstallationManager installationManager, + ISubtitleManager subManager, + IUserManager userManager, + IServerApplicationHost appHost) { + _logger = logger; _sessionManager = sessionManager; + _deviceManager = deviceManager; _taskManager = taskManager; _activityManager = activityManager; _localization = localization; _installationManager = installationManager; - _libraryManager = libraryManager; _subManager = subManager; _userManager = userManager; - _config = config; _appHost = appHost; - _deviceManager = deviceManager; } public Task RunAsync() @@ -124,7 +130,7 @@ namespace Emby.Server.Implementations.Activity if (item == null) { - //_logger.LogWarning("PlaybackStopped reported with null media info."); + _logger.LogWarning("PlaybackStopped reported with null media info."); return; } @@ -155,7 +161,7 @@ namespace Emby.Server.Implementations.Activity if (item == null) { - //_logger.LogWarning("PlaybackStart reported with null media info."); + _logger.LogWarning("PlaybackStart reported with null media info."); return; } @@ -203,6 +209,7 @@ namespace Emby.Server.Implementations.Activity { return NotificationType.AudioPlayback.ToString(); } + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { return NotificationType.VideoPlayback.ToString(); @@ -217,6 +224,7 @@ namespace Emby.Server.Implementations.Activity { return NotificationType.AudioPlaybackStopped.ToString(); } + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { return NotificationType.VideoPlaybackStopped.ToString(); @@ -415,6 +423,7 @@ namespace Emby.Server.Implementations.Activity { vals.Add(e.Result.ErrorMessage); } + if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) { vals.Add(e.Result.LongErrorMessage); @@ -424,7 +433,7 @@ namespace Emby.Server.Implementations.Activity { Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), Type = NotificationType.TaskFailed.ToString(), - Overview = string.Join(Environment.NewLine, vals.ToArray()), + Overview = string.Join(Environment.NewLine, vals), ShortOverview = runningTime, Severity = LogLevel.Error }); @@ -503,6 +512,7 @@ namespace Emby.Server.Implementations.Activity { values.Add(CreateValueString(span.Hours, "hour")); } + // Number of minutes if (span.Minutes >= 1) { @@ -526,6 +536,7 @@ namespace Emby.Server.Implementations.Activity builder.Append(values[i]); } + // Return result return builder.ToString(); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 484942946c..ff49c74a3b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -831,10 +831,10 @@ namespace Emby.Server.Implementations DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager); serviceCollection.AddSingleton(DtoService); - ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager); + ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager); serviceCollection.AddSingleton(ChannelManager); - SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager); + SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, this, AuthenticationRepository, DeviceManager, MediaSourceManager); serviceCollection.AddSingleton(SessionManager); serviceCollection.AddSingleton( @@ -1035,7 +1035,6 @@ namespace Emby.Server.Implementations Video.LiveTvManager = LiveTvManager; Folder.UserViewManager = UserViewManager; UserView.TVSeriesManager = TVSeriesManager; - UserView.PlaylistManager = PlaylistManager; UserView.CollectionManager = CollectionManager; BaseItem.MediaSourceManager = MediaSourceManager; CollectionFolder.XmlSerializer = XmlSerializer; diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 7e50650d70..e9961e8bd7 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -20,7 +19,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; @@ -40,11 +38,8 @@ namespace Emby.Server.Implementations.Channels private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; private readonly IProviderManager _providerManager; - private readonly ILocalizationManager _localization; - public ChannelManager( IUserManager userManager, IDtoService dtoService, @@ -54,8 +49,6 @@ namespace Emby.Server.Implementations.Channels IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, - ILocalizationManager localization, - IHttpClient httpClient, IProviderManager providerManager) { _userManager = userManager; @@ -66,8 +59,6 @@ namespace Emby.Server.Implementations.Channels _fileSystem = fileSystem; _userDataManager = userDataManager; _jsonSerializer = jsonSerializer; - _localization = localization; - _httpClient = httpClient; _providerManager = providerManager; } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7b28a22a84..2f1b60be9c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -89,14 +89,11 @@ namespace Emby.Server.Implementations.Dto var channelTuples = new List>(); var index = 0; - var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType().ToList(); - foreach (var item in items) { - var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user, owner); + var dto = GetBaseItemDtoInternal(item, options, user, owner); - var tvChannel = item as LiveTvChannel; - if (tvChannel != null) + if (item is LiveTvChannel tvChannel) { channelTuples.Add(new Tuple(dto, tvChannel)); } @@ -105,9 +102,7 @@ namespace Emby.Server.Implementations.Dto programTuples.Add(new Tuple(item, dto)); } - var byName = item as IItemByName; - - if (byName != null) + if (item is IItemByName byName) { if (options.ContainsField(ItemFields.ItemCounts)) { @@ -130,8 +125,7 @@ namespace Emby.Server.Implementations.Dto if (programTuples.Count > 0) { - var task = _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user); - Task.WaitAll(task); + _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); } if (channelTuples.Count > 0) @@ -144,8 +138,7 @@ namespace Emby.Server.Implementations.Dto public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) { - var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType().ToList(); - var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user, owner); + var dto = GetBaseItemDtoInternal(item, options, user, owner); var tvChannel = item as LiveTvChannel; if (tvChannel != null) { @@ -188,7 +181,7 @@ namespace Emby.Server.Implementations.Dto }); } - private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, List allCollectionFolders, User user = null, BaseItem owner = null) + private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) { var dto = new BaseItemDto { @@ -312,6 +305,7 @@ namespace Emby.Server.Implementations.Dto { path = path.TrimStart('.'); } + if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase)) { fileExtensionContainer = path; @@ -325,8 +319,7 @@ namespace Emby.Server.Implementations.Dto public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null) { - var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType().ToList(); - var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user); + var dto = GetBaseItemDtoInternal(item, options, user); if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts)) { @@ -1051,14 +1044,15 @@ namespace Emby.Server.Implementations.Dto } else { - mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, item.Id.ToString("N"), StringComparison.OrdinalIgnoreCase)) + string id = item.Id.ToString("N"); + mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => i.MediaStreams) .ToArray(); } } else { - mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true).First().MediaStreams.ToArray(); + mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true)[0].MediaStreams.ToArray(); } dto.MediaStreams = mediaStreams; diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index c81a937672..29b8dfd3d7 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -15,13 +15,9 @@ namespace Emby.Server.Implementations.Security { public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository { - private readonly IServerConfigurationManager _config; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public AuthenticationRepository(ILoggerFactory loggerFactory, IServerConfigurationManager config) : base(loggerFactory.CreateLogger(nameof(AuthenticationRepository))) { - _config = config; DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db"); } diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index f835aa1b5b..7d42b2b510 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Services foreach (var propertyInfo in RestPath.GetSerializableProperties(type)) { - var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); + var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo); var propertyType = propertyInfo.PropertyType; var propertyParseStringFn = GetParseFn(propertyType); var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); @@ -110,9 +110,9 @@ namespace Emby.Server.Implementations.Services } } - internal class TypeAccessor + internal static class TypeAccessor { - public static Action GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) + public static Action GetSetPropertyMethod(PropertyInfo propertyInfo) { if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 985748caf2..dc23551db5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Devices; @@ -25,7 +24,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -53,8 +51,6 @@ namespace Emby.Server.Implementations.Session private readonly IImageProcessor _imageProcessor; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; private readonly IAuthenticationRepository _authRepo; @@ -96,9 +92,7 @@ namespace Emby.Server.Implementations.Session IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, - IJsonSerializer jsonSerializer, IServerApplicationHost appHost, - IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager) @@ -110,9 +104,7 @@ namespace Emby.Server.Implementations.Session _musicManager = musicManager; _dtoService = dtoService; _imageProcessor = imageProcessor; - _jsonSerializer = jsonSerializer; _appHost = appHost; - _httpClient = httpClient; _authRepo = authRepo; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; @@ -347,8 +339,7 @@ namespace Emby.Server.Implementations.Session var runtimeTicks = libraryItem.RunTimeTicks; MediaSourceInfo mediaSource = null; - var hasMediaSources = libraryItem as IHasMediaSources; - if (hasMediaSources != null) + if (libraryItem is IHasMediaSources hasMediaSources) { mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); @@ -1841,64 +1832,49 @@ namespace Emby.Server.Implementations.Session var data = dataFn(); - var tasks = sessions.Select(session => Task.Run(async () => + IEnumerable GetTasks() { - try - { - await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) + foreach (var session in sessions) { - _logger.LogError("Error sending message", ex); + yield return SendMessageToSession(session, name, data, cancellationToken); } + } - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return Task.WhenAll(GetTasks()); } public Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken) { CheckDisposed(); - var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList(); + var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)); - var tasks = sessions.Select(session => Task.Run(async () => + IEnumerable GetTasks() { - try - { - await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) + foreach (var session in sessions) { - _logger.LogError("Error sending message", ex); + yield return SendMessageToSession(session, name, data, cancellationToken); } + } - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return Task.WhenAll(GetTasks()); } public Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken) { CheckDisposed(); - var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)).ToList(); + var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - var tasks = sessions.Select(session => Task.Run(async () => + IEnumerable GetTasks() { - try - { - await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) + foreach (var session in sessions) { - _logger.LogError("Error sending message", ex); + yield return SendMessageToSession(session, name, data, cancellationToken); } + } - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return Task.WhenAll(GetTasks()); } public Task SendMessageToUserDeviceAndAdminSessions(string deviceId, string name, T data, CancellationToken cancellationToken) @@ -1906,23 +1882,17 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var sessions = Sessions - .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)) - .ToList(); + .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)); - var tasks = sessions.Select(session => Task.Run(async () => + IEnumerable GetTasks() { - try - { - await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) + foreach (var session in sessions) { - _logger.LogError("Error sending message", ex); + yield return SendMessageToSession(session, name, data, cancellationToken); } + } - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return Task.WhenAll(GetTasks()); } private bool IsAdminSession(SessionInfo s) diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 3e2191376b..4a6d32dcec 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; @@ -17,7 +16,6 @@ namespace MediaBrowser.Controller.Entities public Guid? UserId { get; set; } public static ITVSeriesManager TVSeriesManager; - public static IPlaylistManager PlaylistManager; [IgnoreDataMember] public string CollectionType => ViewType; @@ -38,6 +36,7 @@ namespace MediaBrowser.Controller.Entities { list.Add(Id); } + return list; } @@ -65,7 +64,7 @@ namespace MediaBrowser.Controller.Entities parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } - return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager) + return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager) .GetUserItems(parent, this, CollectionType, query); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 683218a9e9..e483c8f349 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -21,9 +20,14 @@ namespace MediaBrowser.Controller.Entities private readonly IUserDataManager _userDataManager; private readonly ITVSeriesManager _tvSeriesManager; private readonly IServerConfigurationManager _config; - private readonly IPlaylistManager _playlistManager; - public UserViewBuilder(IUserViewManager userViewManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager) + public UserViewBuilder( + IUserViewManager userViewManager, + ILibraryManager libraryManager, + ILogger logger, + IUserDataManager userDataManager, + ITVSeriesManager tvSeriesManager, + IServerConfigurationManager config) { _userViewManager = userViewManager; _libraryManager = libraryManager; @@ -31,7 +35,6 @@ namespace MediaBrowser.Controller.Entities _userDataManager = userDataManager; _tvSeriesManager = tvSeriesManager; _config = config; - _playlistManager = playlistManager; } public QueryResult GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) @@ -110,6 +113,7 @@ namespace MediaBrowser.Controller.Entities { return GetResult(GetMediaFolders(user).OfType().SelectMany(i => i.GetChildren(user, true)), queryParent, query); } + return queryParent.GetItems(query); } } From 2696ac5eacfb4702d629bc06a8b42b868c316116 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 24 Feb 2019 03:16:19 +0100 Subject: [PATCH 04/91] Lower the amount of running tasks --- .../HttpServer/HttpListenerHost.cs | 15 +++---- .../Session/SessionWebSocketListener.cs | 6 +-- .../SocketSharp/RequestMono.cs | 16 +------- .../SocketSharp/WebSocketSharpRequest.cs | 39 ++++++++++--------- Jellyfin.Server/Program.cs | 6 +-- MediaBrowser.Api/Library/LibraryService.cs | 19 ++++----- .../Net/BasePeriodicWebSocketListener.cs | 4 +- .../Net/IWebSocketListener.cs | 2 +- RSSDP/HttpParserBase.cs | 6 +-- RSSDP/SsdpCommunicationsServer.cs | 21 ++++------ 10 files changed, 53 insertions(+), 81 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e8d47cad52..9214f2afe2 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -823,19 +822,15 @@ namespace Emby.Server.Implementations.HttpServer Logger.LogDebug("Websocket message received: {0}", result.MessageType); - var tasks = _webSocketListeners.Select(i => Task.Run(async () => + IEnumerable GetTasks() { - try - { - await i.ProcessMessage(result).ConfigureAwait(false); - } - catch (Exception ex) + foreach (var x in _webSocketListeners) { - Logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty); + yield return x.ProcessMessageAsync(result); } - })); + } - return Task.WhenAll(tasks); + return Task.WhenAll(GetTasks()); } public void Dispose() diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index a551433ed9..63ec757626 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -89,10 +89,8 @@ namespace Emby.Server.Implementations.Session /// /// The message. /// Task. - public Task ProcessMessage(WebSocketMessageInfo message) - { - return Task.CompletedTask; - } + public Task ProcessMessageAsync(WebSocketMessageInfo message) + => Task.CompletedTask; private void EnsureController(SessionInfo session, IWebSocketConnection connection) { diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs index 373f6d7580..ec637186f0 100644 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -86,8 +86,7 @@ namespace Emby.Server.Implementations.SocketSharp else { // We use a substream, as in 2.x we will support large uploads streamed to disk, - var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - files[e.Name] = sub; + files[e.Name] = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); } } } @@ -374,7 +373,7 @@ namespace Emby.Server.Implementations.SocketSharp var elem = new Element(); ReadOnlySpan header; - while ((header = ReadHeaders().AsSpan()) != null) + while ((header = ReadLine().AsSpan()).Length != 0) { if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { @@ -513,17 +512,6 @@ namespace Emby.Server.Implementations.SocketSharp return false; } - private string ReadHeaders() - { - string s = ReadLine(); - if (s.Length == 0) - { - return null; - } - - return s; - } - private static bool CompareBytes(byte[] orig, byte[] other) { for (int i = orig.Length - 1; i >= 0; i--) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index e0a0ee2861..6fdc6a3c8d 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Linq; using System.Text; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; @@ -474,27 +474,28 @@ namespace Emby.Server.Implementations.SocketSharp { get { - if (httpFiles == null) + if (httpFiles != null) { - if (files == null) - { - return httpFiles = Array.Empty(); - } + return httpFiles; + } - httpFiles = new IHttpFile[files.Count]; - var i = 0; - foreach (var pair in files) + if (files == null) + { + return httpFiles = Array.Empty(); + } + + var values = files.Values; + httpFiles = new IHttpFile[values.Count]; + for (int i = 0; i < values.Count; i++) + { + var reqFile = values.ElementAt(i); + httpFiles[i] = new HttpFile { - var reqFile = pair.Value; - httpFiles[i] = new HttpFile - { - ContentType = reqFile.ContentType, - ContentLength = reqFile.ContentLength, - FileName = reqFile.FileName, - InputStream = reqFile.InputStream, - }; - i++; - } + ContentType = reqFile.ContentType, + ContentLength = reqFile.ContentLength, + FileName = reqFile.FileName, + InputStream = reqFile.InputStream, + }; } return httpFiles; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 82a76c6378..fbeb7a2e82 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -37,7 +37,7 @@ namespace Jellyfin.Server private static bool _restartOnShutdown; private static IConfiguration appConfig; - public static async Task Main(string[] args) + public static Task Main(string[] args) { // For backwards compatibility. // Modify any input arguments now which start with single-hyphen to POSIX standard @@ -51,8 +51,8 @@ namespace Jellyfin.Server } // Parse the command line arguments and either start the app or exit indicating error - await Parser.Default.ParseArguments(args) - .MapResult(StartApp, _ => Task.CompletedTask).ConfigureAwait(false); + return Parser.Default.ParseArguments(args) + .MapResult(StartApp, _ => Task.CompletedTask); } public static void Shutdown() diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 8eefbdf2ca..8a5a793dfd 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -988,19 +988,16 @@ namespace MediaBrowser.Api.Library /// Posts the specified request. /// /// The request. - public void Post(RefreshLibrary request) + public async Task Post(RefreshLibrary request) { - Task.Run(() => + try { - try - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error refreshing library"); - } - }); + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error refreshing library"); + } } /// diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 8444125462..ee5c1a165a 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Controller.Net /// /// The message. /// Task. - public Task ProcessMessage(WebSocketMessageInfo message) + public Task ProcessMessageAsync(WebSocketMessageInfo message) { if (message == null) { @@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.Net Stop(message); } - return Task.FromResult(true); + return Task.CompletedTask; } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs index e38f0e259f..0f472a2bc7 100644 --- a/MediaBrowser.Controller/Net/IWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs @@ -12,6 +12,6 @@ namespace MediaBrowser.Controller.Net /// /// The message. /// Task. - Task ProcessMessage(WebSocketMessageInfo message); + Task ProcessMessageAsync(WebSocketMessageInfo message); } } diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index 18712470da..76d816e7bd 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -23,8 +23,6 @@ namespace Rssdp.Infrastructure #region Public Methods - private static byte[] EmptyByteArray = new byte[]{}; - /// /// Parses the provided into either a or object. /// @@ -46,7 +44,7 @@ namespace Rssdp.Infrastructure if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", nameof(data)); if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", nameof(data)); - using (var retVal = new ByteArrayContent(EmptyByteArray)) + using (var retVal = new ByteArrayContent(Array.Empty())) { var lines = data.Split(LineTerminators, StringSplitOptions.None); @@ -209,4 +207,4 @@ namespace Rssdp.Infrastructure #endregion } -} \ No newline at end of file +} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index d9a4b6ac01..5d2afc37a0 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -355,7 +355,7 @@ namespace Rssdp.Infrastructure { var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort); - ListenToSocket(socket); + _ = ListenToSocketInternal(socket); return socket; } @@ -389,19 +389,12 @@ namespace Rssdp.Infrastructure foreach (var socket in sockets) { - ListenToSocket(socket); + _ = ListenToSocketInternal(socket); } return sockets; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capturing task to local variable removes compiler warning, task is not otherwise required.")] - private void ListenToSocket(ISocket socket) - { - // Tasks are captured to local variables even if we don't use them just to avoid compiler warnings. - var t = Task.Run(() => ListenToSocketInternal(socket)); - } - private async Task ListenToSocketInternal(ISocket socket) { var cancelled = false; @@ -448,10 +441,10 @@ namespace Rssdp.Infrastructure private void ProcessMessage(string data, IpEndPointInfo endPoint, IpAddressInfo receivedOnLocalIpAddress) { - //Responses start with the HTTP version, prefixed with HTTP/ while - //requests start with a method which can vary and might be one we haven't - //seen/don't know. We'll check if this message is a request or a response - //by checking for the HTTP/ prefix on the start of the message. + // Responses start with the HTTP version, prefixed with HTTP/ while + // requests start with a method which can vary and might be one we haven't + // seen/don't know. We'll check if this message is a request or a response + // by checking for the HTTP/ prefix on the start of the message. if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase)) { HttpResponseMessage responseMessage = null; @@ -465,7 +458,9 @@ namespace Rssdp.Infrastructure } if (responseMessage != null) + { OnResponseReceived(responseMessage, endPoint, receivedOnLocalIpAddress); + } } else { From a332092769cec5a3b17b7fb49b2d7c66bfe289bd Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 26 Mar 2019 19:20:40 +0100 Subject: [PATCH 05/91] Reduce complexity http routes --- .../ApplicationHost.cs | 37 +------ .../HttpServer/HttpListenerHost.cs | 50 +++------ .../Services/ServiceController.cs | 54 ++++----- .../Services/ServiceHandler.cs | 103 +++++++++--------- .../Services/SwaggerService.cs | 20 ++-- Jellyfin.Server/Program.cs | 5 +- 6 files changed, 105 insertions(+), 164 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 484942946c..ce08f2a284 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -617,8 +617,6 @@ namespace Emby.Server.Implementations DiscoverTypes(); - SetHttpLimit(); - await RegisterResources(serviceCollection).ConfigureAwait(false); FindParts(); @@ -918,8 +916,7 @@ namespace Emby.Server.Implementations .Distinct(); logger.LogInformation("Arguments: {Args}", commandLineArgs); - // FIXME: @bond this logs the kernel version, not the OS version - logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version); + logger.LogInformation("Operating system: {OS}", OperatingSystem.Name); logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess); logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); @@ -929,19 +926,6 @@ namespace Emby.Server.Implementations logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); } - private void SetHttpLimit() - { - try - { - // Increase the max http request limit - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error setting http limit"); - } - } - private X509Certificate2 GetCertificate(CertificateInfo info) { var certificateLocation = info?.Path; @@ -1483,6 +1467,7 @@ namespace Emby.Server.Implementations { Logger.LogError(ex, "Error getting WAN Ip address information"); } + return null; } @@ -1756,24 +1741,6 @@ namespace Emby.Server.Implementations { } - /// - /// Called when [application updated]. - /// - /// The package. - protected void OnApplicationUpdated(PackageVersionInfo package) - { - Logger.LogInformation("Application has been updated to version {0}", package.versionStr); - - ApplicationUpdated?.Invoke( - this, - new GenericEventArgs() - { - Argument = package - }); - - NotifyPendingRestart(); - } - private bool _disposed = false; /// diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e8d47cad52..79b8f52d78 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net.Sockets; @@ -11,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -127,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer private List GetRequestFilterAttributes(Type requestDtoType) { - var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType().ToList(); + var attributes = requestDtoType.GetCustomAttributes(true).OfType().ToList(); var serviceType = GetServiceTypeByRequest(requestDtoType); if (serviceType != null) { - attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType()); + attributes.AddRange(serviceType.GetCustomAttributes(true).OfType()); } attributes.Sort((x, y) => x.Priority - y.Priority); @@ -154,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer QueryString = e.QueryString ?? new QueryCollection() }; - connection.Closed += Connection_Closed; + connection.Closed += OnConnectionClosed; lock (_webSocketConnections) { @@ -164,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer WebSocketConnected?.Invoke(this, new GenericEventArgs(connection)); } - private void Connection_Closed(object sender, EventArgs e) + private void OnConnectionClosed(object sender, EventArgs e) { lock (_webSocketConnections) { @@ -322,14 +320,14 @@ namespace Emby.Server.Implementations.HttpServer private static string NormalizeConfiguredLocalAddress(string address) { - var index = address.Trim('/').IndexOf('/'); - + var add = address.AsSpan().Trim('/'); + int index = add.IndexOf('/'); if (index != -1) { - address = address.Substring(index + 1); + add = add.Slice(index + 1); } - return address.Trim('/'); + return add.TrimStart('/').ToString(); } private bool ValidateHost(string host) @@ -399,8 +397,8 @@ namespace Emby.Server.Implementations.HttpServer if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) { // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 || - urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) + if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 + || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) { return true; } @@ -572,7 +570,7 @@ namespace Emby.Server.Implementations.HttpServer if (handler != null) { - await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false); + await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false); } else { @@ -613,21 +611,11 @@ namespace Emby.Server.Implementations.HttpServer { var pathInfo = httpReq.PathInfo; - var pathParts = pathInfo.TrimStart('/').Split('/'); - if (pathParts.Length == 0) - { - Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); - return null; - } - - var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType); + pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType); + var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo); if (restPath != null) { - return new ServiceHandler - { - RestPath = restPath, - ResponseContentType = contentType - }; + return new ServiceHandler(restPath, contentType); } Logger.LogError("Could not find handler for {PathInfo}", pathInfo); @@ -655,11 +643,6 @@ namespace Emby.Server.Implementations.HttpServer } else { - // TODO what is this? - var httpsUrl = url - .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) - .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); - RedirectToUrl(httpRes, url); } } @@ -684,10 +667,7 @@ namespace Emby.Server.Implementations.HttpServer UrlPrefixes = urlPrefixes.ToArray(); ServiceController = new ServiceController(); - Logger.LogInformation("Calling ServiceStack AppHost.Init"); - - var types = services.Select(r => r.GetType()).ToArray(); - + var types = services.Select(r => r.GetType()); ServiceController.Init(this, types); ResponseFilters = new Action[] diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 5796956d87..5e3d529c68 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -1,26 +1,17 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; namespace Emby.Server.Implementations.Services { - public delegate Task InstanceExecFn(IRequest requestContext, object intance, object request); public delegate object ActionInvokerFn(object intance, object request); public delegate void VoidActionInvokerFn(object intance, object request); public class ServiceController { - public static ServiceController Instance; - - public ServiceController() - { - Instance = this; - } - - public void Init(HttpListenerHost appHost, Type[] serviceTypes) + public void Init(HttpListenerHost appHost, IEnumerable serviceTypes) { foreach (var serviceType in serviceTypes) { @@ -37,7 +28,11 @@ namespace Emby.Server.Implementations.Services foreach (var mi in serviceType.GetActions()) { var requestType = mi.GetParameters()[0].ParameterType; - if (processedReqs.Contains(requestType)) continue; + if (processedReqs.Contains(requestType)) + { + continue; + } + processedReqs.Add(requestType); ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); @@ -55,18 +50,6 @@ namespace Emby.Server.Implementations.Services } } - public static Type FirstGenericType(Type type) - { - while (type != null) - { - if (type.GetTypeInfo().IsGenericType) - return type; - - type = type.GetTypeInfo().BaseType; - } - return null; - } - public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) @@ -84,17 +67,24 @@ namespace Emby.Server.Implementations.Services public void RegisterRestPath(RestPath restPath) { - if (!restPath.Path.StartsWith("/")) + if (restPath.Path[0] != '/') + { throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName())); + } + if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) + { throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName())); + } - if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch)) + if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch)) { - pathsAtFirstMatch = new List(); - RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch; + pathsAtFirstMatch.Add(restPath); + } + else + { + RestPathMap[restPath.FirstMatchHashKey] = new List() { restPath }; } - pathsAtFirstMatch.Add(restPath); } public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) @@ -155,17 +145,15 @@ namespace Emby.Server.Implementations.Services return null; } - public Task Execute(HttpListenerHost appHost, object requestDto, IRequest req) + public Task Execute(HttpListenerHost httpHost, object requestDto, IRequest req) { req.Dto = requestDto; var requestType = requestDto.GetType(); req.OperationName = requestType.Name; - var serviceType = appHost.GetServiceTypeByRequest(requestType); - - var service = appHost.CreateInstance(serviceType); + var serviceType = httpHost.GetServiceTypeByRequest(requestType); - //var service = typeFactory.CreateInstance(serviceType); + var service = httpHost.CreateInstance(serviceType); var serviceRequiresContext = service as IRequiresRequest; if (serviceRequiresContext != null) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 3c8adfc983..243d2cca27 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -11,6 +11,18 @@ namespace Emby.Server.Implementations.Services { public class ServiceHandler { + private readonly ServiceController _serviceController; + + public RestPath RestPath { get; } + + public string ResponseContentType { get; } + + internal ServiceHandler(RestPath restPath, string responseContentType) + { + RestPath = restPath; + ResponseContentType = responseContentType; + } + protected static Task CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) { if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) @@ -21,21 +33,22 @@ namespace Emby.Server.Implementations.Services return deserializer(requestType, httpReq.InputStream); } } + return Task.FromResult(host.CreateInstance(requestType)); } - public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) + public RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) { pathInfo = GetSanitizedPathInfo(pathInfo, out contentType); - return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo); + return _serviceController.GetRestPathForRequest(httpMethod, pathInfo); } public static string GetSanitizedPathInfo(string pathInfo, out string contentType) { contentType = null; var pos = pathInfo.LastIndexOf('.'); - if (pos >= 0) + if (pos != -1) { var format = pathInfo.Substring(pos + 1); contentType = GetFormatContentType(format); @@ -44,58 +57,38 @@ namespace Emby.Server.Implementations.Services pathInfo = pathInfo.Substring(0, pos); } } + return pathInfo; } private static string GetFormatContentType(string format) { //built-in formats - if (format == "json") - return "application/json"; - if (format == "xml") - return "application/xml"; - - return null; - } - - public RestPath GetRestPath(string httpMethod, string pathInfo) - { - if (this.RestPath == null) + switch (format) { - this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out string contentType); - - if (contentType != null) - ResponseContentType = contentType; + case "json": return "application/json"; + case "xml": return "application/xml"; + default: return null; } - return this.RestPath; } - public RestPath RestPath { get; set; } - - // Set from SSHHF.GetHandlerForPathInfo() - public string ResponseContentType { get; set; } - - public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName, CancellationToken cancellationToken) + public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, IResponse httpRes, ILogger logger, CancellationToken cancellationToken) { - var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo); - if (restPath == null) - { - throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo); - } - - SetRoute(httpReq, restPath); + httpReq.Items["__route"] = RestPath; if (ResponseContentType != null) + { httpReq.ResponseContentType = ResponseContentType; + } - var request = httpReq.Dto = await CreateRequest(appHost, httpReq, restPath, logger).ConfigureAwait(false); + var request = httpReq.Dto = await CreateRequest(httpHost, httpReq, RestPath, logger).ConfigureAwait(false); - appHost.ApplyRequestFilters(httpReq, httpRes, request); + httpHost.ApplyRequestFilters(httpReq, httpRes, request); - var response = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false); + var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); // Apply response filters - foreach (var responseFilter in appHost.ResponseFilters) + foreach (var responseFilter in httpHost.ResponseFilters) { responseFilter(httpReq, httpRes, response); } @@ -152,7 +145,11 @@ namespace Emby.Server.Implementations.Services foreach (var name in request.QueryString.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } var values = request.QueryString[name]; if (values.Count == 1) @@ -175,7 +172,11 @@ namespace Emby.Server.Implementations.Services { foreach (var name in formData.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } var values = formData.GetValues(name); if (values.Count == 1) @@ -210,7 +211,12 @@ namespace Emby.Server.Implementations.Services foreach (var name in request.QueryString.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } + map[name] = request.QueryString[name]; } @@ -221,7 +227,12 @@ namespace Emby.Server.Implementations.Services { foreach (var name in formData.Keys) { - if (name == null) continue; //thank you ASP.NET + if (name == null) + { + // thank you ASP.NET + continue; + } + map[name] = formData[name]; } } @@ -229,17 +240,5 @@ namespace Emby.Server.Implementations.Services return map; } - - private static void SetRoute(IRequest req, RestPath route) - { - req.Items["__route"] = route; - } - - private static RestPath GetRoute(IRequest req) - { - req.Items.TryGetValue("__route", out var route); - return route as RestPath; - } } - } diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs index 3e6970eefd..d223864364 100644 --- a/Emby.Server.Implementations/Services/SwaggerService.cs +++ b/Emby.Server.Implementations/Services/SwaggerService.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; +using Emby.Server.Implementations.HttpServer; namespace Emby.Server.Implementations.Services { @@ -109,10 +109,16 @@ namespace Emby.Server.Implementations.Services public class SwaggerService : IService, IRequiresRequest { + private readonly IHttpServer _httpServer; private SwaggerSpec _spec; public IRequest Request { get; set; } + public SwaggerService(IHttpServer httpServer) + { + _httpServer = httpServer; + } + public object Get(GetSwaggerSpec request) { return _spec ?? (_spec = GetSpec()); @@ -181,7 +187,8 @@ namespace Emby.Server.Implementations.Services { var paths = new SortedDictionary>(); - var all = ServiceController.Instance.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); + // REVIEW: this can be done better + var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); foreach (var current in all) { @@ -192,11 +199,8 @@ namespace Emby.Server.Implementations.Services continue; } - if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase)) + if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase) + || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 82a76c6378..fab584befc 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -19,7 +19,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -119,6 +118,10 @@ namespace Jellyfin.Server SQLitePCL.Batteries_V2.Init(); + // Increase the max http request limit + // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. + ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); + // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); From 93e535d3a143d092effb37e529e5682c3d11802a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 26 Mar 2019 22:56:05 +0100 Subject: [PATCH 06/91] Trying to make sense of the streaming code Mostly small changes as I was looking through the code. * async void -> async Task * Properly implemented dispose methods * Pass the logstream directly to the JobLogger * Style fixes --- MediaBrowser.Api/ApiEntryPoint.cs | 15 +- .../Playback/BaseStreamingService.cs | 152 +++++++----------- MediaBrowser.Api/Playback/StreamState.cs | 91 ++++------- .../MediaEncoding/EncodingJobInfo.cs | 58 +++---- .../MediaEncoding/JobLogger.cs | 7 +- .../Dlna/ContentFeatureBuilder.cs | 20 +-- 6 files changed, 138 insertions(+), 205 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 700cbb9439..a223a4fe38 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -415,7 +415,7 @@ namespace MediaBrowser.Api public void OnTranscodeEndRequest(TranscodingJob job) { job.ActiveRequestCount--; - //Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); + Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); if (job.ActiveRequestCount <= 0) { PingTimer(job, false); @@ -428,7 +428,7 @@ namespace MediaBrowser.Api throw new ArgumentNullException(nameof(playSessionId)); } - //Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); + Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); List jobs; @@ -443,7 +443,7 @@ namespace MediaBrowser.Api { if (isUserPaused.HasValue) { - //Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id); + Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id); job.IsUserPaused = isUserPaused.Value; } PingTimer(job, true); @@ -601,7 +601,6 @@ namespace MediaBrowser.Api { Logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path); - //process.Kill(); process.StandardInput.WriteLine("q"); // Need to wait because killing is asynchronous @@ -701,7 +700,7 @@ namespace MediaBrowser.Api { try { - //Logger.LogDebug("Deleting HLS file {0}", file); + Logger.LogDebug("Deleting HLS file {0}", file); _fileSystem.DeleteFile(file); } catch (FileNotFoundException) @@ -840,12 +839,12 @@ namespace MediaBrowser.Api { if (KillTimer == null) { - //Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); } else { - //Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); KillTimer.Change(intervalMs, Timeout.Infinite); } } @@ -864,7 +863,7 @@ namespace MediaBrowser.Api { var intervalMs = PingTimeout; - //Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); KillTimer.Change(intervalMs, Timeout.Infinite); } } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index ae259a4f59..b480d055af 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -16,7 +15,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -32,6 +30,8 @@ namespace MediaBrowser.Api.Playback /// public abstract class BaseStreamingService : BaseApiService { + protected static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); + /// /// Gets or sets the application paths. /// @@ -65,15 +65,25 @@ namespace MediaBrowser.Api.Playback protected IFileSystem FileSystem { get; private set; } protected IDlnaManager DlnaManager { get; private set; } + protected IDeviceManager DeviceManager { get; private set; } + protected ISubtitleEncoder SubtitleEncoder { get; private set; } + protected IMediaSourceManager MediaSourceManager { get; private set; } + protected IJsonSerializer JsonSerializer { get; private set; } protected IAuthorizationContext AuthorizationContext { get; private set; } protected EncodingHelper EncodingHelper { get; set; } + /// + /// Gets the type of the transcoding job. + /// + /// The type of the transcoding job. + protected abstract TranscodingJobType TranscodingJobType { get; } + /// /// Initializes a new instance of the class. /// @@ -112,12 +122,6 @@ namespace MediaBrowser.Api.Playback /// protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding); - /// - /// Gets the type of the transcoding job. - /// - /// The type of the transcoding job. - protected abstract TranscodingJobType TranscodingJobType { get; } - /// /// Gets the output file extension. /// @@ -133,31 +137,18 @@ namespace MediaBrowser.Api.Playback /// private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension) { - var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; - var data = GetCommandLineArguments("dummy\\dummy", encodingOptions, state, false); - data += "-" + (state.Request.DeviceId ?? string.Empty); - data += "-" + (state.Request.PlaySessionId ?? string.Empty); - - var dataHash = data.GetMD5().ToString("N"); + data += "-" + (state.Request.DeviceId ?? string.Empty) + + "-" + (state.Request.PlaySessionId ?? string.Empty); - if (EnableOutputInSubFolder) - { - return Path.Combine(folder, dataHash, dataHash + (outputFileExtension ?? string.Empty).ToLowerInvariant()); - } + var filename = data.GetMD5().ToString("N") + outputFileExtension.ToLowerInvariant(); + var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; - return Path.Combine(folder, dataHash + (outputFileExtension ?? string.Empty).ToLowerInvariant()); + return Path.Combine(folder, filename); } - protected virtual bool EnableOutputInSubFolder => false; - - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - protected virtual string GetDefaultH264Preset() - { - return "superfast"; - } + protected virtual string GetDefaultH264Preset() => "superfast"; private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) { @@ -171,7 +162,6 @@ namespace MediaBrowser.Api.Playback var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken - }, cancellationTokenSource.Token).ConfigureAwait(false); EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl); @@ -209,22 +199,16 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var auth = AuthorizationContext.GetAuthorizationInfo(Request); - if (auth.User != null) + if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding) { - if (!auth.User.Policy.EnableVideoPlaybackTranscoding) - { - ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state); + ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state); - throw new ArgumentException("User does not have access to video transcoding"); - } + throw new ArgumentException("User does not have access to video transcoding"); } } var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); - var transcodingId = Guid.NewGuid().ToString("N"); - var commandLineArgs = GetCommandLineArguments(outputPath, encodingOptions, state, true); - var process = new Process() { StartInfo = new ProcessStartInfo() @@ -239,7 +223,7 @@ namespace MediaBrowser.Api.Playback RedirectStandardInput = true, FileName = MediaEncoder.EncoderPath, - Arguments = commandLineArgs, + Arguments = GetCommandLineArguments(outputPath, encodingOptions, state, true), WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory, ErrorDialog = false @@ -250,7 +234,7 @@ namespace MediaBrowser.Api.Playback var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, state.Request.PlaySessionId, state.MediaSource.LiveStreamId, - transcodingId, + Guid.NewGuid().ToString("N"), TranscodingJobType, process, state.Request.DeviceId, @@ -261,27 +245,26 @@ namespace MediaBrowser.Api.Playback Logger.LogInformation(commandLineLogMessage); var logFilePrefix = "ffmpeg-transcode"; - if (state.VideoRequest != null) + if (state.VideoRequest != null + && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { logFilePrefix = "ffmpeg-directstream"; } - else if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + else { logFilePrefix = "ffmpeg-remux"; } } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); - Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); + Stream logStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); + await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); @@ -298,13 +281,10 @@ namespace MediaBrowser.Api.Playback throw; } - // MUST read both stdout and stderr asynchronously or a deadlock may occurr - //process.BeginOutputReadLine(); - state.TranscodingJob = transcodingJob; // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream); + _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); // Wait for the file to exist before proceeeding while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) @@ -368,25 +348,16 @@ namespace MediaBrowser.Api.Playback Logger.LogDebug("Disposing stream resources"); state.Dispose(); - try + if (process.ExitCode == 0) { - Logger.LogInformation("FFMpeg exited with code {0}", process.ExitCode); + Logger.LogInformation("FFMpeg exited with code 0"); } - catch + else { - Logger.LogError("FFMpeg exited with an error."); + Logger.LogError("FFMpeg exited with code {0}", process.ExitCode); } - // This causes on exited to be called twice: - //try - //{ - // // Dispose the process - // process.Dispose(); - //} - //catch (Exception ex) - //{ - // Logger.LogError(ex, "Error disposing ffmpeg."); - //} + process.Dispose(); } /// @@ -643,11 +614,19 @@ namespace MediaBrowser.Api.Playback return null; } - if (value.IndexOf("npt=", StringComparison.OrdinalIgnoreCase) != 0) + if (!value.StartsWith("npt=", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid timeseek header"); } - value = value.Substring(4).Split(new[] { '-' }, 2)[0]; + int index = value.IndexOf('-'); + if (index == -1) + { + value = value.Substring(4); + } + else + { + value = value.Substring(4, index); + } if (value.IndexOf(':') == -1) { @@ -728,13 +707,10 @@ namespace MediaBrowser.Api.Playback // state.SegmentLength = 6; //} - if (state.VideoRequest != null) + if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) { - if (!string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) - { - state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); - } + state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(request.AudioCodec)) @@ -779,7 +755,7 @@ namespace MediaBrowser.Api.Playback var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false)).ToList(); mediaSource = string.IsNullOrEmpty(request.MediaSourceId) - ? mediaSources.First() + ? mediaSources[0] : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)); if (mediaSource == null && request.MediaSourceId.Equals(request.Id)) @@ -834,11 +810,11 @@ namespace MediaBrowser.Api.Playback if (state.OutputVideoBitrate.HasValue && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var resolution = ResolutionNormalizer.Normalize( - state.VideoStream == null ? (int?)null : state.VideoStream.BitRate, - state.VideoStream == null ? (int?)null : state.VideoStream.Width, - state.VideoStream == null ? (int?)null : state.VideoStream.Height, + state.VideoStream?.BitRate, + state.VideoStream?.Width, + state.VideoStream?.Height, state.OutputVideoBitrate.Value, - state.VideoStream == null ? null : state.VideoStream.Codec, + state.VideoStream?.Codec, state.OutputVideoCodec, videoRequest.MaxWidth, videoRequest.MaxHeight); @@ -846,17 +822,13 @@ namespace MediaBrowser.Api.Playback videoRequest.MaxWidth = resolution.MaxWidth; videoRequest.MaxHeight = resolution.MaxHeight; } - - ApplyDeviceProfileSettings(state); - } - else - { - ApplyDeviceProfileSettings(state); } + ApplyDeviceProfileSettings(state); + var ext = string.IsNullOrWhiteSpace(state.OutputContainer) ? GetOutputFileExtension(state) - : ("." + state.OutputContainer); + : ('.' + state.OutputContainer); var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); @@ -970,18 +942,18 @@ namespace MediaBrowser.Api.Playback responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode; responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*"; - if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase)) + if (state.RunTimeTicks.HasValue) { - if (state.RunTimeTicks.HasValue) + if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase)) { var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds; responseHeaders["MediaInfo.sec"] = string.Format("SEC_Duration={0};", Convert.ToInt32(ms).ToString(CultureInfo.InvariantCulture)); } - } - if (state.RunTimeTicks.HasValue && !isStaticallyStreamed && profile != null) - { - AddTimeSeekResponseHeaders(state, responseHeaders); + if (!isStaticallyStreamed && profile != null) + { + AddTimeSeekResponseHeaders(state, responseHeaders); + } } if (profile == null) diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 8d4b0cb3d0..ba3c2bfbaf 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -1,9 +1,7 @@ using System; -using System.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback @@ -12,6 +10,7 @@ namespace MediaBrowser.Api.Playback { private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; + private bool _disposed = false; public string RequestedUrl { get; set; } @@ -30,11 +29,6 @@ namespace MediaBrowser.Api.Playback public VideoStreamRequest VideoRequest => Request as VideoStreamRequest; - /// - /// Gets or sets the log file stream. - /// - /// The log file stream. - public Stream LogFileStream { get; set; } public IDirectStreamProvider DirectStreamProvider { get; set; } public string WaitForPath { get; set; } @@ -72,6 +66,7 @@ namespace MediaBrowser.Api.Playback { return 3; } + return 6; } @@ -94,6 +89,16 @@ namespace MediaBrowser.Api.Playback public string UserAgent { get; set; } + public bool EstimateContentLength { get; set; } + + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + public bool EnableDlnaHeaders { get; set; } + + public DeviceProfile DeviceProfile { get; set; } + + public TranscodingJob TranscodingJob { get; set; } + public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType) : base(transcodingType) { @@ -101,75 +106,41 @@ namespace MediaBrowser.Api.Playback _logger = logger; } - public bool EstimateContentLength { get; set; } - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - public bool EnableDlnaHeaders { get; set; } - - public override void Dispose() + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) { - DisposeTranscodingThrottler(); - DisposeLogStream(); - DisposeLiveStream(); - - TranscodingJob = null; + ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); } - private void DisposeTranscodingThrottler() + public void Dispose() { - if (TranscodingThrottler != null) - { - try - { - TranscodingThrottler.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing TranscodingThrottler"); - } - - TranscodingThrottler = null; - } + Dispose(true); + GC.SuppressFinalize(this); } - private void DisposeLogStream() + protected virtual void Dispose(bool disposing) { - if (LogFileStream != null) + if (_disposed) { - try - { - LogFileStream.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing log stream"); - } - - LogFileStream = null; + return; } - } - private async void DisposeLiveStream() - { - if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) + if (disposing) { - try - { - await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).ConfigureAwait(false); - } - catch (Exception ex) + // REVIEW: Is this the right place for this? + if (MediaSource.RequiresClosing + && string.IsNullOrWhiteSpace(Request.LiveStreamId) + && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) { - _logger.LogError(ex, "Error closing media source"); + _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); } + + TranscodingThrottler?.Dispose(); } - } - public DeviceProfile DeviceProfile { get; set; } + TranscodingThrottler = null; + TranscodingJob = null; - public TranscodingJob TranscodingJob; - public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) - { - ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); + _disposed = true; } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 916d691b80..34af3b1568 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -374,14 +374,14 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { if (AudioStream != null) { return AudioStream.SampleRate; } } - else if (BaseRequest.AudioSampleRate.HasValue) { // Don't exceed what the encoder supports @@ -397,7 +397,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { if (AudioStream != null) { @@ -405,13 +406,6 @@ namespace MediaBrowser.Controller.MediaEncoding } } - //else if (BaseRequest.AudioSampleRate.HasValue) - //{ - // // Don't exceed what the encoder supports - // // Seeing issues of attempting to encode to 88200 - // return Math.Min(44100, BaseRequest.AudioSampleRate.Value); - //} - return null; } } @@ -446,7 +440,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.BitDepth; } @@ -463,7 +458,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.RefFrames; } @@ -479,7 +475,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); } @@ -545,7 +542,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.CodecTag; } @@ -558,7 +556,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.IsAnamorphic; } @@ -571,14 +570,12 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var codec = OutputVideoCodec; - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.Codec; } - return codec; + return OutputVideoCodec; } } @@ -586,14 +583,12 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - var codec = OutputAudioCodec; - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return AudioStream?.Codec; } - return codec; + return OutputAudioCodec; } } @@ -601,7 +596,8 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static + || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { return VideoStream?.IsInterlaced; } @@ -636,6 +632,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); } + return GetMediaStreamCount(MediaStreamType.Video, 1); } } @@ -648,17 +645,12 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); } + return GetMediaStreamCount(MediaStreamType.Audio, 1); } } - public int HlsListSize - { - get - { - return 0; - } - } + public int HlsListSize => 0; private int? GetMediaStreamCount(MediaStreamType type, int limit) { @@ -677,10 +669,6 @@ namespace MediaBrowser.Controller.MediaEncoding { Progress.Report(percentComplete.Value); } - - public virtual void Dispose() - { - } } /// diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 2755bf5814..d0d5ebfd69 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using MediaBrowser.Model.Extensions; using Microsoft.Extensions.Logging; @@ -18,10 +19,11 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; } - public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) { try { + using (target) using (var reader = new StreamReader(source)) { while (!reader.EndOfStream && reader.BaseStream.CanRead) @@ -97,8 +99,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var currentMs = startMs + val.TotalMilliseconds; - var percentVal = currentMs / totalMs; - percent = 100 * percentVal; + percent = 100 * currentMs / totalMs; transcodingPosition = val; } diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 901d81c5ff..e52951dd08 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -13,7 +13,8 @@ namespace MediaBrowser.Model.Dlna _profile = profile; } - public string BuildImageHeader(string container, + public string BuildImageHeader( + string container, int? width, int? height, bool isDirectStream, @@ -28,8 +29,7 @@ namespace MediaBrowser.Model.Dlna DlnaFlags.InteractiveTransferMode | DlnaFlags.DlnaV15; - string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); + string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container, width, @@ -37,7 +37,7 @@ namespace MediaBrowser.Model.Dlna if (string.IsNullOrEmpty(orgPn)) { - orgPn = mediaProfile == null ? null : mediaProfile.OrgPn; + orgPn = mediaProfile?.OrgPn; } if (string.IsNullOrEmpty(orgPn)) @@ -50,7 +50,8 @@ namespace MediaBrowser.Model.Dlna return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); } - public string BuildAudioHeader(string container, + public string BuildAudioHeader( + string container, string audioCodec, int? audioBitrate, int? audioSampleRate, @@ -102,7 +103,8 @@ namespace MediaBrowser.Model.Dlna return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); } - public List BuildVideoHeader(string container, + public List BuildVideoHeader( + string container, string videoCodec, string audioCodec, int? width, @@ -206,7 +208,7 @@ namespace MediaBrowser.Model.Dlna return contentFeatureList; } - private string GetImageOrgPnValue(string container, int? width, int? height) + private static string GetImageOrgPnValue(string container, int? width, int? height) { MediaFormatProfile? format = new MediaFormatProfileResolver() .ResolveImageFormat(container, @@ -216,7 +218,7 @@ namespace MediaBrowser.Model.Dlna return format.HasValue ? format.Value.ToString() : null; } - private string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) + private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) { MediaFormatProfile? format = new MediaFormatProfileResolver() .ResolveAudioFormat(container, @@ -227,7 +229,7 @@ namespace MediaBrowser.Model.Dlna return format.HasValue ? format.Value.ToString() : null; } - private string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) + private static string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) { return new MediaFormatProfileResolver().ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp); } From ca37ca291fb06e8b46257ae1dbc0f2786c948d36 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 26 Mar 2019 23:04:53 +0100 Subject: [PATCH 07/91] More style changes --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 6 +++--- MediaBrowser.Api/Playback/StreamState.cs | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index b480d055af..674f3170e4 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -686,7 +686,7 @@ namespace MediaBrowser.Api.Playback var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) /*|| string.Equals(Request.Headers.Get("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase)*/; - var state = new StreamState(MediaSourceManager, Logger, TranscodingJobType) + var state = new StreamState(MediaSourceManager, TranscodingJobType) { Request = request, RequestedUrl = url, @@ -756,11 +756,11 @@ namespace MediaBrowser.Api.Playback mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources[0] - : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)); + : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId)); if (mediaSource == null && request.MediaSourceId.Equals(request.Id)) { - mediaSource = mediaSources.First(); + mediaSource = mediaSources[0]; } } } diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index ba3c2bfbaf..7396b5c99b 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -2,13 +2,11 @@ using System; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback { public class StreamState : EncodingJobInfo, IDisposable { - private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; private bool _disposed = false; @@ -99,11 +97,10 @@ namespace MediaBrowser.Api.Playback public TranscodingJob TranscodingJob { get; set; } - public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType) + public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType) : base(transcodingType) { _mediaSourceManager = mediaSourceManager; - _logger = logger; } public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) From 157a86d0f139d149083036e6ea962e0fd7d77057 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 12:43:46 +0100 Subject: [PATCH 08/91] Remove dead code --- .../Services/ServiceHandler.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 243d2cca27..621be4fcb5 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.Services { public class ServiceHandler { - private readonly ServiceController _serviceController; - public RestPath RestPath { get; } public string ResponseContentType { get; } @@ -28,22 +26,12 @@ namespace Emby.Server.Implementations.Services if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) { var deserializer = RequestHelper.GetRequestReader(host, contentType); - if (deserializer != null) - { - return deserializer(requestType, httpReq.InputStream); - } + return deserializer?.Invoke(requestType, httpReq.InputStream); } return Task.FromResult(host.CreateInstance(requestType)); } - public RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) - { - pathInfo = GetSanitizedPathInfo(pathInfo, out contentType); - - return _serviceController.GetRestPathForRequest(httpMethod, pathInfo); - } - public static string GetSanitizedPathInfo(string pathInfo, out string contentType) { contentType = null; From 8ed5d154b746c086ccb6b1163772ea4d05067abd Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:07:08 +0100 Subject: [PATCH 09/91] Remove duplicate code --- .../Session/SessionManager.cs | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index dc23551db5..b7974dd7e8 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1037,6 +1037,25 @@ namespace Emby.Server.Implementations.Session } } + private static Task SendMessageToSessions(IEnumerable sessions, string name, T data, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + foreach (var session in sessions) + { + var controllers = session.SessionControllers; + var messageId = Guid.NewGuid().ToString("N"); + + foreach (var controller in controllers) + { + yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); + } + } + } + + return Task.WhenAll(GetTasks()); + } + public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) { CheckDisposed(); @@ -1832,15 +1851,7 @@ namespace Emby.Server.Implementations.Session var data = dataFn(); - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } public Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken) @@ -1848,16 +1859,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)); - - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } public Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken) @@ -1865,16 +1867,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } public Task SendMessageToUserDeviceAndAdminSessions(string deviceId, string name, T data, CancellationToken cancellationToken) @@ -1883,16 +1876,7 @@ namespace Emby.Server.Implementations.Session var sessions = Sessions .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)); - - IEnumerable GetTasks() - { - foreach (var session in sessions) - { - yield return SendMessageToSession(session, name, data, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); + return SendMessageToSessions(sessions, name, data, cancellationToken); } private bool IsAdminSession(SessionInfo s) From 6c0e2e249ddbab24c67b655b701e473d9343c2ef Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:13:36 +0100 Subject: [PATCH 10/91] Even more duplicate code removed --- .../Session/SessionManager.cs | 56 ++----------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b7974dd7e8..6b9573b1dd 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1242,12 +1242,13 @@ namespace Emby.Server.Implementations.Session return SendMessageToSession(session, "Playstate", command, cancellationToken); } - private void AssertCanControl(SessionInfo session, SessionInfo controllingSession) + private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession) { if (session == null) { throw new ArgumentNullException(nameof(session)); } + if (controllingSession == null) { throw new ArgumentNullException(nameof(controllingSession)); @@ -1259,26 +1260,11 @@ namespace Emby.Server.Implementations.Session /// /// The cancellation token. /// Task. - public async Task SendRestartRequiredNotification(CancellationToken cancellationToken) + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) { CheckDisposed(); - var sessions = Sessions.ToList(); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await SendMessageToSession(session, "RestartRequired", string.Empty, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError("Error in SendRestartRequiredNotification.", ex); - } - - }, cancellationToken)).ToArray(); - - await Task.WhenAll(tasks).ConfigureAwait(false); + return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken); } /// @@ -1290,22 +1276,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - var sessions = Sessions.ToList(); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await SendMessageToSession(session, "ServerShuttingDown", string.Empty, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError("Error in SendServerShutdownNotification.", ex); - } - - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken); } /// @@ -1319,22 +1290,7 @@ namespace Emby.Server.Implementations.Session _logger.LogDebug("Beginning SendServerRestartNotification"); - var sessions = Sessions.ToList(); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await SendMessageToSession(session, "ServerRestarting", string.Empty, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError("Error in SendServerRestartNotification.", ex); - } - - }, cancellationToken)).ToArray(); - - return Task.WhenAll(tasks); + return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken); } /// From b647959ec41373c513ee55075e4c973bfb68dbcd Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:26:33 +0100 Subject: [PATCH 11/91] Add EnableOutputInSubFolder back --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 12 ++++++++++-- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 674f3170e4..cde6f725aa 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -32,6 +32,8 @@ namespace MediaBrowser.Api.Playback { protected static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); + protected virtual bool EnableOutputInSubFolder => false; + /// /// Gets or sets the application paths. /// @@ -142,10 +144,16 @@ namespace MediaBrowser.Api.Playback data += "-" + (state.Request.DeviceId ?? string.Empty) + "-" + (state.Request.PlaySessionId ?? string.Empty); - var filename = data.GetMD5().ToString("N") + outputFileExtension.ToLowerInvariant(); + var filename = data.GetMD5().ToString("N"); + var ext = outputFileExtension.ToLowerInvariant(); var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; - return Path.Combine(folder, filename); + if (EnableOutputInSubFolder) + { + return Path.Combine(folder, filename, filename + ext); + } + + return Path.Combine(folder, filename + ext); } protected virtual string GetDefaultH264Preset() => "superfast"; diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index d0d5ebfd69..ac989f6ba1 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var currentMs = startMs + val.TotalMilliseconds; - percent = 100 * currentMs / totalMs; + percent = 100.0 * currentMs / totalMs; transcodingPosition = val; } From b69b19ddce63651306bde494adfa656fd658b230 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:28:52 +0100 Subject: [PATCH 12/91] Move messageId out of outer loop --- Emby.Server.Implementations/Session/SessionManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6b9573b1dd..53ed5fc223 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1041,11 +1041,10 @@ namespace Emby.Server.Implementations.Session { IEnumerable GetTasks() { + var messageId = Guid.NewGuid().ToString("N"); foreach (var session in sessions) { var controllers = session.SessionControllers; - var messageId = Guid.NewGuid().ToString("N"); - foreach (var controller in controllers) { yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); From be86ea29828a696aedbbdc21f83ea4222e68b316 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 27 Mar 2019 16:34:56 +0100 Subject: [PATCH 13/91] Update MediaBrowser.Common/Net/IHttpClient.cs Co-Authored-By: Bond-009 --- MediaBrowser.Common/Net/IHttpClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index d332ab2000..db69c6f2cd 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Common.Net Task Get(HttpRequestOptions options); /// - /// Warning: Depricated function, + /// Warning: Deprecated function, /// use 'Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead /// Sends the asynchronous. /// From 9aaeb19418616283de00cdb0d00699ab93d84415 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 27 Mar 2019 16:31:26 +0100 Subject: [PATCH 14/91] Self-documenting code --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index cde6f725aa..efbb17fd2a 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -622,18 +622,19 @@ namespace MediaBrowser.Api.Playback return null; } - if (!value.StartsWith("npt=", StringComparison.OrdinalIgnoreCase)) + const string Npt = "npt="; + if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid timeseek header"); } int index = value.IndexOf('-'); if (index == -1) { - value = value.Substring(4); + value = value.Substring(Npt.Length); } else { - value = value.Substring(4, index); + value = value.Substring(Npt.Length, index); } if (value.IndexOf(':') == -1) From 41df562419d8f1681a9720ab1c62ffb9ad0f96cb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 28 Mar 2019 23:19:56 +0100 Subject: [PATCH 15/91] Improve IO code * Style changes * Remove remnants of SMB support * Use `GetInvalidFileNameChars` instead of rolling our own * Remove possible unexpected behaviour with async file streams * Remove some dead code --- .../IO/FileRefresher.cs | 6 +- .../IO/LibraryMonitor.cs | 125 +++--- .../IO/ManagedFileSystem.cs | 68 ++-- .../IO/StreamHelper.cs | 74 +--- .../IO/ThrottledStream.cs | 355 ------------------ 5 files changed, 82 insertions(+), 546 deletions(-) delete mode 100644 Emby.Server.Implementations/IO/ThrottledStream.cs diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 73242d0ade..40e8ed5dc7 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -6,10 +6,6 @@ using System.Threading; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO { AddAffectedPath(path); } + RestartTimer(); } @@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO AddAffectedPath(affectedFile); } } + RestartTimer(); } diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index df4dc41b99..aeb541c540 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; -using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations.IO { @@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO /// The file system watchers /// private readonly ConcurrentDictionary _fileSystemWatchers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + /// /// The affected paths /// @@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(path)); } - // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. + // This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata await Task.Delay(45000).ConfigureAwait(false); @@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO public void Start() { - LibraryManager.ItemAdded += LibraryManager_ItemAdded; - LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; + LibraryManager.ItemAdded += OnLibraryManagerItemAdded; + LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved; - var pathsToWatch = new List { }; + var pathsToWatch = new List(); var paths = LibraryManager .RootFolder @@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO return lst.Any(str => { - //this should be a little quicker than examining each actual parent folder... + // this should be a little quicker than examining each actual parent folder... var compare = str.TrimEnd(Path.DirectorySeparatorChar); return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); @@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO if (!Directory.Exists(path)) { // Seeing a crash in the mono runtime due to an exception being thrown on a different thread - Logger.LogInformation("Skipping realtime monitor for {0} because the path does not exist", path); + Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path); return; } - if (OperatingSystem.Id != OperatingSystemId.Windows) - { - if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase)) - { - // not supported - return; - } - } - // Already being watched if (_fileSystemWatchers.ContainsKey(path)) { @@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO { var newWatcher = new FileSystemWatcher(path, "*") { - IncludeSubdirectories = true + IncludeSubdirectories = true, + InternalBufferSize = 65536, + NotifyFilter = NotifyFilters.CreationTime | + NotifyFilters.DirectoryName | + NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size | + NotifyFilters.Attributes }; - newWatcher.InternalBufferSize = 65536; - - newWatcher.NotifyFilter = NotifyFilters.CreationTime | - NotifyFilters.DirectoryName | - NotifyFilters.FileName | - NotifyFilters.LastWrite | - NotifyFilters.Size | - NotifyFilters.Attributes; - - newWatcher.Created += watcher_Changed; - newWatcher.Deleted += watcher_Changed; - newWatcher.Renamed += watcher_Changed; - newWatcher.Changed += watcher_Changed; - newWatcher.Error += watcher_Error; + newWatcher.Created += OnWatcherChanged; + newWatcher.Deleted += OnWatcherChanged; + newWatcher.Renamed += OnWatcherChanged; + newWatcher.Changed += OnWatcherChanged; + newWatcher.Error += OnWatcherError; if (_fileSystemWatchers.TryAdd(path, newWatcher)) { @@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO { using (watcher) { - Logger.LogInformation("Stopping directory watching for path {path}", watcher.Path); + Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path); - watcher.Created -= watcher_Changed; - watcher.Deleted -= watcher_Changed; - watcher.Renamed -= watcher_Changed; - watcher.Changed -= watcher_Changed; - watcher.Error -= watcher_Error; + watcher.Created -= OnWatcherChanged; + watcher.Deleted -= OnWatcherChanged; + watcher.Renamed -= OnWatcherChanged; + watcher.Changed -= OnWatcherChanged; + watcher.Error -= OnWatcherError; - try - { - watcher.EnableRaisingEvents = false; - } - catch (InvalidOperationException) - { - // Seeing this under mono on linux sometimes - // Collection was modified; enumeration operation may not execute. - } + watcher.EnableRaisingEvents = false; } - } - catch (NotImplementedException) - { - // the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android - } - catch - { - } finally { @@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO /// The watcher. private void RemoveWatcherFromList(FileSystemWatcher watcher) { - _fileSystemWatchers.TryRemove(watcher.Path, out var removed); + _fileSystemWatchers.TryRemove(watcher.Path, out _); } /// @@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void watcher_Error(object sender, ErrorEventArgs e) + private void OnWatcherError(object sender, ErrorEventArgs e) { var ex = e.GetException(); var dw = (FileSystemWatcher)sender; - Logger.LogError(ex, "Error in Directory watcher for: {path}", dw.Path); + Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); DisposeWatcher(dw, true); } @@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO /// /// The source of the event. /// The instance containing the event data. - void watcher_Changed(object sender, FileSystemEventArgs e) + private void OnWatcherChanged(object sender, FileSystemEventArgs e) { try { - //logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); - - var path = e.FullPath; - - ReportFileSystemChanged(path); + ReportFileSystemChanged(e.FullPath); } catch (Exception ex) { @@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO { if (_fileSystem.AreEqual(i, path)) { - Logger.LogDebug("Ignoring change to {path}", path); + Logger.LogDebug("Ignoring change to {Path}", path); return true; } if (_fileSystem.ContainsSubPath(i, path)) { - Logger.LogDebug("Ignoring change to {path}", path); + Logger.LogDebug("Ignoring change to {Path}", path); return true; } // Go up a level var parent = Path.GetDirectoryName(i); - if (!string.IsNullOrEmpty(parent)) + if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path)) { - if (_fileSystem.AreEqual(parent, path)) - { - Logger.LogDebug("Ignoring change to {path}", path); - return true; - } + Logger.LogDebug("Ignoring change to {Path}", path); + return true; } return false; @@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO lock (_activeRefreshers) { - var refreshers = _activeRefreshers.ToList(); - foreach (var refresher in refreshers) + foreach (var refresher in _activeRefreshers) { // Path is already being refreshed if (_fileSystem.AreEqual(path, refresher.Path)) @@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO /// public void Stop() { - LibraryManager.ItemAdded -= LibraryManager_ItemAdded; - LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; + LibraryManager.ItemAdded -= OnLibraryManagerItemAdded; + LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved; foreach (var watcher in _fileSystemWatchers.Values.ToList()) { @@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO { refresher.Dispose(); } + _activeRefreshers.Clear(); } } - private bool _disposed; + private bool _disposed = false; + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 47cea7269d..4b5cfe3b9f 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -19,8 +19,6 @@ namespace Emby.Server.Implementations.IO { protected ILogger Logger; - private readonly bool _supportsAsyncFileStreams; - private char[] _invalidFileNameChars; private readonly List _shortcutHandlers = new List(); private readonly string _tempPath; @@ -32,11 +30,8 @@ namespace Emby.Server.Implementations.IO IApplicationPaths applicationPaths) { Logger = loggerFactory.CreateLogger("FileSystem"); - _supportsAsyncFileStreams = true; _tempPath = applicationPaths.TempDirectory; - SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows); - _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; } @@ -45,20 +40,6 @@ namespace Emby.Server.Implementations.IO _shortcutHandlers.Add(handler); } - protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars) - { - if (enableManagedInvalidFileNameChars) - { - _invalidFileNameChars = Path.GetInvalidFileNameChars(); - } - else - { - // Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions - // https://referencesource.microsoft.com/#mscorlib/system/io/path.cs - _invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' }; - } - } - /// /// Determines whether the specified filename is shortcut. /// @@ -92,20 +73,25 @@ namespace Emby.Server.Implementations.IO var extension = Path.GetExtension(filename); var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); - if (handler != null) - { - return handler.Resolve(filename); - } - - return null; + return handler?.Resolve(filename); } public virtual string MakeAbsolutePath(string folderPath, string filePath) { - if (string.IsNullOrWhiteSpace(filePath)) return filePath; + if (string.IsNullOrWhiteSpace(filePath)) + { + return filePath; + } + + if (filePath.Contains("://")) + { + return filePath; // stream + } - if (filePath.Contains(@"://")) return filePath; //stream - if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path + if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') + { + return filePath; // absolute local path + } // unc path if (filePath.StartsWith("\\\\")) @@ -125,9 +111,7 @@ namespace Emby.Server.Implementations.IO } try { - string path = System.IO.Path.Combine(folderPath, filePath); - path = System.IO.Path.GetFullPath(path); - return path; + return Path.Combine(Path.GetFullPath(folderPath), filePath); } catch (ArgumentException) { @@ -166,7 +150,7 @@ namespace Emby.Server.Implementations.IO } var extension = Path.GetExtension(shortcutPath); - var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); if (handler != null) { @@ -244,12 +228,13 @@ namespace Emby.Server.Implementations.IO private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info) { - var result = new FileSystemMetadata(); - - result.Exists = info.Exists; - result.FullName = info.FullName; - result.Extension = info.Extension; - result.Name = info.Name; + var result = new FileSystemMetadata + { + Exists = info.Exists, + FullName = info.FullName, + Extension = info.Extension, + Name = info.Name + }; if (result.Exists) { @@ -260,8 +245,7 @@ namespace Emby.Server.Implementations.IO // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; //} - var fileInfo = info as FileInfo; - if (fileInfo != null) + if (info is FileInfo fileInfo) { result.Length = fileInfo.Length; result.DirectoryName = fileInfo.DirectoryName; @@ -307,7 +291,7 @@ namespace Emby.Server.Implementations.IO { var builder = new StringBuilder(filename); - foreach (var c in _invalidFileNameChars) + foreach (var c in Path.GetInvalidFileNameChars()) { builder = builder.Replace(c, ' '); } @@ -394,7 +378,7 @@ namespace Emby.Server.Implementations.IO /// FileStream. public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) { - if (_supportsAsyncFileStreams && isAsync) + if (isAsync) { return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous); } diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index d02cd84a03..7c8c079e39 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO try { int read; - while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { cancellationToken.ThrowIfCancellationRequested(); - await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); if (onStarted != null) { @@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO if (emptyReadLimit <= 0) { int read; - while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { cancellationToken.ThrowIfCancellationRequested(); - await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); } return; @@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO { cancellationToken.ThrowIfCancellationRequested(); - var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) { @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO { eofCount = 0; - await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); } } } @@ -109,64 +109,6 @@ namespace Emby.Server.Implementations.IO } } - public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); - try - { - int bytesRead; - int totalBytesRead = 0; - - while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0) - { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - totalBytesRead += bytesRead; - } - } - - return totalBytesRead; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); - try - { - int bytesRead; - - while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0) - { - var bytesToWrite = Math.Min(bytesRead, copyLength); - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - } - - copyLength -= bytesToWrite; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); @@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO if (bytesRead == 0) { - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } } @@ -225,7 +167,7 @@ namespace Emby.Server.Implementations.IO while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { - await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; } diff --git a/Emby.Server.Implementations/IO/ThrottledStream.cs b/Emby.Server.Implementations/IO/ThrottledStream.cs deleted file mode 100644 index 81e8abc983..0000000000 --- a/Emby.Server.Implementations/IO/ThrottledStream.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.IO -{ - /// - /// Class for streaming data with throttling support. - /// - public class ThrottledStream : Stream - { - /// - /// A constant used to specify an infinite number of bytes that can be transferred per second. - /// - public const long Infinite = 0; - - #region Private members - /// - /// The base stream. - /// - private readonly Stream _baseStream; - - /// - /// The maximum bytes per second that can be transferred through the base stream. - /// - private long _maximumBytesPerSecond; - - /// - /// The number of bytes that has been transferred since the last throttle. - /// - private long _byteCount; - - /// - /// The start time in milliseconds of the last throttle. - /// - private long _start; - #endregion - - #region Properties - /// - /// Gets the current milliseconds. - /// - /// The current milliseconds. - protected long CurrentMilliseconds => Environment.TickCount; - - /// - /// Gets or sets the maximum bytes per second that can be transferred through the base stream. - /// - /// The maximum bytes per second. - public long MaximumBytesPerSecond - { - get => _maximumBytesPerSecond; - set - { - if (MaximumBytesPerSecond != value) - { - _maximumBytesPerSecond = value; - Reset(); - } - } - } - - /// - /// Gets a value indicating whether the current stream supports reading. - /// - /// true if the stream supports reading; otherwise, false. - public override bool CanRead => _baseStream.CanRead; - - /// - /// Gets a value indicating whether the current stream supports seeking. - /// - /// - /// true if the stream supports seeking; otherwise, false. - public override bool CanSeek => _baseStream.CanSeek; - - /// - /// Gets a value indicating whether the current stream supports writing. - /// - /// - /// true if the stream supports writing; otherwise, false. - public override bool CanWrite => _baseStream.CanWrite; - - /// - /// Gets the length in bytes of the stream. - /// - /// - /// A long value representing the length of the stream in bytes. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Length => _baseStream.Length; - - /// - /// Gets or sets the position within the current stream. - /// - /// - /// The current position within the stream. - /// An I/O error occurs. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Position - { - get => _baseStream.Position; - set => _baseStream.Position = value; - } - #endregion - - public long MinThrottlePosition; - - #region Ctor - /// - /// Initializes a new instance of the class. - /// - /// The base stream. - /// The maximum bytes per second that can be transferred through the base stream. - /// Thrown when is a null reference. - /// Thrown when is a negative value. - public ThrottledStream(Stream baseStream, long maximumBytesPerSecond) - { - if (baseStream == null) - { - throw new ArgumentNullException(nameof(baseStream)); - } - - if (maximumBytesPerSecond < 0) - { - throw new ArgumentOutOfRangeException(nameof(maximumBytesPerSecond), - maximumBytesPerSecond, "The maximum number of bytes per second can't be negative."); - } - - _baseStream = baseStream; - _maximumBytesPerSecond = maximumBytesPerSecond; - _start = CurrentMilliseconds; - _byteCount = 0; - } - #endregion - - #region Public methods - /// - /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. - /// - /// An I/O error occurs. - public override void Flush() - { - _baseStream.Flush(); - } - - /// - /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. - /// - /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. - /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. - /// The maximum number of bytes to be read from the current stream. - /// - /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. - /// - /// The sum of offset and count is larger than the buffer length. - /// Methods were called after the stream was closed. - /// The base stream does not support reading. - /// buffer is null. - /// An I/O error occurs. - /// offset or count is negative. - public override int Read(byte[] buffer, int offset, int count) - { - Throttle(count); - - return _baseStream.Read(buffer, offset, count); - } - - /// - /// Sets the position within the current stream. - /// - /// A byte offset relative to the origin parameter. - /// A value of type indicating the reference point used to obtain the new position. - /// - /// The new position within the current stream. - /// - /// An I/O error occurs. - /// The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. - /// Methods were called after the stream was closed. - public override long Seek(long offset, SeekOrigin origin) - { - return _baseStream.Seek(offset, origin); - } - - /// - /// Sets the length of the current stream. - /// - /// The desired length of the current stream in bytes. - /// The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. - /// An I/O error occurs. - /// Methods were called after the stream was closed. - public override void SetLength(long value) - { - _baseStream.SetLength(value); - } - - private long _bytesWritten; - - /// - /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. - /// - /// An array of bytes. This method copies count bytes from buffer to the current stream. - /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. - /// The number of bytes to be written to the current stream. - /// An I/O error occurs. - /// The base stream does not support writing. - /// Methods were called after the stream was closed. - /// buffer is null. - /// The sum of offset and count is greater than the buffer length. - /// offset or count is negative. - public override void Write(byte[] buffer, int offset, int count) - { - Throttle(count); - - _baseStream.Write(buffer, offset, count); - - _bytesWritten += count; - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - await ThrottleAsync(count, cancellationToken).ConfigureAwait(false); - - await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - _bytesWritten += count; - } - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return _baseStream.ToString(); - } - #endregion - - private bool ThrottleCheck(int bufferSizeInBytes) - { - if (_bytesWritten < MinThrottlePosition) - { - return false; - } - - // Make sure the buffer isn't empty. - if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) - { - return false; - } - - return true; - } - - #region Protected methods - /// - /// Throttles for the specified buffer size in bytes. - /// - /// The buffer size in bytes. - protected void Throttle(int bufferSizeInBytes) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - try - { - // The time to sleep is more then a millisecond, so sleep. - var task = Task.Delay(toSleep); - Task.WaitAll(task); - } - catch - { - // Eatup ThreadAbortException. - } - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - // The time to sleep is more then a millisecond, so sleep. - await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false); - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - /// - /// Will reset the bytecount to 0 and reset the start time to the current time. - /// - protected void Reset() - { - long difference = CurrentMilliseconds - _start; - - // Only reset counters when a known history is available of more then 1 second. - if (difference > 1000) - { - _byteCount = 0; - _start = CurrentMilliseconds; - } - } - #endregion - } -} From f911fda34fdc1af76dc475c5af042ff6b44262ab Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 29 Mar 2019 20:34:42 +0100 Subject: [PATCH 16/91] Merge ifs --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 4b5cfe3b9f..0dea5041a1 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -78,16 +78,13 @@ namespace Emby.Server.Implementations.IO public virtual string MakeAbsolutePath(string folderPath, string filePath) { - if (string.IsNullOrWhiteSpace(filePath)) + if (string.IsNullOrWhiteSpace(filePath) + // stream + || filePath.Contains("://")) { return filePath; } - if (filePath.Contains("://")) - { - return filePath; // stream - } - if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') { return filePath; // absolute local path From f2e2065fd4a879c8583f487e161e09909ed704b2 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 31 Mar 2019 15:24:18 +0200 Subject: [PATCH 17/91] Remove unused dependency for Emby.Naming --- Emby.Naming/Emby.Naming.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index e344e7811e..c448ec0ce6 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -10,7 +10,7 @@ - + From 2f33e99006fd479ac9134ede80cbb990948d1261 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 2 Apr 2019 18:17:50 +0200 Subject: [PATCH 18/91] Speed up DeepCopy --- .../Entities/BaseItemExtensions.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 9c955a7247..815239be24 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -64,21 +64,31 @@ namespace MediaBrowser.Controller.Entities where T : BaseItem where TU : BaseItem { - var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList(); - var destProps = typeof(TU).GetProperties() - .Where(x => x.CanWrite) - .ToList(); + var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList(); - foreach (var sourceProp in sourceProps) + foreach (var sourceProp in typeof(T).GetProperties()) { - if (destProps.Any(x => x.Name == sourceProp.Name)) + // We should be able to write to the property + // for both the source and destination type + // This is only false when the derived type hides the base member + // (which we shouldn't copy anyway) + if (!sourceProp.CanRead || !sourceProp.CanWrite) { - var p = destProps.First(x => x.Name == sourceProp.Name); - p.SetValue(dest, sourceProp.GetValue(source, null), null); + continue; } - } + var v = sourceProp.GetValue(source); + if (v == null) + { + continue; + } + var p = destProps.Find(x => x.Name == sourceProp.Name); + if (p != null) + { + p.SetValue(dest, v); + } + } } /// @@ -93,7 +103,5 @@ namespace MediaBrowser.Controller.Entities source.DeepCopy(dest); return dest; } - - } } From f96d1e9e690f2f0e83d153e4ae8c2f4d810e7e2e Mon Sep 17 00:00:00 2001 From: DrPandemic Date: Sun, 7 Apr 2019 22:26:27 -0400 Subject: [PATCH 19/91] Fix README documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f635bdd25..6a206eacf6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ For more information about the project, please see our [about page](https://jell

Want to get started? -Choose from Prebuilt Packages or Build from Source, then see our first-time setup guide. +Choose from Prebuilt Packages or Build from Source, then see our quick start guide.

Want to contribute? From c72393c9704d693b3beee51ebd35da0f68d13261 Mon Sep 17 00:00:00 2001 From: Terror-Gene Date: Mon, 8 Apr 2019 14:56:42 +0930 Subject: [PATCH 20/91] Updated Unraid Docker icon Logo was set to use emby, but binhex has since added the jellyfin logo. --- deployment/unraid/docker-templates/jellyfin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index 1d97a9f00f..5a5b4bb5c7 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -46,6 +46,6 @@ http://[IP]:[PORT:8096]/ - https://raw.githubusercontent.com/binhex/docker-templates/master/binhex/images/emby-icon.png + https://raw.githubusercontent.com/binhex/docker-templates/master/binhex/images/jellyfin-icon.png From 7b4e16bb8ffaa95619a5be0f231e163da6747665 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Tue, 9 Apr 2019 00:43:25 +0200 Subject: [PATCH 21/91] Disabled dotnet_compat part of pipeline. --- .ci/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 7a92d40889..b3389caba9 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -99,7 +99,7 @@ jobs: pool: vmImage: ubuntu-16.04 dependsOn: main_build - condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) + condition: false #and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) strategy: matrix: Naming: From a1d50a6d055c414c95fa0ea2bf58db79afb6dc74 Mon Sep 17 00:00:00 2001 From: Ulysse <5031221+voodoos@users.noreply.github.com> Date: Tue, 9 Apr 2019 20:19:27 +0200 Subject: [PATCH 22/91] Clean `WebSocketSharpRequest.PathInfo` (#1212) * rm useless ResolvePathInfoFromMappedPath method * rm useless NormalizePathInfo method * Use request.Path instead of RawUrl * Removing unused `HandlerFactoryPath` field * Use an expression body definition and rm field `pathInfo` * More (syntactic) sugar * Who needs blocks in cases ? --- .../SocketSharp/WebSocketSharpRequest.cs | 114 +----------------- 1 file changed, 1 insertion(+), 113 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index e0a0ee2861..792615a0f6 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -25,8 +25,6 @@ namespace Emby.Server.Implementations.SocketSharp this.OperationName = operationName; this.request = httpContext; this.Response = new WebSocketSharpResponse(logger, response); - - // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); } public HttpRequest HttpRequest => request; @@ -100,7 +98,6 @@ namespace Emby.Server.Implementations.SocketSharp switch (crlf) { case 0: - { if (c == '\r') { crlf = 1; @@ -117,10 +114,8 @@ namespace Emby.Server.Implementations.SocketSharp } break; - } case 1: - { if (c == '\n') { crlf = 2; @@ -128,10 +123,8 @@ namespace Emby.Server.Implementations.SocketSharp } throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - } case 2: - { if (c == ' ' || c == '\t') { crlf = 0; @@ -139,7 +132,6 @@ namespace Emby.Server.Implementations.SocketSharp } throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - } } } @@ -312,97 +304,7 @@ namespace Emby.Server.Implementations.SocketSharp return pos == -1 ? strVal : strVal.Slice(0, pos); } - public static string HandlerFactoryPath; - - private string pathInfo; - public string PathInfo - { - get - { - if (this.pathInfo == null) - { - var mode = HandlerFactoryPath; - - var pos = RawUrl.IndexOf("?", StringComparison.Ordinal); - if (pos != -1) - { - var path = RawUrl.Substring(0, pos); - this.pathInfo = GetPathInfo( - path, - mode, - mode ?? string.Empty); - } - else - { - this.pathInfo = RawUrl; - } - - this.pathInfo = WebUtility.UrlDecode(pathInfo); - this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString(); - } - - return this.pathInfo; - } - } - - private static string GetPathInfo(string fullPath, string mode, string appPath) - { - var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode); - if (!string.IsNullOrEmpty(pathInfo)) - { - return pathInfo; - } - - // Wildcard mode relies on this to work out the handlerPath - pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath); - if (!string.IsNullOrEmpty(pathInfo)) - { - return pathInfo; - } - - return fullPath; - } - - private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot) - { - if (mappedPathRoot == null) - { - return null; - } - - var sbPathInfo = new StringBuilder(); - var fullPathParts = fullPath.Split('/'); - var mappedPathRootParts = mappedPathRoot.Split('/'); - var fullPathIndexOffset = mappedPathRootParts.Length - 1; - var pathRootFound = false; - - for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++) - { - if (pathRootFound) - { - sbPathInfo.Append("/" + fullPathParts[fullPathIndex]); - } - else if (fullPathIndex - fullPathIndexOffset >= 0) - { - pathRootFound = true; - for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++) - { - if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase)) - { - pathRootFound = false; - break; - } - } - } - } - - if (!pathRootFound) - { - return null; - } - - return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/"; - } + public string PathInfo => this.request.Path.Value; public string UserAgent => request.Headers[HeaderNames.UserAgent]; @@ -500,19 +402,5 @@ namespace Emby.Server.Implementations.SocketSharp return httpFiles; } } - - public static ReadOnlySpan NormalizePathInfo(string pathInfo, string handlerPath) - { - if (handlerPath != null) - { - var trimmed = pathInfo.AsSpan().TrimStart('/'); - if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return trimmed.Slice(handlerPath.Length).ToString().AsSpan(); - } - } - - return pathInfo.AsSpan(); - } } } From f888c4b641218557265148c318074a227a79df9e Mon Sep 17 00:00:00 2001 From: Terror-Gene Date: Thu, 11 Apr 2019 03:19:05 +0930 Subject: [PATCH 23/91] Fix missing Unraid cache mount Cache folder was not mounted outside of the Docker image since its separation from the config folder. Config HostDir was only updated for consistency, previous directory was overridden by unraid into the appdata/appname folder anyway. Name capitalization was corrected as this is only used by new installations & does not affect current installations/updates. --- deployment/unraid/docker-templates/jellyfin.xml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index 5a5b4bb5c7..eec9967bbe 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -3,14 +3,15 @@ https://raw.githubusercontent.com/jellyfin/jellyfin/deployment/unraid/docker-templates/jellyfin.xml False MediaApp:Video MediaApp:Music MediaApp:Photos MediaServer:Video MediaServer:Music MediaServer:Photos - JellyFin + Jellyfin - JellyFin is The Free Software Media Browser Converted By Community Applications Always verify this template (and values) against the dockerhub support page for the container!![br][br] + Jellyfin is The Free Software Media Browser Converted By Community Applications Always verify this template (and values) against the dockerhub support page for the container!![br][br] You can add as many mount points as needed for recordings, movies ,etc. [br][br] [b][span style='color: #E80000;']Directions:[/span][/b][br] - [b]/config[/b] : this is where Jellyfin will store it's databases and configuration.[br][br] + [b]/config[/b] : This is where Jellyfin will store it's databases and configuration.[br][br] [b]Port[/b] : This is the default port for Jellyfin. (Will add ssl port later)[br][br] - [b]Media[/b] : This is the mounting point of your media. When you access it in Jellyfin it will be /media or whatever you chose for a mount point + [b]Media[/b] : This is the mounting point of your media. When you access it in Jellyfin it will be /media or whatever you chose for a mount point[br][br] + [b]Cache[/b] : This is where Jellyfin will store and manage cached files like images to serve to clients. This is not where all images are stored.[br][br] [b]Tip:[/b] You can add more volume mappings if you wish Jellyfin has access to it. @@ -35,7 +36,7 @@ - /mnt/cache/appdata/config + /mnt/user/appdata/jellyfin /config rw @@ -44,6 +45,11 @@ /media rw + + /mnt/user/appdata/jellyfin/cache/ + /cache + rw + http://[IP]:[PORT:8096]/ https://raw.githubusercontent.com/binhex/docker-templates/master/binhex/images/jellyfin-icon.png From a9f790e101804a2ce0a85217c5c5874e0a7cdb91 Mon Sep 17 00:00:00 2001 From: Terror-Gene Date: Thu, 11 Apr 2019 04:00:46 +0930 Subject: [PATCH 24/91] Fix directory capitalization --- deployment/unraid/docker-templates/jellyfin.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml index eec9967bbe..57b4cc5ae1 100644 --- a/deployment/unraid/docker-templates/jellyfin.xml +++ b/deployment/unraid/docker-templates/jellyfin.xml @@ -36,7 +36,7 @@ - /mnt/user/appdata/jellyfin + /mnt/user/appdata/Jellyfin /config rw @@ -46,7 +46,7 @@ rw - /mnt/user/appdata/jellyfin/cache/ + /mnt/user/appdata/Jellyfin/cache/ /cache rw From 5f6ab836dea4341844f1e1e5a1e1d45c7c6621c1 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 15:35:06 +0200 Subject: [PATCH 25/91] Extend Microsoft.Net.Http.Headers.HeaderNames --- MediaBrowser.Common/Net/MoreHeaderNames.cs | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 MediaBrowser.Common/Net/MoreHeaderNames.cs diff --git a/MediaBrowser.Common/Net/MoreHeaderNames.cs b/MediaBrowser.Common/Net/MoreHeaderNames.cs new file mode 100644 index 0000000000..669646db1e --- /dev/null +++ b/MediaBrowser.Common/Net/MoreHeaderNames.cs @@ -0,0 +1,83 @@ +using HN = Microsoft.Net.Http.Headers.HeaderNames; + +namespace MediaBrowser.Common.Net +{ + public static class MoreHeaderNames + { + // Other Headers + public const string XForwardedFor = "X-Forwarded-For"; + public const string XForwardedPort = "X-Forwarded-Port"; + public const string XForwardedProto = "X-Forwarded-Proto"; + public const string XRealIP = "X-Real-IP"; + + // Headers from Microsoft.Net.Http.Headers.HeaderNames + public const string Accept = HN.Accept; + public const string AcceptCharset = HN.AcceptCharset; + public const string AcceptEncoding = HN.AcceptEncoding; + public const string AcceptLanguage = HN.AcceptLanguage; + public const string AcceptRanges = HN.AcceptRanges; + public const string AccessControlAllowCredentials = HN.AccessControlAllowCredentials; + public const string AccessControlAllowHeaders = HN.AccessControlAllowHeaders; + public const string AccessControlAllowMethods = HN.AccessControlAllowMethods; + public const string AccessControlAllowOrigin = HN.AccessControlAllowOrigin; + public const string AccessControlExposeHeaders = HN.AccessControlExposeHeaders; + public const string AccessControlMaxAge = HN.AccessControlMaxAge; + public const string AccessControlRequestHeaders = HN.AccessControlRequestHeaders; + public const string AccessControlRequestMethod = HN.AccessControlRequestMethod; + public const string Age = HN.Age; + public const string Allow = HN.Allow; + public const string Authority = HN.Authority; + public const string Authorization = HN.Authorization; + public const string CacheControl = HN.CacheControl; + public const string Connection = HN.Connection; + public const string ContentDisposition = HN.ContentDisposition; + public const string ContentEncoding = HN.ContentEncoding; + public const string ContentLanguage = HN.ContentLanguage; + public const string ContentLength = HN.ContentLength; + public const string ContentLocation = HN.ContentLocation; + public const string ContentMD5 = HN.ContentMD5; + public const string ContentRange = HN.ContentRange; + public const string ContentSecurityPolicy = HN.ContentSecurityPolicy; + public const string ContentSecurityPolicyReportOnly = HN.ContentSecurityPolicyReportOnly; + public const string ContentType = HN.ContentType; + public const string Cookie = HN.Cookie; + public const string Date = HN.Date; + public const string ETag = HN.ETag; + public const string Expires = HN.Expires; + public const string Expect = HN.Expect; + public const string From = HN.From; + public const string Host = HN.Host; + public const string IfMatch = HN.IfMatch; + public const string IfModifiedSince = HN.IfModifiedSince; + public const string IfNoneMatch = HN.IfNoneMatch; + public const string IfRange = HN.IfRange; + public const string IfUnmodifiedSince = HN.IfUnmodifiedSince; + public const string LastModified = HN.LastModified; + public const string Location = HN.Location; + public const string MaxForwards = HN.MaxForwards; + public const string Method = HN.Method; + public const string Origin = HN.Origin; + public const string Path = HN.Path; + public const string Pragma = HN.Pragma; + public const string ProxyAuthenticate = HN.ProxyAuthenticate; + public const string ProxyAuthorization = HN.ProxyAuthorization; + public const string Range = HN.Range; + public const string Referer = HN.Referer; + public const string RetryAfter = HN.RetryAfter; + public const string Scheme = HN.Scheme; + public const string Server = HN.Server; + public const string SetCookie = HN.SetCookie; + public const string Status = HN.Status; + public const string StrictTransportSecurity = HN.StrictTransportSecurity; + public const string TE = HN.TE; + public const string Trailer = HN.Trailer; + public const string TransferEncoding = HN.TransferEncoding; + public const string Upgrade = HN.Upgrade; + public const string UserAgent = HN.UserAgent; + public const string Vary = HN.Vary; + public const string Via = HN.Via; + public const string Warning = HN.Warning; + public const string WebSocketSubProtocols = HN.WebSocketSubProtocols; + public const string WWWAuthenticate = HN.WWWAuthenticate; + } +} \ No newline at end of file From a6e1b23eb04d620e241e4babbcb6ee0ffbf156a9 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 16:58:28 +0200 Subject: [PATCH 26/91] Simplify headers use in WSS --- .../SocketSharp/WebSocketSharpRequest.cs | 16 +++++---------- MediaBrowser.Model/Services/IHttpRequest.cs | 20 ------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 792615a0f6..957371df62 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using HeaderNames = MediaBrowser.Common.Net.MoreHeaderNames; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; using IResponse = MediaBrowser.Model.Services.IResponse; @@ -38,16 +39,9 @@ namespace Emby.Server.Implementations.SocketSharp public string RawUrl => request.GetEncodedPathAndQuery(); public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); + // Header[name] returns "" when undefined - public string XForwardedFor - => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString(); - - public int? XForwardedPort - => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture); - - public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString(); - - public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString(); + private string GetHeader(string name) => request.Headers[name].ToString(); private string remoteIp; public string RemoteIp @@ -59,13 +53,13 @@ namespace Emby.Server.Implementations.SocketSharp return remoteIp; } - var temp = CheckBadChars(XForwardedFor.AsSpan()); + var temp = CheckBadChars(GetHeader(HeaderNames.XForwardedFor).AsSpan()); if (temp.Length != 0) { return remoteIp = temp.ToString(); } - temp = CheckBadChars(XRealIp.AsSpan()); + temp = CheckBadChars(GetHeader(HeaderNames.XRealIP).AsSpan()); if (temp.Length != 0) { return remoteIp = NormalizeIp(temp).ToString(); diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs index 50c6076f30..daf91488f0 100644 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ b/MediaBrowser.Model/Services/IHttpRequest.cs @@ -7,26 +7,6 @@ namespace MediaBrowser.Model.Services ///

string HttpMethod { get; } - /// - /// The IP Address of the X-Forwarded-For header, null if null or empty - /// - string XForwardedFor { get; } - - /// - /// The Port number of the X-Forwarded-Port header, null if null or empty - /// - int? XForwardedPort { get; } - - /// - /// The http or https scheme of the X-Forwarded-Proto header, null if null or empty - /// - string XForwardedProtocol { get; } - - /// - /// The value of the X-Real-IP header, null if null or empty - /// - string XRealIp { get; } - /// /// The value of the Accept HTTP Request Header /// From 56d1050bac3a56249acd7f3b3615f796683e0783 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 17:13:40 +0200 Subject: [PATCH 27/91] Replace custom ip "normalization" by methods from `IPAddress` --- .../SocketSharp/WebSocketSharpRequest.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 957371df62..d153a85a32 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -62,10 +62,10 @@ namespace Emby.Server.Implementations.SocketSharp temp = CheckBadChars(GetHeader(HeaderNames.XRealIP).AsSpan()); if (temp.Length != 0) { - return remoteIp = NormalizeIp(temp).ToString(); + return remoteIp = NormalizeIp(temp.ToString()).ToString(); } - return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString(); + return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress).ToString(); } } @@ -137,22 +137,21 @@ namespace Emby.Server.Implementations.SocketSharp return name; } - private ReadOnlySpan NormalizeIp(ReadOnlySpan ip) + private IPAddress NormalizeIp(IPAddress ip) { - if (ip.Length != 0 && !ip.IsWhiteSpace()) + if (ip.IsIPv4MappedToIPv6) { - // Handle ipv4 mapped to ipv6 - const string srch = "::ffff:"; - var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase); - if (index == 0) - { - ip = ip.Slice(srch.Length); - } + return ip.MapToIPv4(); } return ip; } + private IPAddress NormalizeIp(string sip) + { + return NormalizeIp(IPAddress.Parse(sip)); + } + public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); private Dictionary items; From bb807554e2c33f066402898a3ff79913865d50e3 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Thu, 11 Apr 2019 17:17:48 +0200 Subject: [PATCH 28/91] Replace CRLF injection mitigation by use of .NET ip parsing --- .../SocketSharp/WebSocketSharpRequest.cs | 93 ++----------------- 1 file changed, 10 insertions(+), 83 deletions(-) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index d153a85a32..38a860a511 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -53,91 +53,23 @@ namespace Emby.Server.Implementations.SocketSharp return remoteIp; } - var temp = CheckBadChars(GetHeader(HeaderNames.XForwardedFor).AsSpan()); - if (temp.Length != 0) - { - return remoteIp = temp.ToString(); - } - - temp = CheckBadChars(GetHeader(HeaderNames.XRealIP).AsSpan()); - if (temp.Length != 0) - { - return remoteIp = NormalizeIp(temp.ToString()).ToString(); - } - - return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress).ToString(); - } - } - - private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; - - // CheckBadChars - throws on invalid chars to be not found in header name/value - internal static ReadOnlySpan CheckBadChars(ReadOnlySpan name) - { - if (name.Length == 0) - { - return name; - } + IPAddress ip; - // VALUE check - // Trim spaces from both ends - name = name.Trim(HttpTrimCharacters); - - // First, check for correctly formed multi-line value - // Second, check for absence of CTL characters - int crlf = 0; - for (int i = 0; i < name.Length; ++i) - { - char c = (char)(0x000000ff & (uint)name[i]); - switch (crlf) + // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip + // (if the server is behind a reverse proxy for example) + if (!IPAddress.TryParse(GetHeader(HeaderNames.XForwardedFor), out ip)) { - case 0: - if (c == '\r') - { - crlf = 1; - } - else if (c == '\n') - { - // Technically this is bad HTTP. But it would be a breaking change to throw here. - // Is there an exploit? - crlf = 2; - } - else if (c == 127 || (c < ' ' && c != '\t')) - { - throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name)); - } - - break; - - case 1: - if (c == '\n') - { - crlf = 2; - break; - } - - throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); - - case 2: - if (c == ' ' || c == '\t') - { - crlf = 0; - break; - } - - throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); + if (!IPAddress.TryParse(GetHeader(HeaderNames.XRealIP), out ip)) + { + ip = request.HttpContext.Connection.RemoteIpAddress; + } } - } - if (crlf != 0) - { - throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name)); + return remoteIp = NormalizeIp(ip).ToString(); } - - return name; } - private IPAddress NormalizeIp(IPAddress ip) + private static IPAddress NormalizeIp(IPAddress ip) { if (ip.IsIPv4MappedToIPv6) { @@ -147,11 +79,6 @@ namespace Emby.Server.Implementations.SocketSharp return ip; } - private IPAddress NormalizeIp(string sip) - { - return NormalizeIp(IPAddress.Parse(sip)); - } - public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); private Dictionary items; From ba12d96d23a53ce16a1da1b2fcf68a301050b858 Mon Sep 17 00:00:00 2001 From: VooDooS Date: Fri, 12 Apr 2019 15:25:18 +0200 Subject: [PATCH 29/91] Removed wrapping of HeaderNames fields --- .../SocketSharp/WebSocketSharpRequest.cs | 6 +- MediaBrowser.Common/Net/CustomHeaderNames.cs | 11 +++ MediaBrowser.Common/Net/MoreHeaderNames.cs | 83 ------------------- 3 files changed, 14 insertions(+), 86 deletions(-) create mode 100644 MediaBrowser.Common/Net/CustomHeaderNames.cs delete mode 100644 MediaBrowser.Common/Net/MoreHeaderNames.cs diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 38a860a511..00465b63eb 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -4,13 +4,13 @@ using System.Globalization; using System.IO; using System.Net; using System.Text; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using HeaderNames = MediaBrowser.Common.Net.MoreHeaderNames; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; using IResponse = MediaBrowser.Model.Services.IResponse; @@ -57,9 +57,9 @@ namespace Emby.Server.Implementations.SocketSharp // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(GetHeader(HeaderNames.XForwardedFor), out ip)) + if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip)) { - if (!IPAddress.TryParse(GetHeader(HeaderNames.XRealIP), out ip)) + if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) { ip = request.HttpContext.Connection.RemoteIpAddress; } diff --git a/MediaBrowser.Common/Net/CustomHeaderNames.cs b/MediaBrowser.Common/Net/CustomHeaderNames.cs new file mode 100644 index 0000000000..ff148dc801 --- /dev/null +++ b/MediaBrowser.Common/Net/CustomHeaderNames.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Common.Net +{ + public static class CustomHeaderNames + { + // Other Headers + public const string XForwardedFor = "X-Forwarded-For"; + public const string XForwardedPort = "X-Forwarded-Port"; + public const string XForwardedProto = "X-Forwarded-Proto"; + public const string XRealIP = "X-Real-IP"; + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Net/MoreHeaderNames.cs b/MediaBrowser.Common/Net/MoreHeaderNames.cs deleted file mode 100644 index 669646db1e..0000000000 --- a/MediaBrowser.Common/Net/MoreHeaderNames.cs +++ /dev/null @@ -1,83 +0,0 @@ -using HN = Microsoft.Net.Http.Headers.HeaderNames; - -namespace MediaBrowser.Common.Net -{ - public static class MoreHeaderNames - { - // Other Headers - public const string XForwardedFor = "X-Forwarded-For"; - public const string XForwardedPort = "X-Forwarded-Port"; - public const string XForwardedProto = "X-Forwarded-Proto"; - public const string XRealIP = "X-Real-IP"; - - // Headers from Microsoft.Net.Http.Headers.HeaderNames - public const string Accept = HN.Accept; - public const string AcceptCharset = HN.AcceptCharset; - public const string AcceptEncoding = HN.AcceptEncoding; - public const string AcceptLanguage = HN.AcceptLanguage; - public const string AcceptRanges = HN.AcceptRanges; - public const string AccessControlAllowCredentials = HN.AccessControlAllowCredentials; - public const string AccessControlAllowHeaders = HN.AccessControlAllowHeaders; - public const string AccessControlAllowMethods = HN.AccessControlAllowMethods; - public const string AccessControlAllowOrigin = HN.AccessControlAllowOrigin; - public const string AccessControlExposeHeaders = HN.AccessControlExposeHeaders; - public const string AccessControlMaxAge = HN.AccessControlMaxAge; - public const string AccessControlRequestHeaders = HN.AccessControlRequestHeaders; - public const string AccessControlRequestMethod = HN.AccessControlRequestMethod; - public const string Age = HN.Age; - public const string Allow = HN.Allow; - public const string Authority = HN.Authority; - public const string Authorization = HN.Authorization; - public const string CacheControl = HN.CacheControl; - public const string Connection = HN.Connection; - public const string ContentDisposition = HN.ContentDisposition; - public const string ContentEncoding = HN.ContentEncoding; - public const string ContentLanguage = HN.ContentLanguage; - public const string ContentLength = HN.ContentLength; - public const string ContentLocation = HN.ContentLocation; - public const string ContentMD5 = HN.ContentMD5; - public const string ContentRange = HN.ContentRange; - public const string ContentSecurityPolicy = HN.ContentSecurityPolicy; - public const string ContentSecurityPolicyReportOnly = HN.ContentSecurityPolicyReportOnly; - public const string ContentType = HN.ContentType; - public const string Cookie = HN.Cookie; - public const string Date = HN.Date; - public const string ETag = HN.ETag; - public const string Expires = HN.Expires; - public const string Expect = HN.Expect; - public const string From = HN.From; - public const string Host = HN.Host; - public const string IfMatch = HN.IfMatch; - public const string IfModifiedSince = HN.IfModifiedSince; - public const string IfNoneMatch = HN.IfNoneMatch; - public const string IfRange = HN.IfRange; - public const string IfUnmodifiedSince = HN.IfUnmodifiedSince; - public const string LastModified = HN.LastModified; - public const string Location = HN.Location; - public const string MaxForwards = HN.MaxForwards; - public const string Method = HN.Method; - public const string Origin = HN.Origin; - public const string Path = HN.Path; - public const string Pragma = HN.Pragma; - public const string ProxyAuthenticate = HN.ProxyAuthenticate; - public const string ProxyAuthorization = HN.ProxyAuthorization; - public const string Range = HN.Range; - public const string Referer = HN.Referer; - public const string RetryAfter = HN.RetryAfter; - public const string Scheme = HN.Scheme; - public const string Server = HN.Server; - public const string SetCookie = HN.SetCookie; - public const string Status = HN.Status; - public const string StrictTransportSecurity = HN.StrictTransportSecurity; - public const string TE = HN.TE; - public const string Trailer = HN.Trailer; - public const string TransferEncoding = HN.TransferEncoding; - public const string Upgrade = HN.Upgrade; - public const string UserAgent = HN.UserAgent; - public const string Vary = HN.Vary; - public const string Via = HN.Via; - public const string Warning = HN.Warning; - public const string WebSocketSubProtocols = HN.WebSocketSubProtocols; - public const string WWWAuthenticate = HN.WWWAuthenticate; - } -} \ No newline at end of file From 65f9141764c18b76d97925e531816e8dd383f4bb Mon Sep 17 00:00:00 2001 From: DrPandemic Date: Wed, 10 Apr 2019 22:08:23 -0400 Subject: [PATCH 30/91] Update the help message to indicate the right output folder --- build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build b/build index 51d4b79a20..fb4ff1984a 100755 --- a/build +++ b/build @@ -29,7 +29,7 @@ usage() { echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching." echo -e "To build all platforms, use 'all'." echo -e "To perform all build actions, use 'all'." - echo -e "Build output files are collected at '../jellyfin-build/'." + echo -e "Build output files are collected at '../bin/'." } # Show usage on stderr with exit 1 on argless From e89c8dbf7623fcb0c19c96957f6260df31fc3945 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 17 Apr 2019 15:23:03 +0200 Subject: [PATCH 31/91] Use CultureInfo.InvariantCulture --- .../Playback/BaseStreamingService.cs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index efbb17fd2a..3994016240 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -30,8 +30,6 @@ namespace MediaBrowser.Api.Playback ///
public abstract class BaseStreamingService : BaseApiService { - protected static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - protected virtual bool EnableOutputInSubFolder => false; /// @@ -418,55 +416,55 @@ namespace MediaBrowser.Api.Playback { if (videoRequest != null) { - videoRequest.AudioStreamIndex = int.Parse(val, UsCulture); + videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 7) { if (videoRequest != null) { - videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture); + videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 8) { if (videoRequest != null) { - videoRequest.VideoBitRate = int.Parse(val, UsCulture); + videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 9) { - request.AudioBitRate = int.Parse(val, UsCulture); + request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); } else if (i == 10) { - request.MaxAudioChannels = int.Parse(val, UsCulture); + request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); } else if (i == 11) { if (videoRequest != null) { - videoRequest.MaxFramerate = float.Parse(val, UsCulture); + videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 12) { if (videoRequest != null) { - videoRequest.MaxWidth = int.Parse(val, UsCulture); + videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 13) { if (videoRequest != null) { - videoRequest.MaxHeight = int.Parse(val, UsCulture); + videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 14) { - request.StartTimeTicks = long.Parse(val, UsCulture); + request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); } else if (i == 15) { @@ -479,14 +477,14 @@ namespace MediaBrowser.Api.Playback { if (videoRequest != null) { - videoRequest.MaxRefFrames = int.Parse(val, UsCulture); + videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 17) { if (videoRequest != null) { - videoRequest.MaxVideoBitDepth = int.Parse(val, UsCulture); + videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); } } else if (i == 18) @@ -535,7 +533,7 @@ namespace MediaBrowser.Api.Playback } else if (i == 26) { - request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture); + request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); } else if (i == 27) { @@ -640,7 +638,7 @@ namespace MediaBrowser.Api.Playback if (value.IndexOf(':') == -1) { // Parses npt times in the format of '417.33' - if (double.TryParse(value, NumberStyles.Any, UsCulture, out var seconds)) + if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) { return TimeSpan.FromSeconds(seconds).Ticks; } @@ -655,7 +653,7 @@ namespace MediaBrowser.Api.Playback foreach (var time in tokens) { - if (double.TryParse(time, NumberStyles.Any, UsCulture, out var digit)) + if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit)) { secondsSum += digit * timeFactor; } @@ -1027,8 +1025,8 @@ namespace MediaBrowser.Api.Playback private void AddTimeSeekResponseHeaders(StreamState state, IDictionary responseHeaders) { - var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture); - var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(UsCulture); + var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); + var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds); responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds); From 8f703f4744b2701843e316210263c8e4cd3256bd Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 18 Apr 2019 13:19:16 +0200 Subject: [PATCH 32/91] Remove unused event Release builds were failing because of this unused event. --- .../Activity/ActivityLogEntryPoint.cs | 14 ------------- .../ApplicationHost.cs | 21 +++++++------------ MediaBrowser.Common/IApplicationHost.cs | 5 ----- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 98cd97c318..190e4d55c0 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -83,8 +83,6 @@ namespace Emby.Server.Implementations.Activity _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - _appHost.ApplicationUpdated += OnApplicationUpdated; - return Task.CompletedTask; } @@ -275,16 +273,6 @@ namespace Emby.Server.Implementations.Activity }); } - private void OnApplicationUpdated(object sender, GenericEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("MessageApplicationUpdatedTo"), e.Argument.versionStr), - Type = NotificationType.ApplicationUpdateInstalled.ToString(), - Overview = e.Argument.description - }); - } - private void OnUserPolicyUpdated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry @@ -460,8 +448,6 @@ namespace Emby.Server.Implementations.Activity _userManager.UserLockedOut -= OnUserLockedOut; _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; - - _appHost.ApplicationUpdated -= OnApplicationUpdated; } /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9804f28cf3..0ebbeea57e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -154,11 +154,6 @@ namespace Emby.Server.Implementations /// public event EventHandler HasPendingRestartChanged; - /// - /// Occurs when [application updated]. - /// - public event EventHandler> ApplicationUpdated; - /// /// Gets a value indicating whether this instance has changes that require the entire application to restart. /// @@ -1392,9 +1387,9 @@ namespace Emby.Server.Implementations public async Task GetSystemInfo(CancellationToken cancellationToken) { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - - string wanAddress; - + + string wanAddress; + if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); @@ -1451,10 +1446,10 @@ namespace Emby.Server.Implementations public async Task GetPublicSystemInfo(CancellationToken cancellationToken) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - + var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + string wanAddress; - + if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) { wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); @@ -1570,9 +1565,9 @@ namespace Emby.Server.Implementations } return string.Format("http://{0}:{1}", host, - ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture)); + ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture)); } - + public Task> GetLocalIpAddresses(CancellationToken cancellationToken) { return GetLocalIpAddressesInternal(true, 0, cancellationToken); diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 2925a3efd9..cb7343440a 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -25,11 +25,6 @@ namespace MediaBrowser.Common /// The device identifier. string SystemId { get; } - /// - /// Occurs when [application updated]. - /// - event EventHandler> ApplicationUpdated; - /// /// Gets or sets a value indicating whether this instance has pending kernel reload. /// From 0794a3edf48eb232030560c4a03587cd77f38b65 Mon Sep 17 00:00:00 2001 From: bugfixin Date: Thu, 18 Apr 2019 21:47:19 +0000 Subject: [PATCH 33/91] Adjust detection of 'sample' in filenames to use regex boundaries --- .../Library/CoreResolutionIgnoreRule.cs | 11 +++-------- .../Library/Resolvers/Movies/MovieResolver.cs | 13 +++---------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index c644d13eab..c9d4e43422 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -148,15 +149,9 @@ namespace Emby.Server.Implementations.Library } // Ignore samples - var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) - .Replace("-", " ", StringComparison.OrdinalIgnoreCase) - .Replace("_", " ", StringComparison.OrdinalIgnoreCase) - .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + Match m = Regex.Match(filename,"\bsample\b",RegexOptions.IgnoreCase); - if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } + return m.Success; } return false; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 8485636794..47c3e71d72 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Emby.Naming.Video; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -167,17 +168,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private static bool IsIgnored(string filename) { // Ignore samples - var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) - .Replace("-", " ", StringComparison.OrdinalIgnoreCase) - .Replace("_", " ", StringComparison.OrdinalIgnoreCase) - .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + Match m = Regex.Match(filename,@"\bsample\b",RegexOptions.IgnoreCase); - if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - return false; + return m.Success; } private bool ContainsFile(List result, FileSystemMetadata file) From da842d5a730ea8db2fab8e4c71a501191d54e46c Mon Sep 17 00:00:00 2001 From: bugfixin Date: Fri, 19 Apr 2019 18:35:28 +0000 Subject: [PATCH 34/91] Fix incorrect escaping in regex pattern --- Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index c9d4e43422..a70077163e 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.Library } // Ignore samples - Match m = Regex.Match(filename,"\bsample\b",RegexOptions.IgnoreCase); + Match m = Regex.Match(filename,@"\bsample\b",RegexOptions.IgnoreCase); return m.Success; } From 08d3a5d2feafe9d54c354028f38d7cfa4e94293c Mon Sep 17 00:00:00 2001 From: bugfixin Date: Sun, 21 Apr 2019 19:29:05 +0000 Subject: [PATCH 35/91] Fix null reference when request content type is application/x-www-form-urlencoded --- Emby.Server.Implementations/Services/ServiceHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 621be4fcb5..d32fce1c77 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -26,7 +26,10 @@ namespace Emby.Server.Implementations.Services if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) { var deserializer = RequestHelper.GetRequestReader(host, contentType); - return deserializer?.Invoke(requestType, httpReq.InputStream); + if (deserializer != null) + { + return deserializer.Invoke(requestType, httpReq.InputStream); + } } return Task.FromResult(host.CreateInstance(requestType)); From a9337033c1d95d7238e9411abbc7255ef2456f35 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 24 Apr 2019 15:25:22 +0200 Subject: [PATCH 36/91] Fix query time logging --- .../Data/SqliteItemRepository.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 088a6694b0..8841a9a504 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2741,15 +2741,16 @@ namespace Emby.Server.Implementations.Data { var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; - int slowThreshold = 100; - #if DEBUG - slowThreshold = 10; + const int SlowThreshold = 100; +#else + const int SlowThreshold = 10; #endif - if (elapsed >= slowThreshold) + if (elapsed >= SlowThreshold) { - Logger.LogWarning("{0} query time (slow): {1:g}. Query: {2}", + Logger.LogWarning( + "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}", methodName, elapsed, commandText); From 84ac6ea12a571b40897d56637bb710c5b4a650c2 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Thu, 9 May 2019 18:57:40 -0400 Subject: [PATCH 37/91] Update getting-help link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f635bdd25..8340b3d7b1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! -For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help). +For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/getting-help/). For more information about the project, please see our [about page](https://jellyfin.readthedocs.io/en/latest/about/). From 35d7e97258347cf60cc9062402093cc53ff0e922 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 11 May 2019 11:55:41 +0200 Subject: [PATCH 38/91] Ignore casing photo extensions --- Emby.Photos/PhotoProvider.cs | 78 +++++++------------ .../Library/Resolvers/PhotoResolver.cs | 52 ++++++------- 2 files changed, 52 insertions(+), 78 deletions(-) diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index a4179e6609..99a635e608 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -20,7 +20,10 @@ namespace Emby.Photos public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IHasItemChangeMonitor { private readonly ILogger _logger; - private IImageProcessor _imageProcessor; + private readonly IImageProcessor _imageProcessor; + + // These are causing taglib to hang + private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" }; public PhotoProvider(ILogger logger, IImageProcessor imageProcessor) { @@ -28,75 +31,55 @@ namespace Emby.Photos _imageProcessor = imageProcessor; } + public string Name => "Embedded Information"; + public bool HasChanged(BaseItem item, IDirectoryService directoryService) { if (item.IsFileProtocol) { var file = directoryService.GetFile(item.Path); - if (file != null && file.LastWriteTimeUtc != item.DateModified) - { - return true; - } + return (file != null && file.LastWriteTimeUtc != item.DateModified); } return false; } - // These are causing taglib to hang - private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" }; - public Task FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken) { item.SetImagePath(ImageType.Primary, item.Path); // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs - if (_includextensions.Contains(Path.GetExtension(item.Path) ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (_includextensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase)) { try { using (var file = TagLib.File.Create(item.Path)) { - var image = file as TagLib.Image.File; - - var tag = file.GetTag(TagTypes.TiffIFD) as IFDTag; - - if (tag != null) + if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag) { var structure = tag.Structure; - - if (structure != null) + if (structure != null + && structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif) { - var exif = structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) as SubIFDEntry; - - if (exif != null) + var exifStructure = exif.Structure; + if (exifStructure != null) { - var exifStructure = exif.Structure; + var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry; + if (entry != null) + { + item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator; + } - if (exifStructure != null) + entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry; + if (entry != null) { - var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry; - - if (entry != null) - { - double val = entry.Value.Numerator; - val /= entry.Value.Denominator; - item.Aperture = val; - } - - entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry; - - if (entry != null) - { - double val = entry.Value.Numerator; - val /= entry.Value.Denominator; - item.ShutterSpeed = val; - } + item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator; } } } } - if (image != null) + if (file is TagLib.Image.File image) { item.CameraMake = image.ImageTag.Make; item.CameraModel = image.ImageTag.Model; @@ -116,12 +99,10 @@ namespace Emby.Photos item.Overview = image.ImageTag.Comment; - if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)) + if (!string.IsNullOrWhiteSpace(image.ImageTag.Title) + && !item.LockedFields.Contains(MetadataFields.Name)) { - if (!item.LockedFields.Contains(MetadataFields.Name)) - { - item.Name = image.ImageTag.Title; - } + item.Name = image.ImageTag.Title; } var dateTaken = image.ImageTag.DateTime; @@ -140,12 +121,9 @@ namespace Emby.Photos { item.Orientation = null; } - else + else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation)) { - if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation)) - { - item.Orientation = orientation; - } + item.Orientation = orientation; } item.ExposureTime = image.ImageTag.ExposureTime; @@ -195,7 +173,5 @@ namespace Emby.Photos const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport; return Task.FromResult(result); } - - public string Name => "Embedded Information"; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index db270c398b..8171c010bd 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers { private readonly IImageProcessor _imageProcessor; private readonly ILibraryManager _libraryManager; + private static readonly HashSet _ignoreFiles = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "folder", + "thumb", + "landscape", + "fanart", + "backdrop", + "poster", + "cover", + "logo", + "default" + }; public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) { @@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers if (!args.IsDirectory) { // Must be an image file within a photo collection - var collectionType = args.GetCollectionType(); + var collectionType = args.CollectionType; - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) || - (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) + if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) + || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { @@ -74,43 +86,29 @@ namespace Emby.Server.Implementations.Library.Resolvers } internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename) + => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); + + internal static bool IsImageFile(string path, IImageProcessor imageProcessor) { - if (imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase)) + if (path == null) { - return true; + throw new ArgumentNullException(nameof(path)); } - return false; - } - - private static readonly HashSet IgnoreFiles = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "folder", - "thumb", - "landscape", - "fanart", - "backdrop", - "poster", - "cover", - "logo", - "default" - }; - - internal static bool IsImageFile(string path, IImageProcessor imageProcessor) - { - var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty; + var filename = Path.GetFileNameWithoutExtension(path); - if (IgnoreFiles.Contains(filename)) + if (_ignoreFiles.Contains(filename)) { return false; } - if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) + if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) { return false; } - return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal); + string extension = Path.GetExtension(path).TrimStart('.'); + return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase); } } } From 5d85076ad5152b60ca2101177330b24944a65411 Mon Sep 17 00:00:00 2001 From: JMCC Date: Sat, 11 May 2019 17:17:32 +0200 Subject: [PATCH 39/91] Enable Exynos V4L2-m2m HW encoder --- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 3eed891cb3..b00350875c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -207,6 +207,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "hevc_omx", "h264_vaapi", "hevc_vaapi", + "h264_v4l2m2m", "ac3" }; From 012e4a3e63f8b0997d5f070b37ac93d257f894e7 Mon Sep 17 00:00:00 2001 From: JMCC Date: Sat, 11 May 2019 17:19:20 +0200 Subject: [PATCH 40/91] Fix transcode bitrate control --- .../MediaEncoding/EncodingHelper.cs | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e378c2b89d..849cf70835 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1083,27 +1083,50 @@ namespace MediaBrowser.Controller.MediaEncoding { var bitrate = request.VideoBitRate; - // If specific values were requested, then force the caller to supply a bitrate as well - if (request.Height.HasValue && request.Width.HasValue) - { - return bitrate; - } - if (videoStream != null) { - if (bitrate.HasValue) - { - var inputVideoCodec = videoStream.Codec; - bitrate = ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); + var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue && + request.Height.Value > videoStream.Height.Value && request.Width.HasValue && videoStream.Width.HasValue && + request.Width.Value > videoStream.Width.Value; - // If a max bitrate was requested, don't let the scaled bitrate exceed it - if (request.VideoBitRate.HasValue) + // Don't allow bitrate increases unless upscaling + if (!isUpscaling) + { + if (bitrate.HasValue && videoStream.BitRate.HasValue) { - bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value); } } } + if (bitrate.HasValue) + { + var inputVideoCodec = videoStream.Codec; + bitrate = ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); + + // If a max bitrate was requested, don't let the scaled bitrate exceed it + if (request.VideoBitRate.HasValue) + { + bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + } + } + + return bitrate; + } + + private int GetMinBitrate(int sourceBitrate, int requestedBitrate) + { + if (sourceBitrate <= 2000000) + { + sourceBitrate = Convert.ToInt32(sourceBitrate * 2.5); + } + else if (sourceBitrate <= 3000000) + { + sourceBitrate = Convert.ToInt32(sourceBitrate * 2); + } + + var bitrate = Math.Min(sourceBitrate, requestedBitrate); + return bitrate; } From 4a9b349c0463b46a3208b949f75d951f092aa735 Mon Sep 17 00:00:00 2001 From: dkanada Date: Thu, 16 May 2019 16:45:56 -0700 Subject: [PATCH 41/91] only return useful drives --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 0dea5041a1..f0bea0455a 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -667,13 +667,13 @@ namespace Emby.Server.Implementations.IO public virtual List GetDrives() { - // Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout - return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata + // check for ready state to avoid waiting for drives to timeout + // some drives on linux have no actual size or are used for other purposes + return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram).Select(d => new FileSystemMetadata { Name = d.Name, FullName = d.RootDirectory.FullName, IsDirectory = true - }).ToList(); } From 2aed2d164b110ecafa88b5036e0875627908bde2 Mon Sep 17 00:00:00 2001 From: erikasne6152 Date: Wed, 22 May 2019 11:05:08 +0000 Subject: [PATCH 42/91] Translated using Weblate (Lithuanian) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/ --- .../Localization/Core/lt-LT.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 558904f060..e2f3ba3dc8 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -1,21 +1,21 @@ { - "Albums": "Albums", + "Albums": "Albumai", "AppDeviceValues": "App: {0}, Device: {1}", "Application": "Application", - "Artists": "Artists", + "Artists": "Atlikėjai", "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", + "Books": "Knygos", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", + "Channels": "Kanalai", "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", + "Collections": "Kolekcijos", "DeviceOfflineWithName": "{0} has disconnected", "DeviceOnlineWithName": "{0} is connected", "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", + "Favorites": "Mėgstami", + "Folders": "Katalogai", "Genres": "Žanrai", - "HeaderAlbumArtists": "Album Artists", + "HeaderAlbumArtists": "Albumo atlikėjai", "HeaderCameraUploads": "Camera Uploads", "HeaderContinueWatching": "Žiūrėti toliau", "HeaderFavoriteAlbums": "Favorite Albums", From b1f764984f7098ee1164efee77f1bcb3de9fd08a Mon Sep 17 00:00:00 2001 From: exveria1015 Date: Thu, 30 May 2019 22:28:11 +0000 Subject: [PATCH 43/91] Added translation using Weblate (Japanese) --- Emby.Server.Implementations/Localization/Core/ja.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/ja.json diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -0,0 +1 @@ +{} From 09dfa071dc27820f1820bd56126dfa5cd5e01027 Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 31 May 2019 01:48:20 -0700 Subject: [PATCH 44/91] move fanart image provider to plugin --- MediaBrowser.Api/Library/LibraryService.cs | 12 - .../Configuration/FanartOptions.cs | 11 - .../Configuration/ServerConfiguration.cs | 7 +- .../Movies/FanartMovieImageProvider.cs | 334 ---------------- .../Music/FanArtAlbumProvider.cs | 201 ---------- .../Music/FanArtArtistProvider.cs | 335 ---------------- .../TV/FanArt/FanArtSeasonProvider.cs | 205 ---------- .../TV/FanArt/FanartSeriesProvider.cs | 378 ------------------ 8 files changed, 3 insertions(+), 1480 deletions(-) delete mode 100644 MediaBrowser.Model/Configuration/FanartOptions.cs delete mode 100644 MediaBrowser.Providers/Movies/FanartMovieImageProvider.cs delete mode 100644 MediaBrowser.Providers/Music/FanArtAlbumProvider.cs delete mode 100644 MediaBrowser.Providers/Music/FanArtArtistProvider.cs delete mode 100644 MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs delete mode 100644 MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 9f8da9c168..ddf45f9f3c 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -490,18 +490,6 @@ namespace MediaBrowser.Api.Library { return false; } - else if (string.Equals(name, "FanArt", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - return true; - } else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)) { return true; diff --git a/MediaBrowser.Model/Configuration/FanartOptions.cs b/MediaBrowser.Model/Configuration/FanartOptions.cs deleted file mode 100644 index 9c8be39be7..0000000000 --- a/MediaBrowser.Model/Configuration/FanartOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MediaBrowser.Model.Configuration -{ - public class FanartOptions - { - /// - /// Gets or sets the user API key. - /// - /// The user API key. - public string UserApiKey { get; set; } - } -} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0ba36b4b9d..2673597caa 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -49,9 +49,9 @@ namespace MediaBrowser.Model.Configuration public bool EnableNormalizedItemByNameIds { get; set; } /// - /// Gets or sets the value pointing to the file system where the ssl certiifcate is located.. + /// Gets or sets the value pointing to the file system where the ssl certificate is located.. /// - /// The value pointing to the file system where the ssl certiifcate is located.. + /// The value pointing to the file system where the ssl certificate is located.. public string CertificatePath { get; set; } public string CertificatePassword { get; set; } @@ -259,7 +259,7 @@ namespace MediaBrowser.Model.Configuration { ItemType = "MusicVideo", DisabledMetadataFetchers = new [] { "The Open Movie Database" }, - DisabledImageFetchers = new [] { "The Open Movie Database", "FanArt" } + DisabledImageFetchers = new [] { "The Open Movie Database" } }, new MetadataOptions { @@ -285,7 +285,6 @@ namespace MediaBrowser.Model.Configuration { ItemType = "Season", DisabledMetadataFetchers = new [] { "TheMovieDb" }, - DisabledImageFetchers = new [] { "FanArt" } }, new MetadataOptions { diff --git a/MediaBrowser.Providers/Movies/FanartMovieImageProvider.cs b/MediaBrowser.Providers/Movies/FanartMovieImageProvider.cs deleted file mode 100644 index 70d187bf55..0000000000 --- a/MediaBrowser.Providers/Movies/FanartMovieImageProvider.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Music; -using MediaBrowser.Providers.TV; -using MediaBrowser.Providers.TV.FanArt; - -namespace MediaBrowser.Providers.Movies -{ - public class FanartMovieImageProvider : IRemoteImageProvider, IHasOrder - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _json; - - private const string FanArtBaseUrl = "https://webservice.fanart.tv/v3/movies/{1}?api_key={0}"; - - internal static FanartMovieImageProvider Current; - - public FanartMovieImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _json = json; - - Current = this; - } - - public string Name => ProviderName; - - public static string ProviderName => "FanArt"; - - public bool Supports(BaseItem item) - { - return item is Movie || item is BoxSet || item is MusicVideo; - } - - public IEnumerable GetSupportedImages(BaseItem item) - { - return new List - { - ImageType.Primary, - ImageType.Thumb, - ImageType.Art, - ImageType.Logo, - ImageType.Disc, - ImageType.Banner, - ImageType.Backdrop - }; - } - - public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var baseItem = item; - var list = new List(); - - var movieId = baseItem.GetProviderId(MetadataProviders.Tmdb); - - if (!string.IsNullOrEmpty(movieId)) - { - // Bad id entered - try - { - await EnsureMovieJson(movieId, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - var path = GetFanartJsonPath(movieId); - - try - { - AddImages(list, path, cancellationToken); - } - catch (FileNotFoundException) - { - // No biggie. Don't blow up - } - catch (IOException) - { - // No biggie. Don't blow up - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0); - } - - private void AddImages(List list, string path, CancellationToken cancellationToken) - { - var root = _json.DeserializeFromFile(path); - - AddImages(list, root, cancellationToken); - } - - private void AddImages(List list, RootObject obj, CancellationToken cancellationToken) - { - PopulateImages(list, obj.hdmovieclearart, ImageType.Art, 1000, 562); - PopulateImages(list, obj.hdmovielogo, ImageType.Logo, 800, 310); - PopulateImages(list, obj.moviedisc, ImageType.Disc, 1000, 1000); - PopulateImages(list, obj.movieposter, ImageType.Primary, 1000, 1426); - PopulateImages(list, obj.movielogo, ImageType.Logo, 400, 155); - PopulateImages(list, obj.movieart, ImageType.Art, 500, 281); - PopulateImages(list, obj.moviethumb, ImageType.Thumb, 1000, 562); - PopulateImages(list, obj.moviebanner, ImageType.Banner, 1000, 185); - PopulateImages(list, obj.moviebackground, ImageType.Backdrop, 1920, 1080); - } - - private void PopulateImages(List list, List images, ImageType type, int width, int height) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - - if (!string.IsNullOrEmpty(url)) - { - var likesString = i.likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url, - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - - public int Order => 1; - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); - } - - /// - /// Gets the movie data path. - /// - /// The application paths. - /// The identifier. - /// System.String. - internal static string GetMovieDataPath(IApplicationPaths appPaths, string id) - { - var dataPath = Path.Combine(GetMoviesDataPath(appPaths), id); - - return dataPath; - } - - /// - /// Gets the movie data path. - /// - /// The app paths. - /// System.String. - internal static string GetMoviesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "fanart-movies"); - - return dataPath; - } - - public string GetFanartJsonPath(string id) - { - var movieDataPath = GetMovieDataPath(_config.ApplicationPaths, id); - return Path.Combine(movieDataPath, "fanart.json"); - } - - /// - /// Downloads the movie json. - /// - /// The identifier. - /// The cancellation token. - /// Task. - internal async Task DownloadMovieJson(string id, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, id); - - var clientKey = FanartSeriesProvider.Current.GetFanartOptions().UserApiKey; - if (!string.IsNullOrWhiteSpace(clientKey)) - { - url += "&client_key=" + clientKey; - } - - var path = GetFanartJsonPath(id); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - try - { - using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = true - - }, "GET").ConfigureAwait(false)) - { - using (var response = httpResponse.Content) - { - using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await response.CopyToAsync(fileStream).ConfigureAwait(false); - } - } - } - } - catch (HttpException exception) - { - if (exception.StatusCode.HasValue && exception.StatusCode.Value == HttpStatusCode.NotFound) - { - // If the user has automatic updates enabled, save a dummy object to prevent repeated download attempts - _json.SerializeToFile(new RootObject(), path); - - return; - } - - throw; - } - } - - internal Task EnsureMovieJson(string id, CancellationToken cancellationToken) - { - var path = GetFanartJsonPath(id); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadMovieJson(id, cancellationToken); - } - - public class Image - { - public string id { get; set; } - public string url { get; set; } - public string lang { get; set; } - public string likes { get; set; } - } - - public class RootObject - { - public string name { get; set; } - public string tmdb_id { get; set; } - public string imdb_id { get; set; } - public List hdmovielogo { get; set; } - public List moviedisc { get; set; } - public List movielogo { get; set; } - public List movieposter { get; set; } - public List hdmovieclearart { get; set; } - public List movieart { get; set; } - public List moviebackground { get; set; } - public List moviebanner { get; set; } - public List moviethumb { get; set; } - } - } -} diff --git a/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs b/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs deleted file mode 100644 index ebb740ffe4..0000000000 --- a/MediaBrowser.Providers/Music/FanArtAlbumProvider.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Providers.Music -{ - public class FanartAlbumProvider : IRemoteImageProvider, IHasOrder - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - - public FanartAlbumProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer jsonSerializer) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - } - - public string Name => ProviderName; - - public static string ProviderName => "FanArt"; - - public bool Supports(BaseItem item) - { - return item is MusicAlbum; - } - - public IEnumerable GetSupportedImages(BaseItem item) - { - return new List - { - ImageType.Primary, - ImageType.Disc - }; - } - - public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var album = (MusicAlbum)item; - - var list = new List(); - - var musicArtist = album.MusicArtist; - - if (musicArtist == null) - { - return list; - } - - var artistMusicBrainzId = musicArtist.GetProviderId(MetadataProviders.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(artistMusicBrainzId)) - { - await FanartArtistProvider.Current.EnsureArtistJson(artistMusicBrainzId, cancellationToken).ConfigureAwait(false); - - var artistJsonPath = FanartArtistProvider.GetArtistJsonPath(_config.CommonApplicationPaths, artistMusicBrainzId); - - var musicBrainzReleaseGroupId = album.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); - - var musicBrainzId = album.GetProviderId(MetadataProviders.MusicBrainzAlbum); - - try - { - AddImages(list, artistJsonPath, musicBrainzId, musicBrainzReleaseGroupId, cancellationToken); - } - catch (FileNotFoundException) - { - - } - catch (IOException) - { - - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - /// - /// Adds the images. - /// - /// The list. - /// The path. - /// The release identifier. - /// The release group identifier. - /// The cancellation token. - private void AddImages(List list, string path, string releaseId, string releaseGroupId, CancellationToken cancellationToken) - { - var obj = _jsonSerializer.DeserializeFromFile(path); - - if (obj.albums != null) - { - var album = obj.albums.FirstOrDefault(i => string.Equals(i.release_group_id, releaseGroupId, StringComparison.OrdinalIgnoreCase)); - - if (album != null) - { - PopulateImages(list, album.albumcover, ImageType.Primary, 1000, 1000); - PopulateImages(list, album.cdart, ImageType.Disc, 1000, 1000); - } - } - } - - private void PopulateImages(List list, - List images, - ImageType type, - int width, - int height) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - - if (!string.IsNullOrEmpty(url)) - { - var likesString = i.likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - // After embedded provider - public int Order => 1; - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); - } - } -} diff --git a/MediaBrowser.Providers/Music/FanArtArtistProvider.cs b/MediaBrowser.Providers/Music/FanArtArtistProvider.cs deleted file mode 100644 index 75b4213c52..0000000000 --- a/MediaBrowser.Providers/Music/FanArtArtistProvider.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.TV; -using MediaBrowser.Providers.TV.FanArt; - -namespace MediaBrowser.Providers.Music -{ - public class FanartArtistProvider : IRemoteImageProvider, IHasOrder - { - internal const string ApiKey = "184e1a2b1fe3b94935365411f919f638"; - private const string FanArtBaseUrl = "https://webservice.fanart.tv/v3.1/music/{1}?api_key={0}"; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - - internal static FanartArtistProvider Current; - - public FanartArtistProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer jsonSerializer) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - - Current = this; - } - - public string Name => ProviderName; - - public static string ProviderName => "FanArt"; - - public bool Supports(BaseItem item) - { - return item is MusicArtist; - } - - public IEnumerable GetSupportedImages(BaseItem item) - { - return new List - { - ImageType.Primary, - ImageType.Logo, - ImageType.Art, - ImageType.Banner, - ImageType.Backdrop - }; - } - - public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var artist = (MusicArtist)item; - - var list = new List(); - - var artistMusicBrainzId = artist.GetProviderId(MetadataProviders.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(artistMusicBrainzId)) - { - await EnsureArtistJson(artistMusicBrainzId, cancellationToken).ConfigureAwait(false); - - var artistJsonPath = GetArtistJsonPath(_config.CommonApplicationPaths, artistMusicBrainzId); - - try - { - AddImages(list, artistJsonPath, cancellationToken); - } - catch (FileNotFoundException) - { - - } - catch (IOException) - { - - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - /// - /// Adds the images. - /// - /// The list. - /// The path. - /// The cancellation token. - private void AddImages(List list, string path, CancellationToken cancellationToken) - { - var obj = _jsonSerializer.DeserializeFromFile(path); - - PopulateImages(list, obj.artistbackground, ImageType.Backdrop, 1920, 1080); - PopulateImages(list, obj.artistthumb, ImageType.Primary, 500, 281); - PopulateImages(list, obj.hdmusiclogo, ImageType.Logo, 800, 310); - PopulateImages(list, obj.musicbanner, ImageType.Banner, 1000, 185); - PopulateImages(list, obj.musiclogo, ImageType.Logo, 400, 155); - PopulateImages(list, obj.hdmusicarts, ImageType.Art, 1000, 562); - PopulateImages(list, obj.musicarts, ImageType.Art, 500, 281); - } - - private void PopulateImages(List list, - List images, - ImageType type, - int width, - int height) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - - if (!string.IsNullOrEmpty(url)) - { - var likesString = i.likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - - public int Order => 0; - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); - } - - internal Task EnsureArtistJson(string musicBrainzId, CancellationToken cancellationToken) - { - var jsonPath = GetArtistJsonPath(_config.ApplicationPaths, musicBrainzId); - - var fileInfo = _fileSystem.GetFileSystemInfo(jsonPath); - - if (fileInfo.Exists) - { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadArtistJson(musicBrainzId, cancellationToken); - } - - /// - /// Downloads the artist data. - /// - /// The music brainz id. - /// The cancellation token. - /// Task{System.Boolean}. - internal async Task DownloadArtistJson(string musicBrainzId, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var url = string.Format(FanArtBaseUrl, ApiKey, musicBrainzId); - - var clientKey = FanartSeriesProvider.Current.GetFanartOptions().UserApiKey; - if (!string.IsNullOrWhiteSpace(clientKey)) - { - url += "&client_key=" + clientKey; - } - - var jsonPath = GetArtistJsonPath(_config.ApplicationPaths, musicBrainzId); - - Directory.CreateDirectory(Path.GetDirectoryName(jsonPath)); - - try - { - using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = true - - }, "GET").ConfigureAwait(false)) - { - using (var response = httpResponse.Content) - { - using (var saveFileStream = _fileSystem.GetFileStream(jsonPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await response.CopyToAsync(saveFileStream).ConfigureAwait(false); - } - } - } - } - catch (HttpException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - _jsonSerializer.SerializeToFile(new FanartArtistResponse(), jsonPath); - } - else - { - throw; - } - } - } - - /// - /// Gets the artist data path. - /// - /// The application paths. - /// The music brainz artist identifier. - /// System.String. - private static string GetArtistDataPath(IApplicationPaths appPaths, string musicBrainzArtistId) - { - var dataPath = Path.Combine(GetArtistDataPath(appPaths), musicBrainzArtistId); - - return dataPath; - } - - /// - /// Gets the artist data path. - /// - /// The application paths. - /// System.String. - internal static string GetArtistDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "fanart-music"); - - return dataPath; - } - - internal static string GetArtistJsonPath(IApplicationPaths appPaths, string musicBrainzArtistId) - { - var dataPath = GetArtistDataPath(appPaths, musicBrainzArtistId); - - return Path.Combine(dataPath, "fanart.json"); - } - - - public class FanartArtistImage - { - public string id { get; set; } - public string url { get; set; } - public string likes { get; set; } - public string disc { get; set; } - public string size { get; set; } - public string lang { get; set; } - } - - public class Album - { - public string release_group_id { get; set; } - public List cdart { get; set; } - public List albumcover { get; set; } - } - - public class FanartArtistResponse - { - public string name { get; set; } - public string mbid_id { get; set; } - public List artistthumb { get; set; } - public List artistbackground { get; set; } - public List hdmusiclogo { get; set; } - public List musicbanner { get; set; } - public List musiclogo { get; set; } - public List musicarts { get; set; } - public List hdmusicarts { get; set; } - public List albums { get; set; } - } - } -} diff --git a/MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs b/MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs deleted file mode 100644 index 58356910f6..0000000000 --- a/MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Providers.TV.FanArt -{ - public class FanArtSeasonProvider : IRemoteImageProvider, IHasOrder - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _json; - - public FanArtSeasonProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _json = json; - } - - public string Name => ProviderName; - - public static string ProviderName => "FanArt"; - - public bool Supports(BaseItem item) - { - return item is Season; - } - - public IEnumerable GetSupportedImages(BaseItem item) - { - return new List - { - ImageType.Backdrop, - ImageType.Thumb, - ImageType.Banner, - ImageType.Primary - }; - } - - public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var list = new List(); - - var season = (Season)item; - var series = season.Series; - - if (series != null) - { - var id = series.GetProviderId(MetadataProviders.Tvdb); - - if (!string.IsNullOrEmpty(id) && season.IndexNumber.HasValue) - { - // Bad id entered - try - { - await FanartSeriesProvider.Current.EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - var path = FanartSeriesProvider.Current.GetFanartJsonPath(id); - - try - { - AddImages(list, season.IndexNumber.Value, path, cancellationToken); - } - catch (FileNotFoundException) - { - // No biggie. Don't blow up - } - catch (IOException) - { - // No biggie. Don't blow up - } - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private void AddImages(List list, int seasonNumber, string path, CancellationToken cancellationToken) - { - var root = _json.DeserializeFromFile(path); - - AddImages(list, root, seasonNumber, cancellationToken); - } - - private void AddImages(List list, FanartSeriesProvider.RootObject obj, int seasonNumber, CancellationToken cancellationToken) - { - PopulateImages(list, obj.seasonposter, ImageType.Primary, 1000, 1426, seasonNumber); - PopulateImages(list, obj.seasonbanner, ImageType.Banner, 1000, 185, seasonNumber); - PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281, seasonNumber); - PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, seasonNumber); - } - - private void PopulateImages(List list, - List images, - ImageType type, - int width, - int height, - int seasonNumber) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - var season = i.season; - - if (!string.IsNullOrEmpty(url) && - !string.IsNullOrEmpty(season) && - int.TryParse(season, NumberStyles.Integer, _usCulture, out var imageSeasonNumber) && - seasonNumber == imageSeasonNumber) - { - var likesString = i.likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - - public int Order => 1; - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); - } - } -} diff --git a/MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs b/MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs deleted file mode 100644 index 49cd9596e4..0000000000 --- a/MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Music; - -namespace MediaBrowser.Providers.TV.FanArt -{ - public class FanartSeriesProvider : IRemoteImageProvider, IHasOrder - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _json; - - private const string FanArtBaseUrl = "https://webservice.fanart.tv/v3/tv/{1}?api_key={0}"; - - internal static FanartSeriesProvider Current { get; private set; } - - public FanartSeriesProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _json = json; - - Current = this; - } - - public string Name => ProviderName; - - public static string ProviderName => "FanArt"; - - public bool Supports(BaseItem item) - { - return item is Series; - } - - public IEnumerable GetSupportedImages(BaseItem item) - { - return new List - { - ImageType.Primary, - ImageType.Thumb, - ImageType.Art, - ImageType.Logo, - ImageType.Backdrop, - ImageType.Banner - }; - } - - public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var list = new List(); - - var series = (Series)item; - - var id = series.GetProviderId(MetadataProviders.Tvdb); - - if (!string.IsNullOrEmpty(id)) - { - // Bad id entered - try - { - await EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - var path = GetFanartJsonPath(id); - - try - { - AddImages(list, path, cancellationToken); - } - catch (FileNotFoundException) - { - // No biggie. Don't blow up - } - catch (IOException) - { - // No biggie. Don't blow up - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private void AddImages(List list, string path, CancellationToken cancellationToken) - { - var root = _json.DeserializeFromFile(path); - - AddImages(list, root, cancellationToken); - } - - private void AddImages(List list, RootObject obj, CancellationToken cancellationToken) - { - PopulateImages(list, obj.hdtvlogo, ImageType.Logo, 800, 310); - PopulateImages(list, obj.hdclearart, ImageType.Art, 1000, 562); - PopulateImages(list, obj.clearlogo, ImageType.Logo, 400, 155); - PopulateImages(list, obj.clearart, ImageType.Art, 500, 281); - PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, true); - PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281); - PopulateImages(list, obj.tvthumb, ImageType.Thumb, 500, 281); - PopulateImages(list, obj.tvbanner, ImageType.Banner, 1000, 185); - PopulateImages(list, obj.tvposter, ImageType.Primary, 1000, 1426); - } - - private void PopulateImages(List list, - List images, - ImageType type, - int width, - int height, - bool allowSeasonAll = false) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - var season = i.season; - - var isSeasonValid = string.IsNullOrEmpty(season) || - (allowSeasonAll && string.Equals(season, "all", StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrEmpty(url) && isSeasonValid) - { - var likesString = i.likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - - public int Order => 1; - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); - } - - /// - /// Gets the series data path. - /// - /// The app paths. - /// The series id. - /// System.String. - internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - /// - /// Gets the series data path. - /// - /// The app paths. - /// System.String. - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "fanart-tv"); - - return dataPath; - } - - public string GetFanartJsonPath(string tvdbId) - { - var dataPath = GetSeriesDataPath(_config.ApplicationPaths, tvdbId); - return Path.Combine(dataPath, "fanart.json"); - } - - private readonly SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); - internal async Task EnsureSeriesJson(string tvdbId, CancellationToken cancellationToken) - { - var path = GetFanartJsonPath(tvdbId); - - // Only allow one thread in here at a time since every season will be calling this method, possibly concurrently - await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return; - } - } - - await DownloadSeriesJson(tvdbId, cancellationToken).ConfigureAwait(false); - } - finally - { - _ensureSemaphore.Release(); - } - } - - public FanartOptions GetFanartOptions() - { - return _config.GetConfiguration("fanart"); - } - - /// - /// Downloads the series json. - /// - /// The TVDB identifier. - /// The cancellation token. - /// Task. - internal async Task DownloadSeriesJson(string tvdbId, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, tvdbId); - - var clientKey = GetFanartOptions().UserApiKey; - if (!string.IsNullOrWhiteSpace(clientKey)) - { - url += "&client_key=" + clientKey; - } - - var path = GetFanartJsonPath(tvdbId); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - try - { - using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = true - - }, "GET").ConfigureAwait(false)) - { - using (var response = httpResponse.Content) - { - using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await response.CopyToAsync(fileStream).ConfigureAwait(false); - } - } - } - } - catch (HttpException exception) - { - if (exception.StatusCode.HasValue && exception.StatusCode.Value == HttpStatusCode.NotFound) - { - // If the user has automatic updates enabled, save a dummy object to prevent repeated download attempts - _json.SerializeToFile(new RootObject(), path); - - return; - } - - throw; - } - } - - public class Image - { - public string id { get; set; } - public string url { get; set; } - public string lang { get; set; } - public string likes { get; set; } - public string season { get; set; } - } - - public class RootObject - { - public string name { get; set; } - public string thetvdb_id { get; set; } - public List clearlogo { get; set; } - public List hdtvlogo { get; set; } - public List clearart { get; set; } - public List showbackground { get; set; } - public List tvthumb { get; set; } - public List seasonposter { get; set; } - public List seasonthumb { get; set; } - public List hdclearart { get; set; } - public List tvbanner { get; set; } - public List characterart { get; set; } - public List tvposter { get; set; } - public List seasonbanner { get; set; } - } - } - - public class FanartConfigStore : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - Key = "fanart", - ConfigurationType = typeof(FanartOptions) - } - }; - } - } -} From a6f9ceedd82fe34a8d1f088d91f7c217ee070aad Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 10 May 2019 20:37:42 +0200 Subject: [PATCH 45/91] Fix more warnings --- Emby.Naming/Audio/AlbumParser.cs | 26 ++++--- Emby.Naming/Audio/MultiPartResult.cs | 2 + Emby.Naming/AudioBook/AudioBookFileInfo.cs | 29 ++++++- .../AudioBook/AudioBookFilePathParser.cs | 13 ++-- .../AudioBookFilePathParserResult.cs | 2 + Emby.Naming/AudioBook/AudioBookInfo.cs | 21 ++++-- .../AudioBook/AudioBookListResolver.cs | 2 +- Emby.Naming/AudioBook/AudioBookResolver.cs | 11 +-- Emby.Naming/Common/EpisodeExpression.cs | 17 ++++- Emby.Naming/Common/MediaType.cs | 2 + Emby.Naming/Common/NamingOptions.cs | 75 ++++++++++--------- Emby.Naming/Emby.Naming.csproj | 18 ++++- Emby.Naming/Extensions/StringExtensions.cs | 1 + Emby.Naming/StringExtensions.cs | 30 -------- Emby.Naming/Subtitles/SubtitleInfo.cs | 3 + Emby.Naming/TV/EpisodeInfo.cs | 11 +++ Emby.Naming/TV/EpisodePathParser.cs | 52 ++++++------- Emby.Naming/TV/EpisodePathParserResult.cs | 7 ++ Emby.Naming/TV/EpisodeResolver.cs | 12 ++- Emby.Naming/TV/SeasonPathParser.cs | 42 +++++------ Emby.Naming/TV/SeasonPathParserResult.cs | 2 + Emby.Naming/Video/CleanDateTimeParser.cs | 20 ++--- Emby.Naming/Video/ExtraResolver.cs | 2 - Emby.Naming/Video/FileStack.cs | 4 +- Emby.Naming/Video/Format3DParser.cs | 10 ++- Emby.Naming/Video/Format3DResult.cs | 12 +-- Emby.Naming/Video/StackResolver.cs | 19 +++-- Emby.Naming/Video/StubResolver.cs | 32 +++++--- Emby.Naming/Video/StubResult.cs | 1 + Emby.Naming/Video/StubTypeRule.cs | 1 + Emby.Naming/Video/VideoFileInfo.cs | 12 ++- Emby.Naming/Video/VideoInfo.cs | 4 + Emby.Naming/Video/VideoListResolver.cs | 40 +++++----- Emby.Naming/Video/VideoResolver.cs | 12 +-- .../Emby.Server.Implementations.csproj | 4 +- .../Library/LibraryManager.cs | 2 +- .../Library/Resolvers/TV/SeasonResolver.cs | 2 +- .../Library/Resolvers/TV/SeriesResolver.cs | 4 +- Jellyfin.Server/Jellyfin.Server.csproj | 6 +- Jellyfin.Server/Program.cs | 6 +- jellyfin.ruleset | 5 ++ 41 files changed, 339 insertions(+), 237 deletions(-) delete mode 100644 Emby.Naming/StringExtensions.cs diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index 7d029a9f49..e8d7655525 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -33,27 +33,29 @@ namespace Emby.Naming.Audio // Normalize // Remove whitespace - filename = filename.Replace("-", " "); - filename = filename.Replace(".", " "); - filename = filename.Replace("(", " "); - filename = filename.Replace(")", " "); + filename = filename.Replace('-', ' '); + filename = filename.Replace('.', ' '); + filename = filename.Replace('(', ' '); + filename = filename.Replace(')', ' '); filename = Regex.Replace(filename, @"\s+", " "); filename = filename.TrimStart(); foreach (var prefix in _options.AlbumStackingPrefixes) { - if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) == 0) + if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0) { - var tmp = filename.Substring(prefix.Length); + continue; + } + + var tmp = filename.Substring(prefix.Length); - tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty; + tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty; - if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - result.IsMultiPart = true; - break; - } + if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) + { + result.IsMultiPart = true; + break; } } diff --git a/Emby.Naming/Audio/MultiPartResult.cs b/Emby.Naming/Audio/MultiPartResult.cs index b1fa6e5639..00e4a9eb2e 100644 --- a/Emby.Naming/Audio/MultiPartResult.cs +++ b/Emby.Naming/Audio/MultiPartResult.cs @@ -7,11 +7,13 @@ namespace Emby.Naming.Audio /// /// The name. public string Name { get; set; } + /// /// Gets or sets the part. /// /// The part. public string Part { get; set; } + /// /// Gets or sets a value indicating whether this instance is multi part. /// diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs index de66a54022..326ea05ef3 100644 --- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs @@ -12,35 +12,56 @@ namespace Emby.Naming.AudioBook /// /// The path. public string Path { get; set; } + /// /// Gets or sets the container. /// /// The container. public string Container { get; set; } + /// /// Gets or sets the part number. /// /// The part number. public int? PartNumber { get; set; } + /// /// Gets or sets the chapter number. /// /// The chapter number. public int? ChapterNumber { get; set; } + /// /// Gets or sets the type. /// /// The type. public bool IsDirectory { get; set; } + /// public int CompareTo(AudioBookFileInfo other) { - if (ReferenceEquals(this, other)) return 0; - if (ReferenceEquals(null, other)) return 1; + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (ReferenceEquals(null, other)) + { + return 1; + } + var chapterNumberComparison = Nullable.Compare(ChapterNumber, other.ChapterNumber); - if (chapterNumberComparison != 0) return chapterNumberComparison; + if (chapterNumberComparison != 0) + { + return chapterNumberComparison; + } + var partNumberComparison = Nullable.Compare(PartNumber, other.PartNumber); - if (partNumberComparison != 0) return partNumberComparison; + if (partNumberComparison != 0) + { + return partNumberComparison; + } + return string.Compare(Path, other.Path, StringComparison.Ordinal); } } diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs index 5909797947..ea7f06c8cb 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs @@ -1,3 +1,4 @@ +using System; using System.Globalization; using System.IO; using System.Text.RegularExpressions; @@ -14,14 +15,13 @@ namespace Emby.Naming.AudioBook _options = options; } - public AudioBookFilePathParserResult Parse(string path, bool IsDirectory) + public AudioBookFilePathParserResult Parse(string path) { - var result = Parse(path); - return !result.Success ? new AudioBookFilePathParserResult() : result; - } + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } - private AudioBookFilePathParserResult Parse(string path) - { var result = new AudioBookFilePathParserResult(); var fileName = Path.GetFileNameWithoutExtension(path); foreach (var expression in _options.AudioBookPartsExpressions) @@ -40,6 +40,7 @@ namespace Emby.Naming.AudioBook } } } + if (!result.PartNumber.HasValue) { var value = match.Groups["part"]; diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs index 3a8e3c31f2..f845e82435 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs @@ -3,7 +3,9 @@ namespace Emby.Naming.AudioBook public class AudioBookFilePathParserResult { public int? PartNumber { get; set; } + public int? ChapterNumber { get; set; } + public bool Success { get; set; } } } diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs index f6e1d5be43..600d3f05da 100644 --- a/Emby.Naming/AudioBook/AudioBookInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookInfo.cs @@ -7,33 +7,40 @@ namespace Emby.Naming.AudioBook /// public class AudioBookInfo { + public AudioBookInfo() + { + Files = new List(); + Extras = new List(); + AlternateVersions = new List(); + } + /// /// Gets or sets the name. /// /// The name. public string Name { get; set; } + + /// + /// Gets or sets the year. + /// public int? Year { get; set; } + /// /// Gets or sets the files. /// /// The files. public List Files { get; set; } + /// /// Gets or sets the extras. /// /// The extras. public List Extras { get; set; } + /// /// Gets or sets the alternate versions. /// /// The alternate versions. public List AlternateVersions { get; set; } - - public AudioBookInfo() - { - Files = new List(); - Extras = new List(); - AlternateVersions = new List(); - } } } diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 4e3ad7cac8..414ef11830 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook _options = options; } - public IEnumerable Resolve(List files) + public IEnumerable Resolve(IEnumerable files) { var audioBookResolver = new AudioBookResolver(_options); diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index 67ab62e801..4a2b516d0e 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -24,19 +24,21 @@ namespace Emby.Naming.AudioBook return Resolve(path, true); } - public AudioBookFileInfo Resolve(string path, bool IsDirectory = false) + public AudioBookFileInfo Resolve(string path, bool isDirectory = false) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } - if (IsDirectory) // TODO + // TODO + if (isDirectory) { return null; } var extension = Path.GetExtension(path); + // Check supported extensions if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { @@ -45,8 +47,7 @@ namespace Emby.Naming.AudioBook var container = extension.TrimStart('.'); - var parsingResult = new AudioBookFilePathParser(_options) - .Parse(path, IsDirectory); + var parsingResult = new AudioBookFilePathParser(_options).Parse(path); return new AudioBookFileInfo { @@ -54,7 +55,7 @@ namespace Emby.Naming.AudioBook Container = container, PartNumber = parsingResult.PartNumber, ChapterNumber = parsingResult.ChapterNumber, - IsDirectory = IsDirectory + IsDirectory = isDirectory }; } } diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs index fd85bf76ad..136d8189dd 100644 --- a/Emby.Naming/Common/EpisodeExpression.cs +++ b/Emby.Naming/Common/EpisodeExpression.cs @@ -6,17 +6,28 @@ namespace Emby.Naming.Common public class EpisodeExpression { private string _expression; - public string Expression { get => _expression; - set { _expression = value; _regex = null; } } + private Regex _regex; + + public string Expression + { + get => _expression; + set + { + _expression = value; + _regex = null; + } + } public bool IsByDate { get; set; } + public bool IsOptimistic { get; set; } + public bool IsNamed { get; set; } + public bool SupportsAbsoluteEpisodeNumbers { get; set; } public string[] DateTimeFormats { get; set; } - private Regex _regex; public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); public EpisodeExpression(string expression, bool byDate) diff --git a/Emby.Naming/Common/MediaType.cs b/Emby.Naming/Common/MediaType.cs index 49cc9ee39b..a7b08bf793 100644 --- a/Emby.Naming/Common/MediaType.cs +++ b/Emby.Naming/Common/MediaType.cs @@ -6,10 +6,12 @@ namespace Emby.Naming.Common /// The audio /// Audio = 0, + /// /// The photo /// Photo = 1, + /// /// The video /// diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 2ef0208baf..88a9b46e63 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -8,19 +8,25 @@ namespace Emby.Naming.Common public class NamingOptions { public string[] AudioFileExtensions { get; set; } + public string[] AlbumStackingPrefixes { get; set; } public string[] SubtitleFileExtensions { get; set; } + public char[] SubtitleFlagDelimiters { get; set; } public string[] SubtitleForcedFlags { get; set; } + public string[] SubtitleDefaultFlags { get; set; } public EpisodeExpression[] EpisodeExpressions { get; set; } + public string[] EpisodeWithoutSeasonExpressions { get; set; } + public string[] EpisodeMultiPartExpressions { get; set; } public string[] VideoFileExtensions { get; set; } + public string[] StubFileExtensions { get; set; } public string[] AudioBookPartsExpressions { get; set; } @@ -28,12 +34,14 @@ namespace Emby.Naming.Common public StubTypeRule[] StubTypes { get; set; } public char[] VideoFlagDelimiters { get; set; } + public Format3DRule[] Format3DRules { get; set; } public string[] VideoFileStackingExpressions { get; set; } + public string[] CleanDateTimes { get; set; } - public string[] CleanStrings { get; set; } + public string[] CleanStrings { get; set; } public EpisodeExpression[] MultipleEpisodeExpressions { get; set; } @@ -41,7 +49,7 @@ namespace Emby.Naming.Common public NamingOptions() { - VideoFileExtensions = new string[] + VideoFileExtensions = new[] { ".m4v", ".3gp", @@ -106,53 +114,53 @@ namespace Emby.Naming.Common { new StubTypeRule { - StubType = "dvd", - Token = "dvd" + StubType = "dvd", + Token = "dvd" }, new StubTypeRule { - StubType = "hddvd", - Token = "hddvd" + StubType = "hddvd", + Token = "hddvd" }, new StubTypeRule { - StubType = "bluray", - Token = "bluray" + StubType = "bluray", + Token = "bluray" }, new StubTypeRule { - StubType = "bluray", - Token = "brrip" + StubType = "bluray", + Token = "brrip" }, new StubTypeRule { - StubType = "bluray", - Token = "bd25" + StubType = "bluray", + Token = "bd25" }, new StubTypeRule { - StubType = "bluray", - Token = "bd50" + StubType = "bluray", + Token = "bd50" }, new StubTypeRule { - StubType = "vhs", - Token = "vhs" + StubType = "vhs", + Token = "vhs" }, new StubTypeRule { - StubType = "tv", - Token = "HDTV" + StubType = "tv", + Token = "HDTV" }, new StubTypeRule { - StubType = "tv", - Token = "PDTV" + StubType = "tv", + Token = "PDTV" }, new StubTypeRule { - StubType = "tv", - Token = "DSR" + StubType = "tv", + Token = "DSR" } }; @@ -286,7 +294,7 @@ namespace Emby.Naming.Common new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true) { - DateTimeFormats = new [] + DateTimeFormats = new[] { "yyyy.MM.dd", "yyyy-MM-dd", @@ -295,7 +303,7 @@ namespace Emby.Naming.Common }, new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true) { - DateTimeFormats = new [] + DateTimeFormats = new[] { "dd.MM.yyyy", "dd-MM-yyyy", @@ -348,9 +356,7 @@ namespace Emby.Naming.Common }, // "1-12 episode title" - new EpisodeExpression(@"([0-9]+)-([0-9]+)") - { - }, + new EpisodeExpression(@"([0-9]+)-([0-9]+)"), // "01 - blah.avi", "01-blah.avi" new EpisodeExpression(@".*(\\|\/)(?\d{1,3})(-(?\d{2,3}))*\s?-\s?[^\\\/]*$") @@ -427,7 +433,7 @@ namespace Emby.Naming.Common Token = "_trailer", MediaType = MediaType.Video }, - new ExtraRule + new ExtraRule { ExtraType = "trailer", RuleType = ExtraRuleType.Suffix, @@ -462,7 +468,7 @@ namespace Emby.Naming.Common Token = "_sample", MediaType = MediaType.Video }, - new ExtraRule + new ExtraRule { ExtraType = "sample", RuleType = ExtraRuleType.Suffix, @@ -476,7 +482,6 @@ namespace Emby.Naming.Common Token = "theme", MediaType = MediaType.Audio }, - new ExtraRule { ExtraType = "scene", @@ -526,8 +531,8 @@ namespace Emby.Naming.Common Token = "-short", MediaType = MediaType.Video } - }; + Format3DRules = new[] { // Kodi rules: @@ -648,12 +653,10 @@ namespace Emby.Naming.Common @".*(\\|\/)(?((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?\d{1,4})[xX](?\d{1,3}))(-[xX]?[eE]?(?\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?[^\\\/]*)[sS](?\d{1,4})[xX\.]?[eE](?\d{1,3})((-| - )?[xXeE](?\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?[^\\\/]*)[sS](?\d{1,4})[xX\.]?[eE](?\d{1,3})(-[xX]?[eE]?(?\d{1,3}))+[^\\\/]*$" - }.Select(i => new EpisodeExpression(i) - { - IsNamed = true - - }).ToArray(); + { + IsNamed = true + }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index c448ec0ce6..699d893256 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -18,6 +18,22 @@ Jellyfin.Naming https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt https://github.com/jellyfin/jellyfin + true + + + + true + + + + + + + + + + + ../jellyfin.ruleset diff --git a/Emby.Naming/Extensions/StringExtensions.cs b/Emby.Naming/Extensions/StringExtensions.cs index 26c09aeb40..5512127a80 100644 --- a/Emby.Naming/Extensions/StringExtensions.cs +++ b/Emby.Naming/Extensions/StringExtensions.cs @@ -5,6 +5,7 @@ namespace Emby.Naming.Extensions { public static class StringExtensions { + // TODO: @bond remove this when moving to netstandard2.1 public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison) { var sb = new StringBuilder(); diff --git a/Emby.Naming/StringExtensions.cs b/Emby.Naming/StringExtensions.cs deleted file mode 100644 index 7c61922af3..0000000000 --- a/Emby.Naming/StringExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Text; - -namespace Emby.Naming -{ - internal static class StringExtensions - { - public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison) - { - var sb = new StringBuilder(); - - var previousIndex = 0; - var index = str.IndexOf(oldValue, comparison); - - while (index != -1) - { - sb.Append(str.Substring(previousIndex, index - previousIndex)); - sb.Append(newValue); - index += oldValue.Length; - - previousIndex = index; - index = str.IndexOf(oldValue, index, comparison); - } - - sb.Append(str.Substring(previousIndex)); - - return sb.ToString(); - } - } -} diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/Subtitles/SubtitleInfo.cs index e4709dfbb6..96fce04d76 100644 --- a/Emby.Naming/Subtitles/SubtitleInfo.cs +++ b/Emby.Naming/Subtitles/SubtitleInfo.cs @@ -7,16 +7,19 @@ namespace Emby.Naming.Subtitles /// /// The path. public string Path { get; set; } + /// /// Gets or sets the language. /// /// The language. public string Language { get; set; } + /// /// Gets or sets a value indicating whether this instance is default. /// /// true if this instance is default; otherwise, false. public bool IsDefault { get; set; } + /// /// Gets or sets a value indicating whether this instance is forced. /// diff --git a/Emby.Naming/TV/EpisodeInfo.cs b/Emby.Naming/TV/EpisodeInfo.cs index c8aca7a6f3..de79b8bbaf 100644 --- a/Emby.Naming/TV/EpisodeInfo.cs +++ b/Emby.Naming/TV/EpisodeInfo.cs @@ -7,31 +7,37 @@ namespace Emby.Naming.TV /// /// The path. public string Path { get; set; } + /// /// Gets or sets the container. /// /// The container. public string Container { get; set; } + /// /// Gets or sets the name of the series. /// /// The name of the series. public string SeriesName { get; set; } + /// /// Gets or sets the format3 d. /// /// The format3 d. public string Format3D { get; set; } + /// /// Gets or sets a value indicating whether [is3 d]. /// /// true if [is3 d]; otherwise, false. public bool Is3D { get; set; } + /// /// Gets or sets a value indicating whether this instance is stub. /// /// true if this instance is stub; otherwise, false. public bool IsStub { get; set; } + /// /// Gets or sets the type of the stub. /// @@ -39,12 +45,17 @@ namespace Emby.Naming.TV public string StubType { get; set; } public int? SeasonNumber { get; set; } + public int? EpisodeNumber { get; set; } + public int? EndingEpsiodeNumber { get; set; } public int? Year { get; set; } + public int? Month { get; set; } + public int? Day { get; set; } + public bool IsByDate { get; set; } } } diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index a8f81a3b85..812bc970ed 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -15,12 +15,12 @@ namespace Emby.Naming.TV _options = options; } - public EpisodePathParserResult Parse(string path, bool IsDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true) + public EpisodePathParserResult Parse(string path, bool isDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true) { // Added to be able to use regex patterns which require a file extension. // There were no failed tests without this block, but to be safe, we can keep it until // the regex which require file extensions are modified so that they don't need them. - if (IsDirectory) + if (isDirectory) { path += ".mp4"; } @@ -29,28 +29,20 @@ namespace Emby.Naming.TV foreach (var expression in _options.EpisodeExpressions) { - if (supportsAbsoluteNumbers.HasValue) + if (supportsAbsoluteNumbers.HasValue + && expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value) { - if (expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value) - { - continue; - } + continue; } - if (isNamed.HasValue) + if (isNamed.HasValue && expression.IsNamed != isNamed.Value) { - if (expression.IsNamed != isNamed.Value) - { - continue; - } + continue; } - if (isOptimistic.HasValue) + if (isOptimistic.HasValue && expression.IsOptimistic != isOptimistic.Value) { - if (expression.IsOptimistic != isOptimistic.Value) - { - continue; - } + continue; } var currentResult = Parse(path, expression); @@ -97,7 +89,8 @@ namespace Emby.Naming.TV DateTime date; if (expression.DateTimeFormats.Length > 0) { - if (DateTime.TryParseExact(match.Groups[0].Value, + if (DateTime.TryParseExact( + match.Groups[0].Value, expression.DateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, @@ -109,17 +102,15 @@ namespace Emby.Naming.TV result.Success = true; } } - else + else if (DateTime.TryParse(match.Groups[0].Value, out date)) { - if (DateTime.TryParse(match.Groups[0].Value, out date)) - { - result.Year = date.Year; - result.Month = date.Month; - result.Day = date.Day; - result.Success = true; - } + result.Year = date.Year; + result.Month = date.Month; + result.Day = date.Day; + result.Success = true; } + // TODO: Only consider success if date successfully parsed? result.Success = true; } @@ -142,7 +133,8 @@ namespace Emby.Naming.TV // or a 'p' or 'i' as what you would get with a pixel resolution specification. // It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108 int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length; - if (nextIndex >= name.Length || "0123456789iIpP".IndexOf(name[nextIndex]) == -1) + if (nextIndex >= name.Length + || "0123456789iIpP".IndexOf(name[nextIndex]) == -1) { if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { @@ -160,6 +152,7 @@ namespace Emby.Naming.TV { result.SeasonNumber = num; } + if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; @@ -171,8 +164,11 @@ namespace Emby.Naming.TV // Invalidate match when the season is 200 through 1927 or above 2500 // because it is an error unless the TV show is intentionally using false season numbers. // It avoids erroneous parsing of something like "Series Special (1920x1080).mkv" as being season 1920 episode 1080. - if (result.SeasonNumber >= 200 && result.SeasonNumber < 1928 || result.SeasonNumber > 2500) + if ((result.SeasonNumber >= 200 && result.SeasonNumber < 1928) + || result.SeasonNumber > 2500) + { result.Success = false; + } result.IsByDate = expression.IsByDate; } diff --git a/Emby.Naming/TV/EpisodePathParserResult.cs b/Emby.Naming/TV/EpisodePathParserResult.cs index e1a48bfbc9..996edfc506 100644 --- a/Emby.Naming/TV/EpisodePathParserResult.cs +++ b/Emby.Naming/TV/EpisodePathParserResult.cs @@ -3,14 +3,21 @@ namespace Emby.Naming.TV public class EpisodePathParserResult { public int? SeasonNumber { get; set; } + public int? EpisodeNumber { get; set; } + public int? EndingEpsiodeNumber { get; set; } + public string SeriesName { get; set; } + public bool Success { get; set; } public bool IsByDate { get; set; } + public int? Year { get; set; } + public int? Month { get; set; } + public int? Day { get; set; } } } diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index fccf9bdecb..2d7bcb6382 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -15,7 +15,13 @@ namespace Emby.Naming.TV _options = options; } - public EpisodeInfo Resolve(string path, bool IsDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true) + public EpisodeInfo Resolve( + string path, + bool isDirectory, + bool? isNamed = null, + bool? isOptimistic = null, + bool? supportsAbsoluteNumbers = null, + bool fillExtendedInfo = true) { if (string.IsNullOrEmpty(path)) { @@ -26,7 +32,7 @@ namespace Emby.Naming.TV string container = null; string stubType = null; - if (!IsDirectory) + if (!isDirectory) { var extension = Path.GetExtension(path); // Check supported extensions @@ -52,7 +58,7 @@ namespace Emby.Naming.TV var format3DResult = new Format3DParser(_options).Parse(flags); var parsingResult = new EpisodePathParser(_options) - .Parse(path, IsDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); + .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); return new EpisodeInfo { diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index f1dcc50b89..e81b2bb349 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -3,30 +3,24 @@ using System.Globalization; using System.IO; using System.Linq; using Emby.Naming.Common; +using Emby.Naming.Extensions; namespace Emby.Naming.TV { public class SeasonPathParser { - private readonly NamingOptions _options; - - public SeasonPathParser(NamingOptions options) - { - _options = options; - } - public SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) { var result = new SeasonPathParserResult(); var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); - result.SeasonNumber = seasonNumberInfo.Item1; + result.SeasonNumber = seasonNumberInfo.seasonNumber; if (result.SeasonNumber.HasValue) { result.Success = true; - result.IsSeasonFolder = seasonNumberInfo.Item2; + result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder; } return result; @@ -35,7 +29,7 @@ namespace Emby.Naming.TV /// /// A season folder must contain one of these somewhere in the name /// - private static readonly string[] SeasonFolderNames = + private static readonly string[] _seasonFolderNames = { "season", "sæson", @@ -54,19 +48,23 @@ namespace Emby.Naming.TV /// if set to true [support special aliases]. /// if set to true [support numeric season folders]. /// System.Nullable{System.Int32}. - private Tuple GetSeasonNumberFromPath(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) + private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath( + string path, + bool supportSpecialAliases, + bool supportNumericSeasonFolders) { - var filename = Path.GetFileName(path); + var filename = Path.GetFileName(path) ?? string.Empty; if (supportSpecialAliases) { if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) { - return new Tuple(0, true); + return (0, true); } + if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase)) { - return new Tuple(0, true); + return (0, true); } } @@ -74,7 +72,7 @@ namespace Emby.Naming.TV { if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { - return new Tuple(val, true); + return (val, true); } } @@ -84,12 +82,12 @@ namespace Emby.Naming.TV if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { - return new Tuple(val, true); + return (val, true); } } // Look for one of the season folder names - foreach (var name in SeasonFolderNames) + foreach (var name in _seasonFolderNames) { var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase); @@ -107,10 +105,10 @@ namespace Emby.Naming.TV var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); var resultNumber = parts.Select(GetSeasonNumberFromPart).FirstOrDefault(i => i.HasValue); - return new Tuple(resultNumber, true); + return (resultNumber, true); } - private int? GetSeasonNumberFromPart(string part) + private static int? GetSeasonNumberFromPart(string part) { if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase)) { @@ -132,7 +130,7 @@ namespace Emby.Naming.TV /// /// The path. /// System.Nullable{System.Int32}. - private Tuple GetSeasonNumberFromPathSubstring(string path) + private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(string path) { var numericStart = -1; var length = 0; @@ -174,10 +172,10 @@ namespace Emby.Naming.TV if (numericStart == -1) { - return new Tuple(null, isSeasonFolder); + return (null, isSeasonFolder); } - return new Tuple(int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder); + return (int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder); } } } diff --git a/Emby.Naming/TV/SeasonPathParserResult.cs b/Emby.Naming/TV/SeasonPathParserResult.cs index eab27a4a5f..548dbd5d22 100644 --- a/Emby.Naming/TV/SeasonPathParserResult.cs +++ b/Emby.Naming/TV/SeasonPathParserResult.cs @@ -7,11 +7,13 @@ namespace Emby.Naming.TV /// /// The season number. public int? SeasonNumber { get; set; } + /// /// Gets or sets a value indicating whether this is success. /// /// true if success; otherwise, false. public bool Success { get; set; } + public bool IsSeasonFolder { get; set; } } } diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index 74807ef53f..25fa09c488 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -27,8 +27,8 @@ namespace Emby.Naming.Video { var extension = Path.GetExtension(name) ?? string.Empty; // Check supported extensions - if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) && - !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) + && !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { // Dummy up a file extension because the expressions will fail without one // This is tricky because we can't just check Path.GetExtension for empty @@ -38,7 +38,6 @@ namespace Emby.Naming.Video } catch (ArgumentException) { - } var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i)) @@ -69,14 +68,15 @@ namespace Emby.Naming.Video var match = expression.Match(name); - if (match.Success && match.Groups.Count == 4) + if (match.Success + && match.Groups.Count == 4 + && match.Groups[1].Success + && match.Groups[2].Success + && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { - if (match.Groups[1].Success && match.Groups[2].Success && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) - { - name = match.Groups[1].Value; - result.Year = year; - result.HasChanged = true; - } + name = match.Groups[1].Value; + result.Year = year; + result.HasChanged = true; } result.Name = name; diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 3459b689ac..9f70494d01 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -56,7 +56,6 @@ namespace Emby.Naming.Video result.Rule = rule; } } - else if (rule.RuleType == ExtraRuleType.Suffix) { var filename = Path.GetFileNameWithoutExtension(path); @@ -67,7 +66,6 @@ namespace Emby.Naming.Video result.Rule = rule; } } - else if (rule.RuleType == ExtraRuleType.Regex) { var filename = Path.GetFileName(path); diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs index 2df1e9aed1..584bdf2d2f 100644 --- a/Emby.Naming/Video/FileStack.cs +++ b/Emby.Naming/Video/FileStack.cs @@ -15,9 +15,9 @@ namespace Emby.Naming.Video Files = new List(); } - public bool ContainsFile(string file, bool IsDirectory) + public bool ContainsFile(string file, bool isDirectory) { - if (IsDirectoryStack == IsDirectory) + if (IsDirectoryStack == isDirectory) { return Files.Contains(file, StringComparer.OrdinalIgnoreCase); } diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index e6f830c583..333a48641e 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -15,10 +15,12 @@ namespace Emby.Naming.Video public Format3DResult Parse(string path) { - var delimeters = _options.VideoFlagDelimiters.ToList(); - delimeters.Add(' '); + int oldLen = _options.VideoFlagDelimiters.Length; + var delimeters = new char[oldLen + 1]; + _options.VideoFlagDelimiters.CopyTo(delimeters, 0); + delimeters[oldLen] = ' '; - return Parse(new FlagParser(_options).GetFlags(path, delimeters.ToArray())); + return Parse(new FlagParser(_options).GetFlags(path, delimeters)); } internal Format3DResult Parse(string[] videoFlags) @@ -66,8 +68,10 @@ namespace Emby.Naming.Video format = flag; result.Tokens.Add(rule.Token); } + break; } + foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase); } diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs index e12494079c..40fc31e082 100644 --- a/Emby.Naming/Video/Format3DResult.cs +++ b/Emby.Naming/Video/Format3DResult.cs @@ -4,25 +4,27 @@ namespace Emby.Naming.Video { public class Format3DResult { + public Format3DResult() + { + Tokens = new List(); + } + /// /// Gets or sets a value indicating whether [is3 d]. /// /// true if [is3 d]; otherwise, false. public bool Is3D { get; set; } + /// /// Gets or sets the format3 d. /// /// The format3 d. public string Format3D { get; set; } + /// /// Gets or sets the tokens. /// /// The tokens. public List Tokens { get; set; } - - public Format3DResult() - { - Tokens = new List(); - } } } diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index 4893002c1b..b8ba42da4f 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -40,17 +40,24 @@ namespace Emby.Naming.Video var result = new StackResult(); foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName))) { - var stack = new FileStack(); - stack.Name = Path.GetFileName(directory.Key); - stack.IsDirectoryStack = false; + var stack = new FileStack() + { + Name = Path.GetFileName(directory.Key), + IsDirectoryStack = false + }; foreach (var file in directory) { if (file.IsDirectory) + { continue; + } + stack.Files.Add(file.FullName); } + result.Stacks.Add(stack); } + return result; } @@ -114,16 +121,16 @@ namespace Emby.Naming.Video { if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) && - string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) + && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase)) { if (stack.Files.Count == 0) { stack.Name = title1 + ignore1; stack.IsDirectoryStack = file1.IsDirectory; - //stack.Name = title1 + ignore1 + extension1; stack.Files.Add(file1.FullName); } + stack.Files.Add(file2.FullName); } else diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index f86bcbdf04..b78244cb33 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -9,24 +9,32 @@ namespace Emby.Naming.Video { public static StubResult ResolveFile(string path, NamingOptions options) { - var result = new StubResult(); - var extension = Path.GetExtension(path) ?? string.Empty; + if (path == null) + { + return default(StubResult); + } + + var extension = Path.GetExtension(path); - if (options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - result.IsStub = true; + return default(StubResult); + } - path = Path.GetFileNameWithoutExtension(path); + var result = new StubResult() + { + IsStub = true + }; - var token = (Path.GetExtension(path) ?? string.Empty).TrimStart('.'); + path = Path.GetFileNameWithoutExtension(path); + var token = Path.GetExtension(path).TrimStart('.'); - foreach (var rule in options.StubTypes) + foreach (var rule in options.StubTypes) + { + if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) - { - result.StubType = rule.StubType; - break; - } + result.StubType = rule.StubType; + break; } } diff --git a/Emby.Naming/Video/StubResult.cs b/Emby.Naming/Video/StubResult.cs index 7f9509ca56..7a62e7b981 100644 --- a/Emby.Naming/Video/StubResult.cs +++ b/Emby.Naming/Video/StubResult.cs @@ -7,6 +7,7 @@ namespace Emby.Naming.Video /// /// true if this instance is stub; otherwise, false. public bool IsStub { get; set; } + /// /// Gets or sets the type of the stub. /// diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs index b46050085d..d765321504 100644 --- a/Emby.Naming/Video/StubTypeRule.cs +++ b/Emby.Naming/Video/StubTypeRule.cs @@ -7,6 +7,7 @@ namespace Emby.Naming.Video /// /// The token. public string Token { get; set; } + /// /// Gets or sets the type of the stub. /// diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index 6a29ada7ea..78f688ca8a 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -1,4 +1,3 @@ - namespace Emby.Naming.Video { /// @@ -11,56 +10,67 @@ namespace Emby.Naming.Video /// /// The path. public string Path { get; set; } + /// /// Gets or sets the container. /// /// The container. public string Container { get; set; } + /// /// Gets or sets the name. /// /// The name. public string Name { get; set; } + /// /// Gets or sets the year. /// /// The year. public int? Year { get; set; } + /// /// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc. /// /// The type of the extra. public string ExtraType { get; set; } + /// /// Gets or sets the extra rule. /// /// The extra rule. public ExtraRule ExtraRule { get; set; } + /// /// Gets or sets the format3 d. /// /// The format3 d. public string Format3D { get; set; } + /// /// Gets or sets a value indicating whether [is3 d]. /// /// true if [is3 d]; otherwise, false. public bool Is3D { get; set; } + /// /// Gets or sets a value indicating whether this instance is stub. /// /// true if this instance is stub; otherwise, false. public bool IsStub { get; set; } + /// /// Gets or sets the type of the stub. /// /// The type of the stub. public string StubType { get; set; } + /// /// Gets or sets the type. /// /// The type. public bool IsDirectory { get; set; } + /// /// Gets the file name without extension. /// diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs index d96d0e7571..2e456bda2b 100644 --- a/Emby.Naming/Video/VideoInfo.cs +++ b/Emby.Naming/Video/VideoInfo.cs @@ -12,21 +12,25 @@ namespace Emby.Naming.Video /// /// The name. public string Name { get; set; } + /// /// Gets or sets the year. /// /// The year. public int? Year { get; set; } + /// /// Gets or sets the files. /// /// The files. public List Files { get; set; } + /// /// Gets or sets the extras. /// /// The extras. public List Extras { get; set; } + /// /// Gets or sets the alternate versions. /// diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index afedc30ef2..5fa0041e07 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -53,7 +53,7 @@ namespace Emby.Naming.Video Name = stack.Name }; - info.Year = info.Files.First().Year; + info.Year = info.Files[0].Year; var extraBaseNames = new List { @@ -87,7 +87,7 @@ namespace Emby.Naming.Video Name = media.Name }; - info.Year = info.Files.First().Year; + info.Year = info.Files[0].Year; var extras = GetExtras(remainingFiles, new List { media.FileNameWithoutExtension }); @@ -115,7 +115,7 @@ namespace Emby.Naming.Video if (!string.IsNullOrEmpty(parentPath)) { - var folderName = Path.GetFileName(Path.GetDirectoryName(videoPath)); + var folderName = Path.GetFileName(parentPath); if (!string.IsNullOrEmpty(folderName)) { var extras = GetExtras(remainingFiles, new List { folderName }); @@ -163,9 +163,7 @@ namespace Emby.Naming.Video Year = i.Year })); - var orderedList = list.OrderBy(i => i.Name); - - return orderedList; + return list.OrderBy(i => i.Name); } private IEnumerable GetVideosGroupedByVersion(List videos) @@ -179,23 +177,21 @@ namespace Emby.Naming.Video var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path)); - if (!string.IsNullOrEmpty(folderName) && folderName.Length > 1) + if (!string.IsNullOrEmpty(folderName) + && folderName.Length > 1 + && videos.All(i => i.Files.Count == 1 + && IsEligibleForMultiVersion(folderName, i.Files[0].Path)) + && HaveSameYear(videos)) { - if (videos.All(i => i.Files.Count == 1 && IsEligibleForMultiVersion(folderName, i.Files[0].Path))) - { - if (HaveSameYear(videos)) - { - var ordered = videos.OrderBy(i => i.Name).ToList(); + var ordered = videos.OrderBy(i => i.Name).ToList(); - list.Add(ordered[0]); + list.Add(ordered[0]); - list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList(); - list[0].Name = folderName; - list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); + list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList(); + list[0].Name = folderName; + list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); - return list; - } - } + return list; } return videos; @@ -213,9 +209,9 @@ namespace Emby.Naming.Video if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { testFilename = testFilename.Substring(folderName.Length).Trim(); - return string.IsNullOrEmpty(testFilename) || - testFilename.StartsWith("-") || - string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)) ; + return string.IsNullOrEmpty(testFilename) + || testFilename[0] == '-' + || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); } return false; diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index a673156517..02a25c4b5b 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -38,10 +38,11 @@ namespace Emby.Naming.Video /// Resolves the specified path. /// /// The path. - /// if set to true [is folder]. + /// if set to true [is folder]. + /// Whether or not the name should be parsed for info /// VideoFileInfo. /// path - public VideoFileInfo Resolve(string path, bool IsDirectory, bool parseName = true) + public VideoFileInfo Resolve(string path, bool isDirectory, bool parseName = true) { if (string.IsNullOrEmpty(path)) { @@ -52,9 +53,10 @@ namespace Emby.Naming.Video string container = null; string stubType = null; - if (!IsDirectory) + if (!isDirectory) { var extension = Path.GetExtension(path); + // Check supported extensions if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { @@ -79,7 +81,7 @@ namespace Emby.Naming.Video var extraResult = new ExtraResolver(_options).GetExtraInfo(path); - var name = IsDirectory + var name = isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); @@ -108,7 +110,7 @@ namespace Emby.Naming.Video Is3D = format3DResult.Is3D, Format3D = format3DResult.Format3D, ExtraType = extraResult.ExtraType, - IsDirectory = IsDirectory, + IsDirectory = isDirectory, ExtraRule = extraResult.Rule }; } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 2c79624524..d4e17c42ae 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -52,8 +52,8 @@ - - + + diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 1673e37776..4b5063ada6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2368,7 +2368,7 @@ namespace Emby.Server.Implementations.Library public int? GetSeasonNumberFromPath(string path) { - return new SeasonPathParser(GetNamingOptions()).Parse(path, true, true).SeasonNumber; + return new SeasonPathParser().Parse(path, true, true).SeasonNumber; } public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index ce1386e91a..3b9e48d978 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; - var seasonParserResult = new SeasonPathParser(namingOptions).Parse(path, true, true); + var seasonParserResult = new SeasonPathParser().Parse(path, true, true); var season = new Season { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 5c95534ec1..1f873d7c6d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -194,9 +194,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// true if [is season folder] [the specified path]; otherwise, false. private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager) { - var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); - - var seasonNumber = new SeasonPathParser(namingOptions).Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = new SeasonPathParser().Parse(path, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 9346a2d254..81f145abff 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -12,7 +12,7 @@ latest - SA1600;SA1601;CS1591 + SA1600;SA1601;SA1629;CS1591 true @@ -26,8 +26,8 @@ - - + + diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index fab584befc..d8ca121176 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -122,8 +122,12 @@ namespace Jellyfin.Server // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); +// CA5359: Do Not Disable Certificate Validation +#pragma warning disable CA5359 + // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); +#pragma warning restore CA5359 var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths); @@ -368,7 +372,7 @@ namespace Jellyfin.Server } catch (Exception ex) { - _logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}"); + _logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder."); } return new NullImageEncoder(); diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 262121a325..0a60c8c7a2 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -14,12 +14,17 @@ + + + + + From 08ac5b6ec3ce829c6691f1751e86480ace3d4b93 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 18 May 2019 13:43:28 +0200 Subject: [PATCH 46/91] Fix build --- Emby.Naming/Emby.Naming.csproj | 4 ---- Emby.Naming/TV/EpisodePathParser.cs | 1 - 2 files changed, 5 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 699d893256..6e05eb7952 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -21,10 +21,6 @@ true - - true - - diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index 812bc970ed..a98e8221a5 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -110,7 +110,6 @@ namespace Emby.Naming.TV result.Success = true; } - // TODO: Only consider success if date successfully parsed? result.Success = true; } From 430483c7a1206e1e4fa6402f67c291931c55c4f0 Mon Sep 17 00:00:00 2001 From: dracocephalum Date: Thu, 30 May 2019 06:09:49 +0000 Subject: [PATCH 47/91] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/ --- Emby.Server.Implementations/Localization/Core/zh-TW.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index effff5566e..293fc28a86 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -89,5 +89,8 @@ "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", - "VersionNumber": "版本 {0}" + "VersionNumber": "版本 {0}", + "HeaderRecordingGroups": "錄製組", + "Inherit": "繼承", + "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕" } From 3431a85adf9a5f9fbcd2ce4551af384c050051e1 Mon Sep 17 00:00:00 2001 From: exveria1015 Date: Thu, 30 May 2019 22:28:51 +0000 Subject: [PATCH 48/91] Translated using Weblate (Japanese) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- .../Localization/Core/ja.json | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 0967ef424b..53a43a1252 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -1 +1,96 @@ -{} +{ + "Albums": "アルバム", + "AppDeviceValues": "アプリ: {0}, デバイス: {1}", + "Application": "アプリケーション", + "Artists": "アーティスト", + "AuthenticationSucceededWithUserName": "{0} 認証に成功しました", + "Books": "ブック", + "CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました", + "Channels": "チャンネル", + "ChapterNameValue": "チャプター {0}", + "Collections": "コレクション", + "DeviceOfflineWithName": "{0} が切断されました", + "DeviceOnlineWithName": "{0} が接続されました", + "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0}によって失敗しました", + "Favorites": "お気に入り", + "Folders": "フォルダ", + "Genres": "ジャンル", + "HeaderAlbumArtists": "アルバムアーティスト", + "HeaderCameraUploads": "カメラアップロード", + "HeaderContinueWatching": "視聴中", + "HeaderFavoriteAlbums": "お気に入りのアルバム", + "HeaderFavoriteArtists": "お気に入りのアーティスト", + "HeaderFavoriteEpisodes": "お気に入りのエピソード", + "HeaderFavoriteShows": "お気に入りの番組", + "HeaderFavoriteSongs": "お気に入りの曲", + "HeaderLiveTV": "ライブ テレビ", + "HeaderNextUp": "次", + "HeaderRecordingGroups": "レコーディンググループ", + "HomeVideos": "ホームビデオ", + "Inherit": "継承", + "ItemAddedWithName": "{0} をライブラリに追加しました", + "ItemRemovedWithName": "{0} をライブラリから削除しました", + "LabelIpAddressValue": "IPアドレス: {0}", + "LabelRunningTimeValue": "稼働時間: {0}", + "Latest": "最新", + "MessageApplicationUpdated": "Jellyfin Server が更新されました", + "MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました", + "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました", + "MessageServerConfigurationUpdated": "サーバー設定が更新されました", + "MixedContent": "ミックスコンテンツ", + "Movies": "ムービー", + "Music": "ミュージック", + "MusicVideos": "ミュージックビデオ", + "NameInstallFailed": "{0}のインストールに失敗しました", + "NameSeasonNumber": "シーズン {0}", + "NameSeasonUnknown": "不明なシーズン", + "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。", + "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります", + "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です", + "NotificationOptionAudioPlayback": "オーディオの再生を開始", + "NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました", + "NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました", + "NotificationOptionInstallationFailed": "インストール失敗", + "NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました", + "NotificationOptionPluginError": "プラグインに障害が発生しました", + "NotificationOptionPluginInstalled": "プラグインがインストールされました", + "NotificationOptionPluginUninstalled": "プラグインがアンインストールされました", + "NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました", + "NotificationOptionServerRestartRequired": "サーバーを再起動してください", + "NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗", + "NotificationOptionUserLockedOut": "ユーザーはロックされています", + "NotificationOptionVideoPlayback": "ビデオの再生を開始しました", + "NotificationOptionVideoPlaybackStopped": "ビデオを停止しました", + "Photos": "フォト", + "Playlists": "プレイリスト", + "Plugin": "プラグイン", + "PluginInstalledWithName": "{0} がインストールされました", + "PluginUninstalledWithName": "{0} がアンインストールされました", + "PluginUpdatedWithName": "{0} が更新されました", + "ProviderValue": "プロバイダ: {0}", + "ScheduledTaskFailedWithName": "{0} が失敗しました", + "ScheduledTaskStartedWithName": "{0} が開始されました", + "ServerNameNeedsToBeRestarted": "{0} を再起動してください", + "Shows": "番組", + "Songs": "曲", + "StartupEmbyServerIsLoading": "Jellyfin Server は現在読み込み中です。しばらくしてからもう一度お試しください。", + "SubtitleDownloadFailureFromForItem": "{0} から {1}の字幕のダウンロードに失敗しました", + "SubtitlesDownloadedForItem": "{0} の字幕がダウンロードされました", + "Sync": "同期", + "System": "システム", + "TvShows": "テレビ番組", + "User": "ユーザー", + "UserCreatedWithName": "ユーザー {0} が作成されました", + "UserDeletedWithName": "User {0} を削除しました", + "UserDownloadingItemWithValues": "{0} が {1} をダウンロードしています", + "UserLockedOutWithName": "ユーザー {0} はロックされています", + "UserOfflineFromDevice": "{0} は {1} から切断しました", + "UserOnlineFromDevice": "{0} は {1} からオンラインになりました", + "UserPasswordChangedWithName": "ユーザー {0} のパスワードは変更されました", + "UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました", + "UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています", + "UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました", + "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました", + "ValueSpecialEpisodeName": "スペシャル - {0}", + "VersionNumber": "バージョン {0}" +} From a2d9420139639ff339daf77c6658bf2f22502110 Mon Sep 17 00:00:00 2001 From: Hyunsu Nam Date: Fri, 31 May 2019 01:10:13 +0000 Subject: [PATCH 49/91] Translated using Weblate (Korean) Currently translated at 97.8% (92 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/ --- .../Localization/Core/ko.json | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 21808fd18a..5a7ba8ba7b 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -1,88 +1,88 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", + "Albums": "앨범", + "AppDeviceValues": "앱: {0}, 디바이스: {1}", + "Application": "애플리케이션", + "Artists": "아티스트", + "AuthenticationSucceededWithUserName": "{0} 인증에 성공했습니다.", + "Books": "책", + "CameraImageUploadedFrom": "새로운 카메라 이미지가 {0}에서 업로드되었습니다.", + "Channels": "채널", + "ChapterNameValue": "챕터 {0}", + "Collections": "컬렉션", + "DeviceOfflineWithName": "{0}가 접속이 끊어졌습니다.", + "DeviceOnlineWithName": "{0}가 접속되었습니다.", + "FailedLoginAttemptWithUserName": "{0}에서 로그인이 실패했습니다.", + "Favorites": "즐겨찾기", + "Folders": "폴더", + "Genres": "장르", "HeaderAlbumArtists": "앨범 아티스트", - "HeaderCameraUploads": "Camera Uploads", + "HeaderCameraUploads": "카메라 업로드", "HeaderContinueWatching": "계속 시청하기", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", + "HeaderFavoriteAlbums": "좋아하는 앨범", + "HeaderFavoriteArtists": "좋아하는 아티스트", "HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteShows": "즐겨찾는 쇼", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", - "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", - "Songs": "Songs", + "HeaderFavoriteSongs": "좋아하는 노래", + "HeaderLiveTV": "TV 방송", + "HeaderNextUp": "다음으로", + "HeaderRecordingGroups": "녹화 그룹", + "HomeVideos": "홈 비디오", + "Inherit": "상속", + "ItemAddedWithName": "{0} 라이브러리에 추가됨", + "ItemRemovedWithName": "{0} 라이브러리에서 제거됨", + "LabelIpAddressValue": "IP 주소: {0}", + "LabelRunningTimeValue": "상영 시간: {0}", + "Latest": "최근", + "MessageApplicationUpdated": "Jellyfin 서버 업데이트됨", + "MessageApplicationUpdatedTo": "Jellyfin 서버가 {0}로 업데이트됨", + "MessageNamedServerConfigurationUpdatedWithValue": "서버 환경 설정 {0} 섹션 업데이트 됨", + "MessageServerConfigurationUpdated": "서버 환경 설정 업데이드됨", + "MixedContent": "혼합 콘텐츠", + "Movies": "영화", + "Music": "음악", + "MusicVideos": "뮤직 비디오", + "NameInstallFailed": "{0} 설치 실패.", + "NameSeasonNumber": "시즌 {0}", + "NameSeasonUnknown": "알 수 없는 시즌", + "NewVersionIsAvailable": "새 버전의 Jellyfin 서버를 사용할 수 있습니다.", + "NotificationOptionApplicationUpdateAvailable": "애플리케이션 업데이트 사용 가능", + "NotificationOptionApplicationUpdateInstalled": "애플리케이션 업데이트가 설치됨", + "NotificationOptionAudioPlayback": "오디오 재생을 시작함", + "NotificationOptionAudioPlaybackStopped": "오디오 재생이 중지됨", + "NotificationOptionCameraImageUploaded": "카메라 이미지가 업로드됨", + "NotificationOptionInstallationFailed": "설치 실패", + "NotificationOptionNewLibraryContent": "새 콘텐트가 추가됨", + "NotificationOptionPluginError": "플러그인 실패", + "NotificationOptionPluginInstalled": "플러그인이 설치됨", + "NotificationOptionPluginUninstalled": "플러그인이 설치 제거됨", + "NotificationOptionPluginUpdateInstalled": "플러그인 업데이트가 설치됨", + "NotificationOptionServerRestartRequired": "서버를 다시 시작하십시오", + "NotificationOptionTaskFailed": "예약 작업 실패", + "NotificationOptionUserLockedOut": "사용자가 잠겼습니다", + "NotificationOptionVideoPlayback": "비디오 재생을 시작함", + "NotificationOptionVideoPlaybackStopped": "비디오 재생이 중지됨", + "Photos": "사진", + "Playlists": "재생목록", + "Plugin": "플러그인", + "PluginInstalledWithName": "{0} 설치됨", + "PluginUninstalledWithName": "{0} 설치 제거됨", + "PluginUpdatedWithName": "{0} 업데이트됨", + "ProviderValue": "제공자: {0}", + "ScheduledTaskFailedWithName": "{0} 실패", + "ScheduledTaskStartedWithName": "{0} 시작", + "ServerNameNeedsToBeRestarted": "{0} 를 재시작하십시오", + "Shows": "프로그램", + "Songs": "노래", "StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시후 다시시도 해주세요.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", - "System": "System", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", + "SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다", + "SubtitlesDownloadedForItem": "{0} 자막을 다운로드했습니다", + "Sync": "동기화", + "System": "시스템", + "TvShows": "TV 쇼", + "User": "사용자", + "UserCreatedWithName": "사용자 {0} 생성됨", + "UserDeletedWithName": "사용자 {0} 삭제됨", "UserDownloadingItemWithValues": "{0} is downloading {1}", "UserLockedOutWithName": "User {0} has been locked out", "UserOfflineFromDevice": "{0} has disconnected from {1}", From 4adaeee0547bf67ebaebb200dcdd7b34af964051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Garc=C3=ADa?= Date: Sat, 1 Jun 2019 11:29:04 +0000 Subject: [PATCH 50/91] Translated using Weblate (Spanish) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 1850b8f255..f03184d5ba 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -21,7 +21,7 @@ "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos", - "HeaderFavoriteShows": "Programas favoritos", + "HeaderFavoriteShows": "Series favoritas", "HeaderFavoriteSongs": "Canciones favoritas", "HeaderLiveTV": "TV en directo", "HeaderNextUp": "Siguiendo", From 6b6776042c0c456c128734056e28765f1717ffc7 Mon Sep 17 00:00:00 2001 From: Juvenal Yescas Date: Tue, 28 May 2019 14:56:25 +0000 Subject: [PATCH 51/91] Translated using Weblate (Spanish (Mexico)) Currently translated at 100.0% (94 of 94 strings) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/ --- .../Localization/Core/es-MX.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 2285f2808a..0036329689 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -18,11 +18,11 @@ "HeaderAlbumArtists": "Artistas del Álbum", "HeaderCameraUploads": "Subidos desde Camara", "HeaderContinueWatching": "Continuar Viendo", - "HeaderFavoriteAlbums": "Álbumes Favoritos", - "HeaderFavoriteArtists": "Artistas Favoritos", - "HeaderFavoriteEpisodes": "Episodios Preferidos", - "HeaderFavoriteShows": "Programas Preferidos", - "HeaderFavoriteSongs": "Canciones Favoritas", + "HeaderFavoriteAlbums": "Álbumes favoritos", + "HeaderFavoriteArtists": "Artistas favoritos", + "HeaderFavoriteEpisodes": "Episodios favoritos", + "HeaderFavoriteShows": "Programas favoritos", + "HeaderFavoriteSongs": "Canciones favoritas", "HeaderLiveTV": "TV en Vivo", "HeaderNextUp": "A Continuación", "HeaderRecordingGroups": "Grupos de Grabaciones", From b768ad978efdb653022dc490055688d321f2bf34 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 2 Jun 2019 21:49:12 -0700 Subject: [PATCH 52/91] split the new command to more than one line --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index f0bea0455a..7c2ea50e2a 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -647,7 +647,6 @@ namespace Emby.Server.Implementations.IO public virtual bool IsPathFile(string path) { // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ - if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { @@ -655,8 +654,6 @@ namespace Emby.Server.Implementations.IO } return true; - - //return Path.IsPathRooted(path); } public virtual void DeleteFile(string path) @@ -669,7 +666,8 @@ namespace Emby.Server.Implementations.IO { // check for ready state to avoid waiting for drives to timeout // some drives on linux have no actual size or are used for other purposes - return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram).Select(d => new FileSystemMetadata + return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram) + .Select(d => new FileSystemMetadata { Name = d.Name, FullName = d.RootDirectory.FullName, From c05b7c382af5c760e1e40e0bf0c7d7f200e5b7e0 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 5 Jun 2019 20:59:06 +0200 Subject: [PATCH 53/91] Change UsCulture to InvariantCulture --- .../Playback/Hls/BaseHlsService.cs | 19 ++++----- .../Playback/Hls/DynamicHlsService.cs | 40 +++++++++---------- .../Playback/Hls/VideoHlsService.cs | 7 ++-- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 1acc42ea52..3c6d33e7e2 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text; using System.Threading; @@ -143,10 +144,10 @@ namespace MediaBrowser.Api.Playback.Hls text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT"); - var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(UsCulture); + var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); - text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase); - //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase); + text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); return text; } @@ -163,7 +164,7 @@ namespace MediaBrowser.Api.Playback.Hls var paddedBitrate = Convert.ToInt32(bitrate * 1.15); // Main stream - builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture)); + builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture)); var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8"); builder.AppendLine(playlistUrl); @@ -231,7 +232,7 @@ namespace MediaBrowser.Api.Playback.Hls { var itsOffsetMs = 0; - var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture)); + var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture)); var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions); @@ -240,7 +241,7 @@ namespace MediaBrowser.Api.Playback.Hls var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions); // If isEncoding is true we're actually starting ffmpeg - var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; + var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0"; var baseUrlParam = string.Empty; @@ -272,7 +273,7 @@ namespace MediaBrowser.Api.Playback.Hls EncodingHelper.GetMapArgs(state), GetVideoArguments(state, encodingOptions), GetAudioArguments(state, encodingOptions), - state.SegmentLength.ToString(UsCulture), + state.SegmentLength.ToString(CultureInfo.InvariantCulture), startNumberParam, outputPath, outputTsArg, @@ -293,9 +294,9 @@ namespace MediaBrowser.Api.Playback.Hls EncodingHelper.GetMapArgs(state), GetVideoArguments(state, encodingOptions), GetAudioArguments(state, encodingOptions), - state.SegmentLength.ToString(UsCulture), + state.SegmentLength.ToString(CultureInfo.InvariantCulture), startNumberParam, - state.HlsListSize.ToString(UsCulture), + state.HlsListSize.ToString(CultureInfo.InvariantCulture), baseUrlParam, outputPath ).Trim(); diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 45f003cae8..fdae59f569 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -177,7 +177,7 @@ namespace MediaBrowser.Api.Playback.Hls var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, UsCulture); + var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture); var state = await GetState(request, cancellationToken).ConfigureAwait(false); @@ -364,7 +364,7 @@ namespace MediaBrowser.Api.Playback.Hls var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); - return int.Parse(indexString, NumberStyles.Integer, UsCulture); + return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); } private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount) @@ -438,7 +438,7 @@ namespace MediaBrowser.Api.Playback.Hls segmentId = segmentRequest.SegmentId; } - return int.Parse(segmentId, NumberStyles.Integer, UsCulture); + return int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture); } private string GetSegmentPath(StreamState state, string playlist, int index) @@ -447,7 +447,7 @@ namespace MediaBrowser.Api.Playback.Hls var filename = Path.GetFileNameWithoutExtension(playlist); - return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state.Request)); + return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request)); } private async Task GetSegmentResult(StreamState state, @@ -628,8 +628,8 @@ namespace MediaBrowser.Api.Playback.Hls private string ReplaceBitrate(string url, int oldValue, int newValue) { return url.Replace( - "videobitrate=" + oldValue.ToString(UsCulture), - "videobitrate=" + newValue.ToString(UsCulture), + "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), + "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); } @@ -648,8 +648,8 @@ namespace MediaBrowser.Api.Playback.Hls var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", state.Request.MediaSourceId, - stream.Index.ToString(UsCulture), - 30.ToString(UsCulture), + stream.Index.ToString(CultureInfo.InvariantCulture), + 30.ToString(CultureInfo.InvariantCulture), AuthorizationContext.GetAuthorizationInfo(Request).Token); var line = string.Format(format, @@ -705,7 +705,7 @@ namespace MediaBrowser.Api.Playback.Hls private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup) { - var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(UsCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(UsCulture); + var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture); // tvos wants resolution, codecs, framerate //if (state.TargetFramerate.HasValue) @@ -770,7 +770,7 @@ namespace MediaBrowser.Api.Playback.Hls builder.AppendLine("#EXTM3U"); builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); builder.AppendLine("#EXT-X-VERSION:3"); - builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(UsCulture)); + builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture)); builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); var queryStringIndex = Request.RawUrl.IndexOf('?'); @@ -785,12 +785,12 @@ namespace MediaBrowser.Api.Playback.Hls foreach (var length in segmentLengths) { - builder.AppendLine("#EXTINF:" + length.ToString("0.0000", UsCulture) + ", nodesc"); + builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc"); builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}", name, - index.ToString(UsCulture), + index.ToString(CultureInfo.InvariantCulture), GetSegmentFileExtension(request), queryString)); @@ -821,17 +821,17 @@ namespace MediaBrowser.Api.Playback.Hls if (state.OutputAudioBitrate.HasValue) { - audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(UsCulture)); + audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); } if (state.OutputAudioChannels.HasValue) { - audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture)); + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } if (state.OutputAudioSampleRate.HasValue) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } audioTranscodeParams.Add("-vn"); @@ -863,12 +863,12 @@ namespace MediaBrowser.Api.Playback.Hls if (bitrate.HasValue) { - args += " -ab " + bitrate.Value.ToString(UsCulture); + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - args += " -ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture); + args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true); @@ -905,7 +905,7 @@ namespace MediaBrowser.Api.Playback.Hls else { var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", - state.SegmentLength.ToString(UsCulture)); + state.SegmentLength.ToString(CultureInfo.InvariantCulture)); var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; @@ -953,7 +953,7 @@ namespace MediaBrowser.Api.Playback.Hls // If isEncoding is true we're actually starting ffmpeg var startNumber = GetStartNumber(state); - var startNumberParam = isEncoding ? startNumber.ToString(UsCulture) : "0"; + var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0"; var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty; @@ -984,7 +984,7 @@ namespace MediaBrowser.Api.Playback.Hls mapArgs, GetVideoArguments(state, encodingOptions), GetAudioArguments(state, encodingOptions), - state.SegmentLength.ToString(UsCulture), + state.SegmentLength.ToString(CultureInfo.InvariantCulture), startNumberParam, outputPath, outputTsArg, diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index eb1bbfb74b..3c715c5add 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -55,12 +56,12 @@ namespace MediaBrowser.Api.Playback.Hls if (bitrate.HasValue) { - args += " -ab " + bitrate.Value.ToString(UsCulture); + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - args += " -ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture); + args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true); @@ -100,7 +101,7 @@ namespace MediaBrowser.Api.Playback.Hls else { var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", - state.SegmentLength.ToString(UsCulture)); + state.SegmentLength.ToString(CultureInfo.InvariantCulture)); var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; From defc5f1cf9b486357b379c610663e1bad48428ad Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 28 Mar 2019 22:43:32 +0100 Subject: [PATCH 54/91] Force LF line endings --- .gitattributes | 2 + .gitignore | 7 +- Doxyfile | 5130 ++++++++--------- .../Cryptography/PasswordHash.cs | 306 +- build | 0 bump_version | 0 deployment/centos-package-x64/clean.sh | 0 deployment/centos-package-x64/docker-build.sh | 0 deployment/centos-package-x64/package.sh | 0 deployment/centos-package-x64/pkg-src | 0 deployment/common.build.sh | 0 deployment/debian-package-armhf/clean.sh | 0 .../debian-package-armhf/docker-build.sh | 0 deployment/debian-package-armhf/package.sh | 0 deployment/debian-package-armhf/pkg-src | 0 deployment/debian-package-x64/clean.sh | 0 deployment/debian-package-x64/docker-build.sh | 0 deployment/debian-package-x64/package.sh | 0 deployment/docker/build.sh | 0 deployment/docker/package.sh | 0 deployment/fedora-package-x64/clean.sh | 0 .../fedora-package-x64/create_tarball.sh | 0 deployment/fedora-package-x64/docker-build.sh | 0 deployment/fedora-package-x64/package.sh | 0 deployment/linux-x64/build.sh | 0 deployment/linux-x64/clean.sh | 0 deployment/linux-x64/package.sh | 0 deployment/macos/build.sh | 0 deployment/macos/clean.sh | 0 deployment/macos/package.sh | 0 deployment/portable/build.sh | 0 deployment/portable/clean.sh | 0 deployment/portable/package.sh | 0 deployment/ubuntu-package-x64/clean.sh | 0 deployment/ubuntu-package-x64/docker-build.sh | 0 deployment/ubuntu-package-x64/package.sh | 0 deployment/ubuntu-package-x64/pkg-src | 0 deployment/win-x64/build.sh | 0 deployment/win-x64/clean.sh | 0 deployment/win-x64/package.sh | 0 deployment/win-x86/build.sh | 0 deployment/win-x86/clean.sh | 0 deployment/win-x86/package.sh | 0 43 files changed, 2721 insertions(+), 2724 deletions(-) mode change 100755 => 100644 build mode change 100755 => 100644 bump_version mode change 100755 => 100644 deployment/centos-package-x64/clean.sh mode change 100755 => 100644 deployment/centos-package-x64/docker-build.sh mode change 100755 => 100644 deployment/centos-package-x64/package.sh mode change 120000 => 100644 deployment/centos-package-x64/pkg-src mode change 100755 => 100644 deployment/common.build.sh mode change 100755 => 100644 deployment/debian-package-armhf/clean.sh mode change 100755 => 100644 deployment/debian-package-armhf/docker-build.sh mode change 100755 => 100644 deployment/debian-package-armhf/package.sh mode change 120000 => 100644 deployment/debian-package-armhf/pkg-src mode change 100755 => 100644 deployment/debian-package-x64/clean.sh mode change 100755 => 100644 deployment/debian-package-x64/docker-build.sh mode change 100755 => 100644 deployment/debian-package-x64/package.sh mode change 100755 => 100644 deployment/docker/build.sh mode change 100755 => 100644 deployment/docker/package.sh mode change 100755 => 100644 deployment/fedora-package-x64/clean.sh mode change 100755 => 100644 deployment/fedora-package-x64/create_tarball.sh mode change 100755 => 100644 deployment/fedora-package-x64/docker-build.sh mode change 100755 => 100644 deployment/fedora-package-x64/package.sh mode change 100755 => 100644 deployment/linux-x64/build.sh mode change 100755 => 100644 deployment/linux-x64/clean.sh mode change 100755 => 100644 deployment/linux-x64/package.sh mode change 100755 => 100644 deployment/macos/build.sh mode change 100755 => 100644 deployment/macos/clean.sh mode change 100755 => 100644 deployment/macos/package.sh mode change 100755 => 100644 deployment/portable/build.sh mode change 100755 => 100644 deployment/portable/clean.sh mode change 100755 => 100644 deployment/portable/package.sh mode change 100755 => 100644 deployment/ubuntu-package-x64/clean.sh mode change 100755 => 100644 deployment/ubuntu-package-x64/docker-build.sh mode change 100755 => 100644 deployment/ubuntu-package-x64/package.sh mode change 120000 => 100644 deployment/ubuntu-package-x64/pkg-src mode change 100755 => 100644 deployment/win-x64/build.sh mode change 100755 => 100644 deployment/win-x64/clean.sh mode change 100755 => 100644 deployment/win-x64/package.sh mode change 100755 => 100644 deployment/win-x86/build.sh mode change 100755 => 100644 deployment/win-x86/clean.sh mode change 100755 => 100644 deployment/win-x86/package.sh diff --git a/.gitattributes b/.gitattributes index 0c0ab960a0..d78b0459d8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ +* text=auto eol=lf + CONTRIBUTORS.md merge=union diff --git a/.gitignore b/.gitignore index a1b7ac59c8..2ce41d76e7 100644 --- a/.gitignore +++ b/.gitignore @@ -239,11 +239,6 @@ pip-log.txt ########## .idea/ -########## -# Visual Studio Code -########## -.vscode/ - ######################### # Build artifacts ######################### @@ -268,4 +263,4 @@ jellyfin_version.ini ci/ # Doxygen -doc/ \ No newline at end of file +doc/ diff --git a/Doxyfile b/Doxyfile index 1628b4bde4..c859737cac 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1,2565 +1,2565 @@ -# Doxyfile 1.8.15 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project. -# -# All text after a double hash (##) is considered a comment and is placed in -# front of the TAG it is preceding. -# -# All text after a single hash (#) is considered a comment and will be ignored. -# The format is: -# TAG = value [value, ...] -# For lists, items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (\" \"). - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# This tag specifies the encoding used for all characters in the configuration -# file that follow. The default is UTF-8 which is also the encoding used for all -# text before the first occurrence of this tag. Doxygen uses libiconv (or the -# iconv built into libc) for the transcoding. See -# https://www.gnu.org/software/libiconv/ for the list of possible encodings. -# The default value is: UTF-8. - -DOXYFILE_ENCODING = UTF-8 - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by -# double-quotes, unless you are using Doxywizard) that should identify the -# project for which the documentation is generated. This name is used in the -# title of most generated pages and in a few other places. -# The default value is: My Project. - -PROJECT_NAME = Jellyfin - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. This -# could be handy for archiving the generated documentation or if some version -# control system is used. - -PROJECT_NUMBER = - -# Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer a -# quick idea about the purpose of the project. Keep the description short. - -PROJECT_BRIEF = "The Free Software Media System" - -# With the PROJECT_LOGO tag one can specify a logo or an icon that is included -# in the documentation. The maximum height of the logo should not exceed 55 -# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy -# the logo to the output directory. - -PROJECT_LOGO = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path -# into which the generated documentation will be written. If a relative path is -# entered, it will be relative to the location where doxygen was started. If -# left blank the current directory will be used. - -OUTPUT_DIRECTORY = doc - -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this -# option can be useful when feeding doxygen a huge amount of source files, where -# putting all generated files in the same directory would otherwise causes -# performance problems for the file system. -# The default value is: NO. - -CREATE_SUBDIRS = NO - -# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII -# characters to appear in the names of generated files. If set to NO, non-ASCII -# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode -# U+3044. -# The default value is: NO. - -ALLOW_UNICODE_NAMES = NO - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. -# The default value is: English. - -OUTPUT_LANGUAGE = English - -# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all generated output in the proper direction. -# Possible values are: None, LTR, RTL and Context. -# The default value is: None. - -OUTPUT_TEXT_DIRECTION = None - -# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member -# descriptions after the members that are listed in the file and class -# documentation (similar to Javadoc). Set to NO to disable this. -# The default value is: YES. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief -# description of a member or function before the detailed description -# -# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. -# The default value is: YES. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator that is -# used to form the text in various listings. Each string in this list, if found -# as the leading text of the brief description, will be stripped from the text -# and the result, after processing the whole list, is used as the annotated -# text. Otherwise, the brief description is used as-is. If left blank, the -# following values are used ($name is automatically replaced with the name of -# the entity):The $name class, The $name widget, The $name file, is, provides, -# specifies, contains, represents, a, an and the. - -ABBREVIATE_BRIEF = "The $name class" \ - "The $name widget" \ - "The $name file" \ - is \ - provides \ - specifies \ - contains \ - represents \ - a \ - an \ - the - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# doxygen will generate a detailed section even if there is only a brief -# description. -# The default value is: NO. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. -# The default value is: NO. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path -# before files name in the file list and in the header files. If set to NO the -# shortest path that makes the file name unique will be used -# The default value is: YES. - -FULL_PATH_NAMES = YES - -# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. -# Stripping is only done if one of the specified strings matches the left-hand -# part of the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the path to -# strip. -# -# Note that you can specify absolute paths here, but also relative paths, which -# will be relative from the directory where doxygen is started. -# This tag requires that the tag FULL_PATH_NAMES is set to YES. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the -# path mentioned in the documentation of a class, which tells the reader which -# header file to include in order to use a class. If left blank only the name of -# the header file containing the class definition is used. Otherwise one should -# specify the list of include paths that are normally passed to the compiler -# using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -# less readable) file names. This can be useful is your file systems doesn't -# support long names like on DOS, Mac, or CD-ROM. -# The default value is: NO. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -# first line (until the first dot) of a Javadoc-style comment as the brief -# description. If set to NO, the Javadoc-style will behave just like regular Qt- -# style comments (thus requiring an explicit @brief command for a brief -# description.) -# The default value is: NO. - -JAVADOC_AUTOBRIEF = NO - -# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -# line (until the first dot) of a Qt-style comment as the brief description. If -# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -# requiring an explicit \brief command for a brief description.) -# The default value is: NO. - -QT_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a -# multi-line C++ special comment block (i.e. a block of //! or /// comments) as -# a brief description. This used to be the default behavior. The new default is -# to treat a multi-line C++ comment block as a detailed description. Set this -# tag to YES if you prefer the old behavior instead. -# -# Note that setting this tag to YES also means that rational rose comments are -# not recognized any more. -# The default value is: NO. - -MULTILINE_CPP_IS_BRIEF = NO - -# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the -# documentation from any documented member that it re-implements. -# The default value is: YES. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new -# page for each member. If set to NO, the documentation of a member will be part -# of the file/class/namespace that contains it. -# The default value is: NO. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen -# uses this value to replace tabs by spaces in code fragments. -# Minimum value: 1, maximum value: 16, default value: 4. - -TAB_SIZE = 4 - -# This tag can be used to specify a number of aliases that act as commands in -# the documentation. An alias has the form: -# name=value -# For example adding -# "sideeffect=@par Side Effects:\n" -# will allow you to put the command \sideeffect (or @sideeffect) in the -# documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines (in the resulting output). You can put ^^ in the value part of an -# alias to insert a newline as if a physical newline was in the original file. -# When you need a literal { or } or , in the value part of an alias you have to -# escape them by means of a backslash (\), this can lead to conflicts with the -# commands \{ and \} for these it is advised to use the version @{ and @} or use -# a double escape (\\{ and \\}) - -ALIASES = - -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources -# only. Doxygen will then generate output that is more tailored for C. For -# instance, some of the names that are used will be different. The list of all -# members will be omitted, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or -# Python sources only. Doxygen will then generate output that is more tailored -# for that language. For instance, namespaces will be presented as packages, -# qualified scopes will look different, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_JAVA = YES - -# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources. Doxygen will then generate output that is tailored for Fortran. -# The default value is: NO. - -OPTIMIZE_FOR_FORTRAN = NO - -# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for VHDL. -# The default value is: NO. - -OPTIMIZE_OUTPUT_VHDL = NO - -# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice -# sources only. Doxygen will then generate output that is more tailored for that -# language. For instance, namespaces will be presented as modules, types will be -# separated into more groups, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_SLICE = NO - -# Doxygen selects the parser to use depending on the extension of the files it -# parses. With this tag you can assign which parser to use for a given -# extension. Doxygen has a built-in mapping, but you can override or extend it -# using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, Javascript, -# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, -# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: -# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser -# tries to guess whether the code is fixed or free formatted code, this is the -# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat -# .inc files as Fortran files (default is PHP), and .f files as C (default is -# Fortran), use: inc=Fortran f=C. -# -# Note: For files without extension you can use no_extension as a placeholder. -# -# Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. - -EXTENSION_MAPPING = - -# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments -# according to the Markdown format, which allows for more readable -# documentation. See https://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you can -# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in -# case of backward compatibilities issues. -# The default value is: YES. - -MARKDOWN_SUPPORT = YES - -# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up -# to that level are automatically included in the table of contents, even if -# they do not have an id attribute. -# Note: This feature currently applies only to Markdown headings. -# Minimum value: 0, maximum value: 99, default value: 0. -# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. - -TOC_INCLUDE_HEADINGS = 0 - -# When enabled doxygen tries to link words that correspond to documented -# classes, or namespaces to their corresponding documentation. Such a link can -# be prevented in individual cases by putting a % sign in front of the word or -# globally by setting AUTOLINK_SUPPORT to NO. -# The default value is: YES. - -AUTOLINK_SUPPORT = YES - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should set this -# tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); -# versus func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. -# The default value is: NO. - -BUILTIN_STL_SUPPORT = NO - -# If you use Microsoft's C++/CLI language, you should set this option to YES to -# enable parsing support. -# The default value is: NO. - -CPP_CLI_SUPPORT = NO - -# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen -# will parse them like normal C++ but will assume all classes use public instead -# of private inheritance when no explicit protection keyword is present. -# The default value is: NO. - -SIP_SUPPORT = NO - -# For Microsoft's IDL there are propget and propput attributes to indicate -# getter and setter methods for a property. Setting this option to YES will make -# doxygen to replace the get and set methods by a property in the documentation. -# This will only work if the methods are indeed getting or setting a simple -# type. If this is not the case, or you want to show the methods anyway, you -# should set this option to NO. -# The default value is: YES. - -IDL_PROPERTY_SUPPORT = YES - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. -# The default value is: NO. - -DISTRIBUTE_GROUP_DOC = NO - -# If one adds a struct or class to a group and this option is enabled, then also -# any nested class or struct is added to the same group. By default this option -# is disabled and one has to add nested compounds explicitly via \ingroup. -# The default value is: NO. - -GROUP_NESTED_COMPOUNDS = NO - -# Set the SUBGROUPING tag to YES to allow class member groups of the same type -# (for instance a group of public functions) to be put as a subgroup of that -# type (e.g. under the Public Functions section). Set it to NO to prevent -# subgrouping. Alternatively, this can be done per class using the -# \nosubgrouping command. -# The default value is: YES. - -SUBGROUPING = YES - -# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions -# are shown inside the group in which they are included (e.g. using \ingroup) -# instead of on a separate page (for HTML and Man pages) or section (for LaTeX -# and RTF). -# -# Note that this feature does not work in combination with -# SEPARATE_MEMBER_PAGES. -# The default value is: NO. - -INLINE_GROUPED_CLASSES = NO - -# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions -# with only public data fields or simple typedef fields will be shown inline in -# the documentation of the scope in which they are defined (i.e. file, -# namespace, or group documentation), provided this scope is documented. If set -# to NO, structs, classes, and unions are shown on a separate page (for HTML and -# Man pages) or section (for LaTeX and RTF). -# The default value is: NO. - -INLINE_SIMPLE_STRUCTS = NO - -# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or -# enum is documented as struct, union, or enum with the name of the typedef. So -# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -# with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically be -# useful for C code in case the coding convention dictates that all compound -# types are typedef'ed and only the typedef is referenced, never the tag name. -# The default value is: NO. - -TYPEDEF_HIDES_STRUCT = NO - -# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This -# cache is used to resolve symbols given their name and scope. Since this can be -# an expensive process and often the same symbol appears multiple times in the -# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -# doxygen will become slower. If the cache is too large, memory is wasted. The -# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range -# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -# symbols. At the end of a run doxygen will report the cache usage and suggest -# the optimal cache size from a speed point of view. -# Minimum value: 0, maximum value: 9, default value: 0. - -LOOKUP_CACHE_SIZE = 0 - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in -# documentation are documented, even if no documentation was available. Private -# class members and static file members will be hidden unless the -# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. -# Note: This will also disable the warnings about undocumented members that are -# normally produced when WARNINGS is set to YES. -# The default value is: NO. - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will -# be included in the documentation. -# The default value is: NO. - -EXTRACT_PRIVATE = NO - -# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal -# scope will be included in the documentation. -# The default value is: NO. - -EXTRACT_PACKAGE = NO - -# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be -# included in the documentation. -# The default value is: NO. - -EXTRACT_STATIC = NO - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined -# locally in source files will be included in the documentation. If set to NO, -# only classes defined in header files are included. Does not have any effect -# for Java sources. -# The default value is: YES. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. If set to YES, local methods, -# which are defined in the implementation section but not in the interface are -# included in the documentation. If set to NO, only methods in the interface are -# included. -# The default value is: NO. - -EXTRACT_LOCAL_METHODS = NO - -# If this flag is set to YES, the members of anonymous namespaces will be -# extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base name of -# the file that contains the anonymous namespace. By default anonymous namespace -# are hidden. -# The default value is: NO. - -EXTRACT_ANON_NSPACES = NO - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all -# undocumented members inside documented classes or files. If set to NO these -# members will be included in the various overviews, but no documentation -# section is generated. This option has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. If set -# to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -# (class|struct|union) declarations. If set to NO, these declarations will be -# included in the documentation. -# The default value is: NO. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any -# documentation blocks found inside the body of a function. If set to NO, these -# blocks will be appended to the function's detailed documentation block. -# The default value is: NO. - -HIDE_IN_BODY_DOCS = NO - -# The INTERNAL_DOCS tag determines if documentation that is typed after a -# \internal command is included. If the tag is set to NO then the documentation -# will be excluded. Set it to YES to include the internal documentation. -# The default value is: NO. - -INTERNAL_DOCS = NO - -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. -# The default value is: system dependent. - -CASE_SENSE_NAMES = NO - -# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with -# their full class and namespace scopes in the documentation. If set to YES, the -# scope will be hidden. -# The default value is: NO. - -HIDE_SCOPE_NAMES = NO - -# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will -# append additional text to a page's title, such as Class Reference. If set to -# YES the compound reference will be hidden. -# The default value is: NO. - -HIDE_COMPOUND_REFERENCE= NO - -# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of -# the files that are included by a file in the documentation of that file. -# The default value is: YES. - -SHOW_INCLUDE_FILES = YES - -# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each -# grouped member an include statement to the documentation, telling the reader -# which file to include in order to use the member. -# The default value is: NO. - -SHOW_GROUPED_MEMB_INC = NO - -# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include -# files with double quotes in the documentation rather than with sharp brackets. -# The default value is: NO. - -FORCE_LOCAL_INCLUDES = NO - -# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the -# documentation for inline members. -# The default value is: YES. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the -# (detailed) documentation of file and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. -# The default value is: YES. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief -# descriptions of file, namespace and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. Note that -# this will also influence the order of the classes in the class list. -# The default value is: NO. - -SORT_BRIEF_DOCS = NO - -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the -# (brief and detailed) documentation of class members so that constructors and -# destructors are listed first. If set to NO the constructors will appear in the -# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. -# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief -# member documentation. -# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting -# detailed member documentation. -# The default value is: NO. - -SORT_MEMBERS_CTORS_1ST = NO - -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy -# of group names into alphabetical order. If set to NO the group names will -# appear in their defined order. -# The default value is: NO. - -SORT_GROUP_NAMES = NO - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by -# fully-qualified names, including namespaces. If set to NO, the class list will -# be sorted only by class name, not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the alphabetical -# list. -# The default value is: NO. - -SORT_BY_SCOPE_NAME = NO - -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper -# type resolution of all parameters of a function it will reject a match between -# the prototype and the implementation of a member function even if there is -# only one candidate or it is obvious which candidate to choose by doing a -# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still -# accept a match between prototype and implementation in such cases. -# The default value is: NO. - -STRICT_PROTO_MATCHING = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo -# list. This list is created by putting \todo commands in the documentation. -# The default value is: YES. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test -# list. This list is created by putting \test commands in the documentation. -# The default value is: YES. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug -# list. This list is created by putting \bug commands in the documentation. -# The default value is: YES. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) -# the deprecated list. This list is created by putting \deprecated commands in -# the documentation. -# The default value is: YES. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional documentation -# sections, marked by \if ... \endif and \cond -# ... \endcond blocks. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the -# initial value of a variable or macro / define can have for it to appear in the -# documentation. If the initializer consists of more lines than specified here -# it will be hidden. Use a value of 0 to hide initializers completely. The -# appearance of the value of individual variables and macros / defines can be -# controlled using \showinitializer or \hideinitializer command in the -# documentation regardless of this setting. -# Minimum value: 0, maximum value: 10000, default value: 30. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at -# the bottom of the documentation of classes and structs. If set to YES, the -# list will mention the files that were used to generate the documentation. -# The default value is: YES. - -SHOW_USED_FILES = YES - -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This -# will remove the Files entry from the Quick Index and from the Folder Tree View -# (if specified). -# The default value is: YES. - -SHOW_FILES = YES - -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces -# page. This will remove the Namespaces entry from the Quick Index and from the -# Folder Tree View (if specified). -# The default value is: YES. - -SHOW_NAMESPACES = YES - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from -# the version control system). Doxygen will invoke the program by executing (via -# popen()) the command command input-file, where command is the value of the -# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -# by doxygen. Whatever the program writes to standard output is used as the file -# version. For an example see the documentation. - -FILE_VERSION_FILTER = - -# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -# by doxygen. The layout file controls the global structure of the generated -# output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. You can -# optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. -# -# Note that if you run doxygen from a directory containing a file called -# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE -# tag is left empty. - -LAYOUT_FILE = - -# The CITE_BIB_FILES tag can be used to specify one or more bib files containing -# the reference definitions. This must be a list of .bib files. The .bib -# extension is automatically appended if omitted. This requires the bibtex tool -# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. -# For LaTeX the style of the bibliography can be controlled using -# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the -# search path. See also \cite for info how to create references. - -CITE_BIB_FILES = - -#--------------------------------------------------------------------------- -# Configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated to -# standard output by doxygen. If QUIET is set to YES this implies that the -# messages are off. -# The default value is: NO. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES -# this implies that the warnings are on. -# -# Tip: Turn warnings on while writing the documentation. -# The default value is: YES. - -WARNINGS = YES - -# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate -# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag -# will automatically be disabled. -# The default value is: YES. - -WARN_IF_UNDOCUMENTED = YES - -# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. -# The default value is: YES. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that -# are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. If -# EXTRACT_ALL is set to YES then this flag will automatically be disabled. -# The default value is: NO. - -WARN_NO_PARAMDOC = NO - -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. -# The default value is: NO. - -WARN_AS_ERROR = NO - -# The WARN_FORMAT tag determines the format of the warning messages that doxygen -# can produce. The string should contain the $file, $line, and $text tags, which -# will be replaced by the file and line number from which the warning originated -# and the warning text. Optionally the format may contain $version, which will -# be replaced by the version of the file (if it could be obtained via -# FILE_VERSION_FILTER) -# The default value is: $file:$line: $text. - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning and error -# messages should be written. If left blank the output is written to standard -# error (stderr). - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# Configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag is used to specify the files and/or directories that contain -# documented source files. You may enter file names like myfile.cpp or -# directories like /usr/src/myproject. Separate the files or directories with -# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING -# Note: If this tag is empty the current directory is searched. - -INPUT = - -# This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses -# libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: https://www.gnu.org/software/libiconv/) for the list of -# possible encodings. -# The default value is: UTF-8. - -INPUT_ENCODING = UTF-8 - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and -# *.h) to filter out the source-files in the directories. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# read by doxygen. -# -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, -# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. - -FILE_PATTERNS = *.c \ - *.cc \ - *.cxx \ - *.cpp \ - *.c++ \ - *.java \ - *.ii \ - *.ixx \ - *.ipp \ - *.i++ \ - *.inl \ - *.idl \ - *.ddl \ - *.odl \ - *.h \ - *.hh \ - *.hxx \ - *.hpp \ - *.h++ \ - *.cs \ - *.d \ - *.php \ - *.php4 \ - *.php5 \ - *.phtml \ - *.inc \ - *.m \ - *.markdown \ - *.md \ - *.mm \ - *.dox \ - *.py \ - *.pyw \ - *.f90 \ - *.f95 \ - *.f03 \ - *.f08 \ - *.f \ - *.for \ - *.tcl \ - *.vhd \ - *.vhdl \ - *.ucf \ - *.qsf \ - *.ice - -# The RECURSIVE tag can be used to specify whether or not subdirectories should -# be searched for input files as well. -# The default value is: NO. - -RECURSIVE = YES - -# The EXCLUDE tag can be used to specify files and/or directories that should be -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. -# -# Note that relative paths are relative to the directory from which doxygen is -# run. - -EXCLUDE = - -# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or -# directories that are symbolic links (a Unix file system feature) are excluded -# from the input. -# The default value is: NO. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -# (namespaces, classes, functions, etc.) that should be excluded from the -# output. The symbol name can be a fully qualified name, a word, or if the -# wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* - -EXCLUDE_SYMBOLS = - -# The EXAMPLE_PATH tag can be used to specify one or more files or directories -# that contain example code fragments that are included (see the \include -# command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and -# *.h) to filter out the source-files in the directories. If left blank all -# files are included. - -EXAMPLE_PATTERNS = * - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude commands -# irrespective of the value of the RECURSIVE tag. -# The default value is: NO. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or directories -# that contain images that are to be included in the documentation (see the -# \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command: -# -# -# -# where is the value of the INPUT_FILTER tag, and is the -# name of an input file. Doxygen will then use the output that the filter -# program writes to standard output. If FILTER_PATTERNS is specified, this tag -# will be ignored. -# -# Note that the filter must not add or remove lines; it is applied before the -# code is scanned, but not when the output code is generated. If lines are added -# or removed, the anchors will not be placed correctly. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: pattern=filter -# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how -# filters are used. If the FILTER_PATTERNS tag is empty or if none of the -# patterns match the file name, INPUT_FILTER is applied. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will also be used to filter the input files that are used for -# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). -# The default value is: NO. - -FILTER_SOURCE_FILES = NO - -# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file -# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and -# it is also possible to disable source filtering for a specific pattern using -# *.ext= (so without naming a filter). -# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. - -FILTER_SOURCE_PATTERNS = - -# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that -# is part of the input, its contents will be placed on the main page -# (index.html). This can be useful if you have a project on for instance GitHub -# and want to reuse the introduction page also for the doxygen output. - -USE_MDFILE_AS_MAINPAGE = ./README.md - -#--------------------------------------------------------------------------- -# Configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will be -# generated. Documented entities will be cross-referenced with these sources. -# -# Note: To get rid of all source code in the generated output, make sure that -# also VERBATIM_HEADERS is set to NO. -# The default value is: NO. - -SOURCE_BROWSER = YES - -# Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. -# The default value is: NO. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any -# special comment blocks from generated source code fragments. Normal C, C++ and -# Fortran comments will always remain visible. -# The default value is: YES. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES then for each documented -# entity all documented functions referencing it will be listed. -# The default value is: NO. - -REFERENCED_BY_RELATION = NO - -# If the REFERENCES_RELATION tag is set to YES then for each documented function -# all documented entities called/used by that function will be listed. -# The default value is: NO. - -REFERENCES_RELATION = NO - -# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set -# to YES then the hyperlinks from functions in REFERENCES_RELATION and -# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will -# link to the documentation. -# The default value is: YES. - -REFERENCES_LINK_SOURCE = YES - -# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the -# source code will show a tooltip with additional information such as prototype, -# brief description and links to the definition and documentation. Since this -# will make the HTML file larger and loading of large files a bit slower, you -# can opt to disable this feature. -# The default value is: YES. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -SOURCE_TOOLTIPS = YES - -# If the USE_HTAGS tag is set to YES then the references to source code will -# point to the HTML generated by the htags(1) tool instead of doxygen built-in -# source browser. The htags tool is part of GNU's global source tagging system -# (see https://www.gnu.org/software/global/global.html). You will need version -# 4.8.6 or higher. -# -# To use it do the following: -# - Install the latest version of global -# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file -# - Make sure the INPUT points to the root of the source tree -# - Run doxygen as normal -# -# Doxygen will invoke htags (and that will in turn invoke gtags), so these -# tools must be available from the command line (i.e. in the search path). -# -# The result: instead of the source browser generated by doxygen, the links to -# source code will now point to the output of htags. -# The default value is: NO. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a -# verbatim copy of the header file for each class for which an include is -# specified. Set to NO to disable this. -# See also: Section \class. -# The default value is: YES. - -VERBATIM_HEADERS = YES - -# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the -# cost of reduced performance. This can be particularly helpful with template -# rich C++ code for which doxygen's built-in parser lacks the necessary type -# information. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. -# The default value is: NO. - -CLANG_ASSISTED_PARSING = NO - -# If clang assisted parsing is enabled you can provide the compiler with command -# line options that you would normally use when invoking the compiler. Note that -# the include paths will already be set by doxygen for the files and directories -# specified with INPUT and INCLUDE_PATH. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_OPTIONS = - -# If clang assisted parsing is enabled you can provide the clang parser with the -# path to the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files -# were built. This is equivalent to specifying the "-p" option to a clang tool, -# such as clang-check. These options will then be passed to the parser. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. - -CLANG_DATABASE_PATH = - -#--------------------------------------------------------------------------- -# Configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all -# compounds will be generated. Enable this if the project contains a lot of -# classes, structs, unions or interfaces. -# The default value is: YES. - -ALPHABETICAL_INDEX = YES - -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output -# The default value is: YES. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each -# generated HTML page (for example: .htm, .php, .asp). -# The default value is: .html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a user-defined HTML header file for -# each generated HTML page. If the tag is left blank doxygen will generate a -# standard header. -# -# To get valid HTML the header file that includes any scripts and style sheets -# that doxygen needs, which is dependent on the configuration options used (e.g. -# the setting GENERATE_TREEVIEW). It is highly recommended to start with a -# default header using -# doxygen -w html new_header.html new_footer.html new_stylesheet.css -# YourConfigFile -# and then modify the file new_header.html. See also section "Doxygen usage" -# for information on how to generate the default header that doxygen normally -# uses. -# Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. For a description -# of the possible markers and block names see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_HEADER = - -# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -# generated HTML page. If the tag is left blank doxygen will generate a standard -# footer. See HTML_HEADER for more information on how to generate a default -# footer and what special commands can be used inside the footer. See also -# section "Doxygen usage" for information on how to generate the default footer -# that doxygen normally uses. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FOOTER = - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style -# sheet that is used by each HTML page. It can be used to fine-tune the look of -# the HTML output. If left blank doxygen will generate a default style sheet. -# See also section "Doxygen usage" for information on how to generate the style -# sheet that doxygen normally uses. -# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as -# it is more robust and this tag (HTML_STYLESHEET) will in the future become -# obsolete. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_STYLESHEET = - -# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined -# cascading style sheets that are included after the standard style sheets -# created by doxygen. Using this option one can overrule certain style aspects. -# This is preferred over using HTML_STYLESHEET since it does not replace the -# standard style sheet and is therefore more robust against future updates. -# Doxygen will copy the style sheet files to the output directory. -# Note: The order of the extra style sheet files is of importance (e.g. the last -# style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_STYLESHEET = - -# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or -# other source files which should be copied to the HTML output directory. Note -# that these files will be copied to the base HTML output directory. Use the -# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these -# files. In the HTML_STYLESHEET file, use the file name only. Also note that the -# files will be copied as-is; there are no commands or markers available. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_FILES = - -# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen -# will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see -# https://en.wikipedia.org/wiki/Hue for more information. For instance the value -# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 -# purple, and 360 is red again. -# Minimum value: 0, maximum value: 359, default value: 220. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_HUE = 195 - -# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A -# value of 255 will produce the most vivid colors. -# Minimum value: 0, maximum value: 255, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_SAT = 100 - -# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the -# luminance component of the colors in the HTML output. Values below 100 -# gradually make the output lighter, whereas values above 100 make the output -# darker. The value divided by 100 is the actual gamma applied, so 80 represents -# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not -# change the gamma. -# Minimum value: 40, maximum value: 240, default value: 80. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_GAMMA = 86 - -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - -# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML -# documentation will contain a main index with vertical navigation menus that -# are dynamically created via Javascript. If disabled, the navigation index will -# consists of multiple levels of tabs that are statically embedded in every HTML -# page. Disable this option to support browsers that do not have Javascript, -# like the Qt help browser. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_MENUS = YES - -# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -# documentation will contain sections that can be hidden and shown after the -# page has loaded. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_SECTIONS = NO - -# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries -# shown in the various tree structured indices initially; the user can expand -# and collapse entries dynamically later on. Doxygen will expand the tree to -# such a level that at most the specified number of entries are visible (unless -# a fully collapsed tree already exceeds this amount). So setting the number of -# entries 1 will produce a full collapsed tree by default. 0 is a special value -# representing an infinite number of entries and will result in a full expanded -# tree by default. -# Minimum value: 0, maximum value: 9999, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_INDEX_NUM_ENTRIES = 100 - -# If the GENERATE_DOCSET tag is set to YES, additional index files will be -# generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: https://developer.apple.com/xcode/), introduced with OSX -# 10.5 (Leopard). To create a documentation set, doxygen will generate a -# Makefile in the HTML output directory. Running make will produce the docset in -# that directory and running make install will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy -# genXcode/_index.html for more information. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_DOCSET = NO - -# This tag determines the name of the docset feed. A documentation feed provides -# an umbrella under which multiple documentation sets from a single provider -# (such as a company or product suite) can be grouped. -# The default value is: Doxygen generated docs. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_FEEDNAME = "Doxygen generated docs" - -# This tag specifies a string that should uniquely identify the documentation -# set bundle. This should be a reverse domain-name style string, e.g. -# com.mycompany.MyDocSet. Doxygen will append .docset to the name. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_BUNDLE_ID = org.doxygen.Project - -# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify -# the documentation publisher. This should be a reverse domain-name style -# string, e.g. com.mycompany.MyDocSet.documentation. -# The default value is: org.doxygen.Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_ID = org.doxygen.Publisher - -# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. -# The default value is: Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_NAME = Publisher - -# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three -# additional HTML index files: index.hhp, index.hhc, and index.hhk. The -# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on -# Windows. -# -# The HTML Help Workshop contains a compiler that can convert all HTML output -# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML -# files are now used as the Windows 98 help format, and will replace the old -# Windows help format (.hlp) on all Windows platforms in the future. Compressed -# HTML files also contain an index, a table of contents, and you can search for -# words in the documentation. The HTML workshop also contains a viewer for -# compressed HTML files. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_HTMLHELP = NO - -# The CHM_FILE tag can be used to specify the file name of the resulting .chm -# file. You can add a path in front of the file if the result should not be -# written to the html output directory. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_FILE = - -# The HHC_LOCATION tag can be used to specify the location (absolute path -# including file name) of the HTML help compiler (hhc.exe). If non-empty, -# doxygen will try to run the HTML help compiler on the generated index.hhp. -# The file has to be specified with full path. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -HHC_LOCATION = - -# The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -GENERATE_CHI = NO - -# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) -# and project file content. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_INDEX_ENCODING = - -# The BINARY_TOC flag controls whether a binary table of contents is generated -# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it -# enables the Previous and Next buttons. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members to -# the table of contents of the HTML help documentation and to the tree view. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -TOC_EXPAND = NO - -# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and -# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that -# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help -# (.qch) of the generated HTML documentation. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_QHP = NO - -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify -# the file name of the resulting .qch file. The path specified is relative to -# the HTML output folder. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QCH_FILE = - -# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help -# Project output. For more information please see Qt Help Project / Namespace -# (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_NAMESPACE = org.doxygen.Project - -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt -# Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- -# folders). -# The default value is: doc. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_VIRTUAL_FOLDER = doc - -# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom -# filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_NAME = - -# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the -# custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_ATTRS = - -# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this -# project's filter section matches. Qt Help Project / Filter Attributes (see: -# http://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_SECT_FILTER_ATTRS = - -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHG_LOCATION = - -# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be -# generated, together with the HTML files, they form an Eclipse help plugin. To -# install this plugin and make it available under the help contents menu in -# Eclipse, the contents of the directory containing the HTML and XML files needs -# to be copied into the plugins directory of eclipse. The name of the directory -# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. -# After copying Eclipse needs to be restarted before the help appears. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_ECLIPSEHELP = NO - -# A unique identifier for the Eclipse help plugin. When installing the plugin -# the directory name containing the HTML and XML files should also have this -# name. Each documentation set should have its own identifier. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. - -ECLIPSE_DOC_ID = org.doxygen.Project - -# If you want full control over the layout of the generated HTML pages it might -# be necessary to disable the index and replace it with your own. The -# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top -# of each HTML page. A value of NO enables the index and the value YES disables -# it. Since the tabs in the index contain the same information as the navigation -# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -DISABLE_INDEX = NO - -# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. If the tag -# value is set to YES, a side panel will be generated containing a tree-like -# index structure (just like the one that is generated for HTML Help). For this -# to work a browser that supports JavaScript, DHTML, CSS and frames is required -# (i.e. any modern browser). Windows users are probably better off using the -# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_TREEVIEW = YES - -# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -# doxygen will group on one line in the generated HTML documentation. -# -# Note that a value of 0 will completely suppress the enum values from appearing -# in the overview section. -# Minimum value: 0, maximum value: 20, default value: 4. -# This tag requires that the tag GENERATE_HTML is set to YES. - -ENUM_VALUES_PER_LINE = 4 - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used -# to set the initial width (in pixels) of the frame in which the tree is shown. -# Minimum value: 0, maximum value: 1500, default value: 250. -# This tag requires that the tag GENERATE_HTML is set to YES. - -TREEVIEW_WIDTH = 250 - -# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to -# external symbols imported via tag files in a separate window. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -EXT_LINKS_IN_WINDOW = NO - -# Use this tag to change the font size of LaTeX formulas included as images in -# the HTML documentation. When you change the font size after a successful -# doxygen run you need to manually remove any form_*.png images from the HTML -# output directory to force them to be regenerated. -# Minimum value: 8, maximum value: 50, default value: 10. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_FONTSIZE = 10 - -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - -# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# https://www.mathjax.org) which uses client side Javascript for the rendering -# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX -# installed or if you want to formulas look prettier in the HTML output. When -# enabled you may also need to install MathJax separately and configure the path -# to it using the MATHJAX_RELPATH option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -USE_MATHJAX = NO - -# When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/latest/output.html) for more details. -# Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. -# The default value is: HTML-CSS. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_FORMAT = HTML-CSS - -# When MathJax is enabled you need to specify the location relative to the HTML -# output directory using the MATHJAX_RELPATH option. The destination directory -# should contain the MathJax.js script. For instance, if the mathjax directory -# is located at the same level as the HTML output directory, then -# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax -# Content Delivery Network so you can quickly see the result without installing -# MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. -# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/ - -# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax -# extension names that should be enabled during MathJax rendering. For example -# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_EXTENSIONS = - -# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces -# of code that will be used on startup of the MathJax code. See the MathJax site -# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an -# example see the documentation. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_CODEFILE = - -# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -# the HTML output. The underlying search engine uses javascript and DHTML and -# should work on any modern browser. Note that when using HTML help -# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) -# there is already a search function so this one should typically be disabled. -# For large projects the javascript based search engine can be slow, then -# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to -# search using the keyboard; to jump to the search box use + S -# (what the is depends on the OS and browser, but it is typically -# , /