diff --git a/Dockerfile b/Dockerfile index 02332d40b4..f21249e54e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,5 +27,9 @@ COPY --from=ffmpeg /ffmpeg-bin/* /usr/bin/ EXPOSE 8096 VOLUME /config /media RUN apt-get update \ - && apt-get install -y libfontconfig1 --no-install-recommends # needed for Skia + && apt-get install --no-install-recommends --no-install-suggests -y \ + libfontconfig1 # Required for Skia \ + && apt-get clean autoclean \ + && apt-get autoremove \ + && rm -rf /var/lib/{apt,dpkg,cache,log} ENTRYPOINT dotnet /jellyfin/jellyfin.dll -programdata /config diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 7b40f905b0..49619d5560 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -13,4 +13,11 @@ + + Jellyfin Contributors + Jellyfin.Naming + https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + https://github.com/jellyfin/jellyfin + + diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d4144d5c5c..91eaf9bbf2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -230,7 +230,6 @@ namespace Emby.Server.Implementations { get { - #if BETA return PackageVersionClass.Beta; #endif @@ -510,7 +509,7 @@ namespace Emby.Server.Implementations protected void RegisterSingleInstance(T obj, bool manageLifetime = true) where T : class { - Container.RegisterSingleton(obj); + Container.RegisterInstance(obj); if (manageLifetime) { @@ -575,7 +574,7 @@ namespace Emby.Server.Implementations } catch (Exception ex) { - Logger.LogError(ex, "Error loading assembly {file}", file); + Logger.LogError(ex, "Error loading assembly {File}", file); return null; } } diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 8b0012410a..3aab10026f 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.HttpClientManager { options.ResourcePool?.Release(); - throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true }; + throw new HttpException($"Connection to {options.Url} timed out") { IsTimedOut = true }; } if (options.LogRequest) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 163b54e39d..033ffd4c02 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -54,39 +54,33 @@ namespace Emby.Server.Implementations.HttpServer private IWebSocketListener[] _webSocketListeners = Array.Empty(); private readonly List _webSocketConnections = new List(); - public HttpListenerHost(IServerApplicationHost applicationHost, + public HttpListenerHost( + IServerApplicationHost applicationHost, ILogger logger, IServerConfigurationManager config, - string defaultRedirectPath, INetworkManager networkManager, ITextEncoding textEncoding, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, Func> funcParseFn) + string defaultRedirectPath, + INetworkManager networkManager, + ITextEncoding textEncoding, + IJsonSerializer jsonSerializer, + IXmlSerializer xmlSerializer, + Func> funcParseFn) { - Instance = this; - _appHost = applicationHost; + _logger = logger; + _config = config; DefaultRedirectPath = defaultRedirectPath; _networkManager = networkManager; _textEncoding = textEncoding; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - _config = config; - - _logger = logger; _funcParseFn = funcParseFn; - ResponseFilters = new Action[] { }; + Instance = this; + ResponseFilters = Array.Empty>(); } public string GlobalResponse { get; set; } - readonly Dictionary _mapExceptionToStatusCode = new Dictionary - { - {typeof (ResourceNotFoundException), 404}, - {typeof (RemoteServiceUnavailableException), 502}, - {typeof (FileNotFoundException), 404}, - //{typeof (DirectoryNotFoundException), 404}, - {typeof (SecurityException), 401}, - {typeof (ArgumentException), 400} - }; - protected ILogger Logger => _logger; public object CreateInstance(Type type) @@ -103,9 +97,9 @@ namespace Emby.Server.Implementations.HttpServer { //Exec all RequestFilter attributes with Priority < 0 var attributes = GetRequestFilterAttributes(requestDto.GetType()); - var i = 0; - var count = attributes.Count; + int count = attributes.Count; + int i = 0; for (; i < count && attributes[i].Priority < 0; i++) { var attribute = attributes[i]; @@ -167,10 +161,7 @@ namespace Emby.Server.Implementations.HttpServer _webSocketConnections.Add(connection); } - if (WebSocketConnected != null) - { - WebSocketConnected?.Invoke(this, new GenericEventArgs(connection)); - } + WebSocketConnected?.Invoke(this, new GenericEventArgs(connection)); } private void Connection_Closed(object sender, EventArgs e) @@ -205,26 +196,16 @@ namespace Emby.Server.Implementations.HttpServer private int GetStatusCode(Exception ex) { - if (ex is ArgumentException) + switch (ex) { - return 400; + case ArgumentException _: return 400; + case SecurityException _: return 401; + case DirectoryNotFoundException _: + case FileNotFoundException _: + case ResourceNotFoundException _: return 404; + case RemoteServiceUnavailableException _: return 502; + default: return 500; } - - var exceptionType = ex.GetType(); - - if (!_mapExceptionToStatusCode.TryGetValue(exceptionType, out var statusCode)) - { - if (ex is DirectoryNotFoundException) - { - statusCode = 404; - } - else - { - statusCode = 500; - } - } - - return statusCode; } private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, bool logExceptionMessage) @@ -310,29 +291,22 @@ namespace Emby.Server.Implementations.HttpServer } } - private readonly Dictionary _skipLogExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase) + private static readonly string[] _skipLogExtensions = { - {".js", 0}, - {".css", 0}, - {".woff", 0}, - {".woff2", 0}, - {".ttf", 0}, - {".html", 0} + ".js", + ".css", + ".woff", + ".woff2", + ".ttf", + ".html" }; private bool EnableLogging(string url, string localPath) { var extension = GetExtension(url); - if (string.IsNullOrEmpty(extension) || !_skipLogExtensions.ContainsKey(extension)) - { - if (string.IsNullOrEmpty(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) - { - return true; - } - } - - return false; + return ((string.IsNullOrEmpty(extension) || !_skipLogExtensions.Contains(extension)) + && (string.IsNullOrEmpty(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)); } private static string GetExtension(string url) @@ -555,9 +529,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase) || - string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase) || - localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1) + if (localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1) { httpRes.StatusCode = 200; httpRes.ContentType = "text/html"; @@ -711,7 +683,7 @@ namespace Emby.Server.Implementations.HttpServer }; } - _logger.LogError("Could not find handler for {pathInfo}", pathInfo); + _logger.LogError("Could not find handler for {PathInfo}", pathInfo); return null; } @@ -725,13 +697,13 @@ namespace Emby.Server.Implementations.HttpServer private void RedirectToSecureUrl(IHttpRequest httpReq, IResponse httpRes, string url) { - int currentPort; - if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) + if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { - currentPort = uri.Port; - var builder = new UriBuilder(uri); - builder.Port = _config.Configuration.PublicHttpsPort; - builder.Scheme = "https"; + var builder = new UriBuilder(uri) + { + Port = _config.Configuration.PublicHttpsPort, + Scheme = "https" + }; url = builder.Uri.ToString(); RedirectToUrl(httpRes, url); @@ -831,12 +803,6 @@ namespace Emby.Server.Implementations.HttpServer public Task DeserializeJson(Type type, Stream stream) { - //using (var reader = new StreamReader(stream)) - //{ - // var json = reader.ReadToEnd(); - // logger.LogInformation(json); - // return _jsonSerializer.DeserializeFromString(json, type); - //} return _jsonSerializer.DeserializeFromStreamAsync(stream, type); } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 851250ecd1..08e43732d6 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -410,8 +410,10 @@ namespace Emby.Server.Implementations.HttpServer serializer.WriteObject(xw, from); xw.Flush(); ms.Seek(0, SeekOrigin.Begin); - var reader = new StreamReader(ms); - return reader.ReadToEnd(); + using (var reader = new StreamReader(ms)) + { + return reader.ReadToEnd(); + } } } } @@ -423,7 +425,7 @@ namespace Emby.Server.Implementations.HttpServer { responseHeaders["ETag"] = string.Format("\"{0}\"", cacheKeyString); - var noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; + bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; if (!noCache) { @@ -461,8 +463,7 @@ namespace Emby.Server.Implementations.HttpServer }); } - public Task GetStaticFileResult(IRequest requestContext, - StaticFileResultOptions options) + public Task GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options) { var path = options.Path; var fileShare = options.FileShare; @@ -697,32 +698,26 @@ namespace Emby.Server.Implementations.HttpServer var ifModifiedSinceHeader = requestContext.Headers.Get("If-Modified-Since"); - if (!string.IsNullOrEmpty(ifModifiedSinceHeader)) + if (!string.IsNullOrEmpty(ifModifiedSinceHeader) + && DateTime.TryParse(ifModifiedSinceHeader, out DateTime ifModifiedSince) + && IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified)) { - if (DateTime.TryParse(ifModifiedSinceHeader, out var ifModifiedSince)) - { - if (IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified)) - { - return true; - } - } + return true; } var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); - var hasCacheKey = !cacheKey.Equals(Guid.Empty); + bool hasCacheKey = !cacheKey.Equals(Guid.Empty); // Validate If-None-Match - if ((hasCacheKey || !string.IsNullOrEmpty(ifNoneMatchHeader))) + if ((hasCacheKey && !string.IsNullOrEmpty(ifNoneMatchHeader))) { ifNoneMatchHeader = (ifNoneMatchHeader ?? string.Empty).Trim('\"'); - if (Guid.TryParse(ifNoneMatchHeader, out var ifNoneMatch)) + if (Guid.TryParse(ifNoneMatchHeader, out Guid ifNoneMatch) + && cacheKey.Equals(ifNoneMatch)) { - if (hasCacheKey && cacheKey.Equals(ifNoneMatch)) - { - return true; - } + return true; } } diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index bb52cd7053..891a76ec2a 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -40,8 +40,6 @@ namespace Emby.Server.Implementations.HttpServer /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public List Cookies { get; private set; } - /// /// Additional HTTP Headers /// @@ -72,7 +70,6 @@ namespace Emby.Server.Implementations.HttpServer Headers["Accept-Ranges"] = "bytes"; StatusCode = HttpStatusCode.PartialContent; - Cookies = new List(); SetRangeValues(contentLength); } @@ -220,7 +217,5 @@ namespace Emby.Server.Implementations.HttpServer get => (HttpStatusCode)Status; set => Status = (int)value; } - - public string StatusDescription { get; set; } } } diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index 80386f35e9..da2bf983a0 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -25,14 +25,11 @@ namespace Emby.Server.Implementations.HttpServer public void FilterResponse(IRequest req, IResponse res, object dto) { // Try to prevent compatibility view - //res.AddHeader("X-UA-Compatible", "IE=Edge"); res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.AddHeader("Access-Control-Allow-Origin", "*"); - var exception = dto as Exception; - - if (exception != null) + if (dto is Exception exception) { _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl); @@ -45,42 +42,26 @@ namespace Emby.Server.Implementations.HttpServer } } - var hasHeaders = dto as IHasHeaders; - - if (hasHeaders != null) + if (dto is IHasHeaders hasHeaders) { if (!hasHeaders.Headers.ContainsKey("Server")) { hasHeaders.Headers["Server"] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; - //hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1"; } // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - - if (hasHeaders.Headers.TryGetValue("Content-Length", out string contentLength) && !string.IsNullOrEmpty(contentLength)) + if (hasHeaders.Headers.TryGetValue("Content-Length", out string contentLength) + && !string.IsNullOrEmpty(contentLength)) { var length = long.Parse(contentLength, UsCulture); if (length > 0) { res.SetContentLength(length); - - //var listenerResponse = res.OriginalResponse as HttpListenerResponse; - - //if (listenerResponse != null) - //{ - // // Disable chunked encoding. Technically this is only needed when using Content-Range, but - // // anytime we know the content length there's no need for it - // listenerResponse.SendChunked = false; - // return; - //} - res.SendChunked = false; } } } - - //res.KeepAlive = false; } /// diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs index 7ce41a368f..296da2f7a0 100644 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ b/Emby.Server.Implementations/Services/HttpResult.cs @@ -15,7 +15,6 @@ namespace Emby.Server.Implementations.Services public HttpResult(object response, string contentType, HttpStatusCode statusCode) { this.Headers = new Dictionary(); - this.Cookies = new List(); this.Response = response; this.ContentType = contentType; @@ -26,8 +25,6 @@ namespace Emby.Server.Implementations.Services public IDictionary Headers { get; private set; } - public List Cookies { get; private set; } - public int Status { get; set; } public HttpStatusCode StatusCode @@ -40,15 +37,16 @@ namespace Emby.Server.Implementations.Services public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) { - var response = RequestContext != null ? RequestContext.Response : null; + var response = RequestContext == null ? null : RequestContext.Response; - var bytesResponse = this.Response as byte[]; - if (bytesResponse != null) + if (this.Response is byte[] bytesResponse) { var contentLength = bytesResponse.Length; if (response != null) + { response.SetContentLength(contentLength); + } if (contentLength > 0) { diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 62c65d32eb..a36e652d44 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Security; using System.Reflection; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Emby.Drawing; using Emby.Drawing.Skia; @@ -29,35 +30,58 @@ namespace Jellyfin.Server { public static class Program { - private static readonly TaskCompletionSource ApplicationTaskCompletionSource = new TaskCompletionSource(); - private static ILoggerFactory _loggerFactory; + private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); + private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static ILogger _logger; private static bool _restartOnShutdown; - public static async Task Main(string[] args) + public static async Task Main(string[] args) { - var options = new StartupOptions(args); - var version = Assembly.GetEntryAssembly().GetName().Version; + StartupOptions options = new StartupOptions(args); + Version version = Assembly.GetEntryAssembly().GetName().Version; if (options.ContainsOption("-v") || options.ContainsOption("--version")) { Console.WriteLine(version.ToString()); - return 0; } - var appPaths = createApplicationPaths(options); + ServerApplicationPaths appPaths = createApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); await createLogger(appPaths); - _loggerFactory = new SerilogLoggerFactory(); _logger = _loggerFactory.CreateLogger("Main"); AppDomain.CurrentDomain.UnhandledException += (sender, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); + // Intercept Ctrl+C and Ctrl+Break + Console.CancelKeyPress += (sender, e) => + { + if (_tokenSource.IsCancellationRequested) + { + return; // Already shutting down + } + e.Cancel = true; + _logger.LogInformation("Ctrl+C, shutting down"); + Environment.ExitCode = 128 + 2; + Shutdown(); + }; + + // Register a SIGTERM handler + AppDomain.CurrentDomain.ProcessExit += (sender, e) => + { + if (_tokenSource.IsCancellationRequested) + { + return; // Already shutting down + } + _logger.LogInformation("Received a SIGTERM signal, shutting down"); + Environment.ExitCode = 128 + 15; + Shutdown(); + }; + _logger.LogInformation("Jellyfin version: {Version}", version); - var environmentInfo = new EnvironmentInfo(getOperatingSystem()); + EnvironmentInfo environmentInfo = new EnvironmentInfo(getOperatingSystem()); ApplicationHost.LogEnvironmentInfo(_logger, appPaths, environmentInfo); SQLitePCL.Batteries_V2.Init(); @@ -86,8 +110,16 @@ namespace Jellyfin.Server await appHost.RunStartupTasks(); // TODO: read input for a stop command - // Block main thread until shutdown - await ApplicationTaskCompletionSource.Task; + + try + { + // Block main thread until shutdown + await Task.Delay(-1, _tokenSource.Token); + } + catch (TaskCanceledException) + { + // Don't throw on cancellation + } _logger.LogInformation("Disposing app host"); } @@ -96,8 +128,6 @@ namespace Jellyfin.Server { StartNewInstance(options); } - - return 0; } private static ServerApplicationPaths createApplicationPaths(StartupOptions options) @@ -173,7 +203,7 @@ namespace Jellyfin.Server if (!File.Exists(configPath)) { // For some reason the csproj name is used instead of the assembly name - using (var rscstr = typeof(Program).Assembly + using (Stream rscstr = typeof(Program).Assembly .GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json")) using (Stream fstr = File.Open(configPath, FileMode.CreateNew)) { @@ -259,7 +289,10 @@ namespace Jellyfin.Server public static void Shutdown() { - ApplicationTaskCompletionSource.SetResult(true); + if (!_tokenSource.IsCancellationRequested) + { + _tokenSource.Cancel(); + } } public static void Restart() diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 686bb643b1..60c8321893 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -61,8 +61,7 @@ namespace MediaBrowser.Controller.Entities } } - width /= Height.Value; - return width; + return width / height; } return base.GetDefaultPrimaryImageAspectRatio(); diff --git a/SharedVersion.cs b/SharedVersion.cs index 07026f70a3..a29381f639 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.0.1")] -[assembly: AssemblyFileVersion("10.0.1")] +[assembly: AssemblyVersion("10.0.2")] +[assembly: AssemblyFileVersion("10.0.2")] diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index 685d31a5eb..f7a1994b76 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,11 @@ +jellyfin (10.0.2-1) unstable; urgency=medium + + * Hotfix release + * jellyfin/jellyfin-web#23: Update Chromecast app ID [via direct commit] + * #540: Update Emby API keys to our own + * #541: Change ItemId to Guid in ProviderManager + * #566: Avoid printing stacktrace when bind to port 1900 fails + jellyfin (10.0.1-1) unstable; urgency=medium * Hotfix release, corrects several small bugs from 10.0.0