From 2b5d515de79f2309219459c7223b8a56269737f8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 17 Jul 2020 09:08:29 -0600 Subject: [PATCH 001/153] specify plugin repo on install --- .../Updates/InstallationManager.cs | 7 ++++++- MediaBrowser.Api/PackageService.cs | 10 ++++++++++ MediaBrowser.Model/Updates/PackageInfo.cs | 10 ++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 4f54c06dd2..7833044eda 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -161,7 +161,12 @@ namespace Emby.Server.Implementations.Updates var result = new List(); foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) { - result.AddRange(await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true)); + foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true)) + { + package.repositoryName = repository.Name; + package.repositoryUrl = repository.Url; + result.Add(package); + } } return result; diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index a84556fcc4..d36a7f55c2 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -83,6 +84,9 @@ namespace MediaBrowser.Api /// The version. [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public string Version { get; set; } + + [ApiMember(Name = "RepositoryUrl", Description = "Optional. Specify the repository to install from", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string RepositoryUrl { get; set; } } /// @@ -167,6 +171,12 @@ namespace MediaBrowser.Api public async Task Post(InstallPackage request) { var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + if (!string.IsNullOrEmpty(request.RepositoryUrl)) + { + packages = packages.Where(p => p.repositoryUrl.Equals(request.RepositoryUrl, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + var package = _installationManager.GetCompatibleVersions( packages, request.Name, diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index d9eb1386ef..98b151d551 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -52,6 +52,16 @@ namespace MediaBrowser.Model.Updates /// The versions. public IReadOnlyList versions { get; set; } + /// + /// Gets or sets the repository name. + /// + public string repositoryName { get; set; } + + /// + /// Gets or sets the repository url. + /// + public string repositoryUrl { get; set; } + /// /// Initializes a new instance of the class. /// From 50a9c8c8a747ea3a13f1622b8e77e1a9e635aacf Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 18 Aug 2020 20:22:15 -0600 Subject: [PATCH 002/153] resolve merge conflicts --- Jellyfin.Api/Controllers/PackageController.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 3d6a879093..2be0ad9fe9 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -76,6 +76,7 @@ namespace Jellyfin.Api.Controllers /// Package name. /// GUID of the associated assembly. /// Optional version. Defaults to latest version. + /// Optional. Specify the repository to install from. /// Package found. /// Package not found. /// A on success, or a if the package could not be found. @@ -86,9 +87,16 @@ namespace Jellyfin.Api.Controllers public async Task InstallPackage( [FromRoute] [Required] string? name, [FromQuery] string? assemblyGuid, - [FromQuery] string? version) + [FromQuery] string? version, + [FromQuery] string? repositoryUrl) { var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + if (!string.IsNullOrEmpty(repositoryUrl)) + { + packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + var package = _installationManager.GetCompatibleVersions( packages, name, From 852213d90cd6fe3c4e7b6922ca1f8162bc34c55e Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 18 Aug 2020 20:23:34 -0600 Subject: [PATCH 003/153] resolve merge conflicts --- MediaBrowser.Api/PackageService.cs | 207 ----------------------------- 1 file changed, 207 deletions(-) delete mode 100644 MediaBrowser.Api/PackageService.cs diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs deleted file mode 100644 index d36a7f55c2..0000000000 --- a/MediaBrowser.Api/PackageService.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Updates; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Repositories", "GET", Summary = "Gets all package repositories")] - [Authenticated] - public class GetRepositories : IReturnVoid - { - } - - [Route("/Repositories", "POST", Summary = "Sets the enabled and existing package repositories")] - [Authenticated] - public class SetRepositories : List, IReturnVoid - { - } - - /// - /// Class GetPackage. - /// - [Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")] - [Authenticated] - public class GetPackage : IReturn - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "The name of the package", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "AssemblyGuid", Description = "The guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string AssemblyGuid { get; set; } - } - - /// - /// Class GetPackages. - /// - [Route("/Packages", "GET", Summary = "Gets available packages")] - [Authenticated] - public class GetPackages : IReturn - { - } - - /// - /// Class InstallPackage. - /// - [Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")] - [Authenticated(Roles = "Admin")] - public class InstallPackage : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "Package name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "AssemblyGuid", Description = "Guid of the associated assembly", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string AssemblyGuid { get; set; } - - /// - /// Gets or sets the version. - /// - /// The version. - [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Version { get; set; } - - [ApiMember(Name = "RepositoryUrl", Description = "Optional. Specify the repository to install from", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string RepositoryUrl { get; set; } - } - - /// - /// Class CancelPackageInstallation. - /// - [Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")] - [Authenticated(Roles = "Admin")] - public class CancelPackageInstallation : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Installation Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - /// - /// Class PackageService. - /// - public class PackageService : BaseApiService - { - private readonly IInstallationManager _installationManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - - public PackageService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IInstallationManager installationManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _installationManager = installationManager; - _serverConfigurationManager = serverConfigurationManager; - } - - public object Get(GetRepositories request) - { - var result = _serverConfigurationManager.Configuration.PluginRepositories; - return ToOptimizedResult(result); - } - - public void Post(SetRepositories request) - { - _serverConfigurationManager.Configuration.PluginRepositories = request; - _serverConfigurationManager.SaveConfiguration(); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetPackage request) - { - var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult(); - var result = _installationManager.FilterPackages( - packages, - request.Name, - string.IsNullOrEmpty(request.AssemblyGuid) ? default : Guid.Parse(request.AssemblyGuid)).FirstOrDefault(); - - return ToOptimizedResult(result); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetPackages request) - { - IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - - return ToOptimizedResult(packages.ToArray()); - } - - /// - /// Posts the specified request. - /// - /// The request. - /// - public async Task Post(InstallPackage request) - { - var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - if (!string.IsNullOrEmpty(request.RepositoryUrl)) - { - packages = packages.Where(p => p.repositoryUrl.Equals(request.RepositoryUrl, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - - var package = _installationManager.GetCompatibleVersions( - packages, - request.Name, - string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid), - string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault(); - - if (package == null) - { - throw new ResourceNotFoundException( - string.Format( - CultureInfo.InvariantCulture, - "Package not found: {0}", - request.Name)); - } - - await _installationManager.InstallPackage(package); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(CancelPackageInstallation request) - { - _installationManager.CancelInstallation(new Guid(request.Id)); - } - } -} From a3020f2917176baef19653a485cc0aaaeb8d00f2 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 21 Aug 2020 19:53:55 +0200 Subject: [PATCH 004/153] Use backdrop with library name as library thumbnail --- Emby.Drawing/ImageProcessor.cs | 6 +- Emby.Drawing/NullImageEncoder.cs | 2 +- .../Images/BaseDynamicImageProvider.cs | 13 +++- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 4 +- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 60 ++++++++++--------- .../Drawing/IImageEncoder.cs | 3 +- .../Drawing/IImageProcessor.cs | 3 +- 7 files changed, 55 insertions(+), 36 deletions(-) diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index f585b90ca1..e86c22fd6e 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -36,7 +36,7 @@ namespace Emby.Drawing private readonly IImageEncoder _imageEncoder; private readonly IMediaEncoder _mediaEncoder; - private bool _disposed = false; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -466,11 +466,11 @@ namespace Emby.Drawing } /// - public void CreateImageCollage(ImageCollageOptions options) + public void CreateImageCollage(ImageCollageOptions options, string? libraryName) { _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); - _imageEncoder.CreateImageCollage(options); + _imageEncoder.CreateImageCollage(options, libraryName); _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); } diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index bbb5c17162..2a1cfd3da5 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -38,7 +38,7 @@ namespace Emby.Drawing } /// - public void CreateImageCollage(ImageCollageOptions options) + public void CreateImageCollage(ImageCollageOptions options, string? libraryName) { throw new NotImplementedException(); } diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 57302b5067..5f7e51858a 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images protected virtual IEnumerable GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable items) { + var useBackdrop = primaryItem is CollectionFolder; return items .Select(i => { + // Use Backdrop instead of Primary image for Library images. + if (useBackdrop) + { + var backdrop = i.GetImageInfo(ImageType.Backdrop, 0); + if (backdrop != null && backdrop.IsLocalFile) + { + return backdrop.Path; + } + } + var image = i.GetImageInfo(ImageType.Primary, 0); if (image != null && image.IsLocalFile) { @@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images return null; } - ImageProcessor.CreateImageCollage(options); + ImageProcessor.CreateImageCollage(options, primaryItem.Name); return outputPath; } diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a1caa751b1..6a9dbdae42 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -553,13 +553,13 @@ namespace Jellyfin.Drawing.Skia } /// - public void CreateImageCollage(ImageCollageOptions options) + public void CreateImageCollage(ImageCollageOptions options, string? libraryName) { double ratio = (double)options.Width / options.Height; if (ratio >= 1.4) { - new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName); } else if (ratio >= .9) { diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 10bb59648f..302df6fcb9 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -82,48 +82,54 @@ namespace Jellyfin.Drawing.Skia /// The path at which to place the resulting image. /// The desired width of the collage. /// The desired height of the collage. - public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) + /// The name of the library to draw on the collage. + public void BuildThumbCollage(string[] paths, string outputPath, int width, int height, string? libraryName) { - using var bitmap = BuildThumbCollageBitmap(paths, width, height); + using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName); using var outputStream = new SKFileWStream(outputPath); using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); } - private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height) + private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height, string? libraryName) { var bitmap = new SKBitmap(width, height); using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Black); - // number of images used in the thumbnail - var iCount = 3; - - // determine sizes for each image that will composited into the final image - var iSlice = Convert.ToInt32(width / iCount); - int iHeight = Convert.ToInt32(height * 1.00); - int imageIndex = 0; - for (int i = 0; i < iCount; i++) + using var backdrop = GetNextValidImage(paths, 0, out _); + if (backdrop == null) { - using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex); - imageIndex = newIndex; - if (currentBitmap == null) - { - continue; - } - - // resize to the same aspect as the original - int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); - using var resizedImage = SkiaEncoder.ResizeImage(currentBitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace)); - - // crop image - int ix = Math.Abs((iWidth - iSlice) / 2); - using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)); - // draw image onto canvas - canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0); + return bitmap; } + // resize to the same aspect as the original + var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width); + using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); + // draw the backdrop + canvas.DrawImage(residedBackdrop, 0, 0); + + // draw shadow rectangle + var paintColor = new SKPaint + { + Color = SKColors.Black.WithAlpha(0x78), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(0, 0, width, height, paintColor); + + // draw library name + var textPaint = new SKPaint + { + Color = SKColors.White, + Style = SKPaintStyle.Fill, + TextSize = 56, + TextAlign = SKTextAlign.Center, + Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), + IsAntialias = true + }; + canvas.DrawText(libraryName, width / 2f, height / 2f, textPaint); + return bitmap; } diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index e09ccd204a..0f97f05688 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -61,6 +61,7 @@ namespace MediaBrowser.Controller.Drawing /// Create an image collage. /// /// The options to use when creating the collage. - void CreateImageCollage(ImageCollageOptions options); + /// Optional. + void CreateImageCollage(ImageCollageOptions options, string? libraryName); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 69d7991652..80687c9d04 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -85,7 +85,8 @@ namespace MediaBrowser.Controller.Drawing /// Creates the image collage. /// /// The options. - void CreateImageCollage(ImageCollageOptions options); + /// The library name to draw onto the collage. + void CreateImageCollage(ImageCollageOptions options, string? libraryName); bool SupportsTransparency(string path); } From d740e7aee6582d576c5bc2688df3c356203f5974 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 21 Aug 2020 20:36:56 +0200 Subject: [PATCH 005/153] Increase font size, center text --- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 302df6fcb9..09036b9f91 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -123,12 +123,12 @@ namespace Jellyfin.Drawing.Skia { Color = SKColors.White, Style = SKPaintStyle.Fill, - TextSize = 56, + TextSize = 112, TextAlign = SKTextAlign.Center, Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), IsAntialias = true }; - canvas.DrawText(libraryName, width / 2f, height / 2f, textPaint); + canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); return bitmap; } From 9165dc3b3a94714da307079413f62326d5e585ef Mon Sep 17 00:00:00 2001 From: David Date: Sat, 22 Aug 2020 14:31:28 +0200 Subject: [PATCH 006/153] Scale down text if too long --- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 09036b9f91..0e94f87f6a 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -128,6 +128,14 @@ namespace Jellyfin.Drawing.Skia Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), IsAntialias = true }; + + // scale down text to 90% of the width if text is larger than 95% of the width + var textWidth = textPaint.MeasureText(libraryName); + if (textWidth > width * 0.95) + { + textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; + } + canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); return bitmap; From 2a84d5a6933842e99b3b1f734e9a2b7241705a7f Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 31 Aug 2020 16:35:37 +0200 Subject: [PATCH 007/153] Enable nullable for interface --- MediaBrowser.Controller/Drawing/IImageEncoder.cs | 1 + MediaBrowser.Controller/Drawing/IImageProcessor.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4e640d4215..770c6dc2d2 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#nullable enable using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index cab73ed0c8..935a790312 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#nullable enable using System; using System.Collections.Generic; @@ -75,7 +76,7 @@ namespace MediaBrowser.Controller.Drawing /// /// The options. /// Task. - Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options); + Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options); /// /// Gets the supported image output formats. From 320e3b98ab50e83932156b1a8ce7fc25685f7252 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 23:32:21 -0600 Subject: [PATCH 008/153] Add ci task to publish api client --- .ci/azure-pipelines-api-client.yml | 72 +++++++++++++++++++ .gitignore | 1 + apiclient/.openapi-generator-ignore | 2 + .../templates/typescript/package.mustache | 30 ++++++++ apiclient/templates/typescript/stable.sh | 10 +++ apiclient/templates/typescript/unstable.sh | 10 +++ 6 files changed, 125 insertions(+) create mode 100644 .ci/azure-pipelines-api-client.yml create mode 100644 apiclient/.openapi-generator-ignore create mode 100644 apiclient/templates/typescript/package.mustache create mode 100644 apiclient/templates/typescript/stable.sh create mode 100644 apiclient/templates/typescript/unstable.sh diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml new file mode 100644 index 0000000000..babee15b5a --- /dev/null +++ b/.ci/azure-pipelines-api-client.yml @@ -0,0 +1,72 @@ +parameters: + - name: LinuxImage + type: string + default: "ubuntu-latest" + - name: GeneratorVersion + type: string + default: "4.3.1" + +jobs: +- job: GenerateApiClients + displayName: 'Generate Api Clients' + + pool: + vmImage: "${{ parameters.LinuxImage }}" + + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download OpenAPI Spec Artifact' + inputs: + source: "specific" + artifact: "OpenAPI Spec" + path: "$(System.ArtifactsDirectory)/openapi" + project: "$(System.TeamProjectId)" + pipeline: "29" # The main server CI build + runVersion: "latestFromBranch" + runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" + + - task: CmdLine@2 + displayName: 'Download OpenApi Generator' + inputs: + scripts: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" + +# Generate npm api client +# Unstable + - task: npmAuthenticate@0 + displayName: 'Authenticate to unstable npm feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + + - task: CmdLine@2 + displayName: 'Build unstable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + script: 'bash ./apiclient/templates/typescript/unstable.sh axios' + + - task: Npm@1 + displayName: 'Publish unstable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + command: publish + publishRegistry: useFeed + publishFeed: jellyfin/unstable + workingDir: ./apiclient/generated/typescript/axios + +# Stable + - task: npmAuthenticate@0 + displayName: 'Authenticate to stable npm feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + + - task: CmdLine@2 + displayName: 'Build stable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + script: 'bash ./apiclient/templates/typescript/stable.sh axios' + + - task: Npm@1 + displayName: 'Publish stable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + command: publish + publishRegistry: useExternalRegistry + publishEndpoint: + workingDir: ./apiclient/generated/typescript/axios diff --git a/.gitignore b/.gitignore index 0df7606ce9..7cd3d0068a 100644 --- a/.gitignore +++ b/.gitignore @@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts web/ web-src.* MediaBrowser.WebDashboard/jellyfin-web +apiclient/generated diff --git a/apiclient/.openapi-generator-ignore b/apiclient/.openapi-generator-ignore new file mode 100644 index 0000000000..f3802cf541 --- /dev/null +++ b/apiclient/.openapi-generator-ignore @@ -0,0 +1,2 @@ +# Prevent generator from creating these files: +git_push.sh diff --git a/apiclient/templates/typescript/package.mustache b/apiclient/templates/typescript/package.mustache new file mode 100644 index 0000000000..5127917a14 --- /dev/null +++ b/apiclient/templates/typescript/package.mustache @@ -0,0 +1,30 @@ +{ + "name": "jellyfin-apiclient-{{npmName}}", + "version": "10.7.0{{snapshotVersion}}", + "description": "Jellyfin api client using {{npmName}}", + "author": "Jellyfin Contributors", + "keywords": [ + "{{npmName}}", + "typescript", + "jellyfin" + ], + "license": "GPL-3.0-only", + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "scripts": { + "build": "tsc --outDir dist/", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "axios": "^0.19.2" + }, + "devDependencies": { + "@types/node": "^12.11.5", + "typescript": "^3.6.4" + }{{#npmRepository}},{{/npmRepository}} +{{#npmRepository}} + "publishConfig": { + "registry": "{{npmRepository}}" + } +{{/npmRepository}} +} diff --git a/apiclient/templates/typescript/stable.sh b/apiclient/templates/typescript/stable.sh new file mode 100644 index 0000000000..46af6433f4 --- /dev/null +++ b/apiclient/templates/typescript/stable.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +CLIENT=$1 +openapi-generator generate \ + --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ + --generator-name typescript-${CLIENT} \ + --output ./apiclient/generated/typescript/${CLIENT} \ + --template-dir ./apiclient/templates/typescript \ + --ignore-file-override ./apiclient/.openapi-generator-ignore \ + --additional-properties=useSingleRequestParameter="true",npmName="${CLIENT}" diff --git a/apiclient/templates/typescript/unstable.sh b/apiclient/templates/typescript/unstable.sh new file mode 100644 index 0000000000..255e20c4fc --- /dev/null +++ b/apiclient/templates/typescript/unstable.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +CLIENT=$1 +openapi-generator generate \ + --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ + --generator-name typescript-${CLIENT} \ + --output ./apiclient/generated/typescript/${CLIENT} \ + --template-dir ./apiclient/templates/typescript \ + --ignore-file-override ./apiclient/.openapi-generator-ignore \ + --additional-properties=useSingleRequestParameter="true",npmName="${CLIENT}",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://dev.azure.com/jellyfin-project/jellyfin/_packaging" From 6ffffa95a7cfa092bc4dd1b75e25f1010d0d8855 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 6 Sep 2020 08:26:36 -0600 Subject: [PATCH 009/153] Use jar directly --- .ci/azure-pipelines.yml | 3 +++ apiclient/templates/typescript/stable.sh | 2 +- apiclient/templates/typescript/unstable.sh | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index b417aae678..f3e515447d 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -55,3 +55,6 @@ jobs: - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - template: azure-pipelines-package.yml + +- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: + - template: azure-pipelines-api-client.yml diff --git a/apiclient/templates/typescript/stable.sh b/apiclient/templates/typescript/stable.sh index 46af6433f4..f23a85cc96 100644 --- a/apiclient/templates/typescript/stable.sh +++ b/apiclient/templates/typescript/stable.sh @@ -1,7 +1,7 @@ #!/bin/bash CLIENT=$1 -openapi-generator generate \ +java -jar openapi-generator-cli.jar generate \ --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ --generator-name typescript-${CLIENT} \ --output ./apiclient/generated/typescript/${CLIENT} \ diff --git a/apiclient/templates/typescript/unstable.sh b/apiclient/templates/typescript/unstable.sh index 255e20c4fc..3571c8ad5c 100644 --- a/apiclient/templates/typescript/unstable.sh +++ b/apiclient/templates/typescript/unstable.sh @@ -1,7 +1,7 @@ #!/bin/bash CLIENT=$1 -openapi-generator generate \ +java -jar openapi-generator-cli.jar generate \ --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ --generator-name typescript-${CLIENT} \ --output ./apiclient/generated/typescript/${CLIENT} \ From c41ed13b3dd875498b5784f11250ae7f421d94e2 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sun, 13 Sep 2020 14:36:10 +0100 Subject: [PATCH 010/153] Commenting. --- .../MediaReceiverRegistrar/ControlHandler.cs | 18 ++- .../MediaReceiverRegistrarService.cs | 13 +- .../MediaReceiverRegistrarXmlBuilder.cs | 122 ++++++++++-------- .../ServiceActionListBuilder.cs | 48 ++++++- Emby.Dlna/Service/BaseControlHandler.cs | 20 +-- 5 files changed, 140 insertions(+), 81 deletions(-) diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index 8bf0cd961b..464f71a6f1 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Xml; @@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar { + /// + /// Defines the . + /// public class ControlHandler : BaseControlHandler { + /// + /// Initializes a new instance of the class. + /// + /// The for use with the instance. + /// The for use with the instance. public ControlHandler(IServerConfigurationManager config, ILogger logger) : base(config, logger) { @@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } + /// + /// Records that the handle is authorized in the xml stream. + /// + /// The . private static void HandleIsAuthorized(XmlWriter xmlWriter) => xmlWriter.WriteElementString("Result", "1"); + /// + /// Records that the handle is validated in the xml stream. + /// + /// The . private static void HandleIsValidated(XmlWriter xmlWriter) => xmlWriter.WriteElementString("Result", "1"); } diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs index e6d845e1e4..a5aae515c4 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Net.Http; using System.Threading.Tasks; using Emby.Dlna.Service; @@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar { + /// + /// Defines the . + /// public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar { private readonly IServerConfigurationManager _config; + /// + /// Initializes a new instance of the class. + /// + /// The for use with the instance. + /// The for use with the instance. + /// The for use with the instance. public MediaReceiverRegistrarService( ILogger logger, IHttpClientFactory httpClientFactory, @@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar /// public string GetServiceXml() { - return new MediaReceiverRegistrarXmlBuilder().GetXml(); + return MediaReceiverRegistrarXmlBuilder.GetXml(); } /// diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index 26994925d1..37840cd096 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -1,79 +1,89 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; +using MediaBrowser.Model.Dlna; namespace Emby.Dlna.MediaReceiverRegistrar { - public class MediaReceiverRegistrarXmlBuilder + /// + /// Defines the . + /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482. + /// + public static class MediaReceiverRegistrarXmlBuilder { - public string GetXml() + /// + /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar. + /// + /// An XML representation of this service. + public static string GetXml() { - return new ServiceXmlBuilder().GetXml( - new ServiceActionListBuilder().GetActions(), - GetStateVariables()); + return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables()); } + /// + /// The a list of all the state variables for this invocation. + /// + /// The . private static IEnumerable GetStateVariables() { - var list = new List(); - - list.Add(new StateVariable + var list = new List { - Name = "AuthorizationGrantedUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "AuthorizationGrantedUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_DeviceID", - DataType = "string", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_DeviceID", + DataType = "string", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "AuthorizationDeniedUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "AuthorizationDeniedUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "ValidationSucceededUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "ValidationSucceededUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_RegistrationRespMsg", - DataType = "bin.base64", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_RegistrationRespMsg", + DataType = "bin.base64", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_RegistrationReqMsg", - DataType = "bin.base64", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_RegistrationReqMsg", + DataType = "bin.base64", + SendsEvents = false + }, - list.Add(new StateVariable - { - Name = "ValidationRevokedUpdateID", - DataType = "ui4", - SendsEvents = true - }); + new StateVariable + { + Name = "ValidationRevokedUpdateID", + DataType = "ui4", + SendsEvents = true + }, - list.Add(new StateVariable - { - Name = "A_ARG_TYPE_Result", - DataType = "int", - SendsEvents = false - }); + new StateVariable + { + Name = "A_ARG_TYPE_Result", + DataType = "int", + SendsEvents = false + } + }; return list; } diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs index 13545c6894..1dc9c79c14 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs @@ -1,13 +1,19 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using Emby.Dlna.Common; +using MediaBrowser.Model.Dlna; namespace Emby.Dlna.MediaReceiverRegistrar { - public class ServiceActionListBuilder + /// + /// Defines the . + /// + public static class ServiceActionListBuilder { - public IEnumerable GetActions() + /// + /// Returns a list of services that this instance provides. + /// + /// An . + public static IEnumerable GetActions() { return new[] { @@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar }; } + /// + /// Returns the action details for "IsValidated". + /// + /// The . private static ServiceAction GetIsValidated() { var action = new ServiceAction @@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } + /// + /// Returns the action details for "IsAuthorized". + /// + /// The . private static ServiceAction GetIsAuthorized() { var action = new ServiceAction @@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } + /// + /// Returns the action details for "RegisterDevice". + /// + /// The . private static ServiceAction GetRegisterDevice() { var action = new ServiceAction @@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } + /// + /// Returns the action details for "GetValidationSucceededUpdateID". + /// + /// The . private static ServiceAction GetGetValidationSucceededUpdateID() { var action = new ServiceAction @@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } - private ServiceAction GetGetAuthorizationDeniedUpdateID() + /// + /// Returns the action details for "GetGetAuthorizationDeniedUpdateID". + /// + /// The . + private static ServiceAction GetGetAuthorizationDeniedUpdateID() { var action = new ServiceAction { @@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } - private ServiceAction GetGetValidationRevokedUpdateID() + /// + /// Returns the action details for "GetValidationRevokedUpdateID". + /// + /// The . + private static ServiceAction GetGetValidationRevokedUpdateID() { var action = new ServiceAction { @@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar return action; } - private ServiceAction GetGetAuthorizationGrantedUpdateID() + /// + /// Returns the action details for "GetAuthorizationGrantedUpdateID". + /// + /// The . + private static ServiceAction GetGetAuthorizationGrantedUpdateID() { var action = new ServiceAction { diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index d160e33393..36903b5ea8 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -60,10 +60,8 @@ namespace Emby.Dlna.Service Async = true }; - using (var reader = XmlReader.Create(streamReader, readerSettings)) - { - requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); - } + using var reader = XmlReader.Create(streamReader, readerSettings); + requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); } Logger.LogDebug("Received control request {0}", requestInfo.LocalName); @@ -124,10 +122,8 @@ namespace Emby.Dlna.Service { if (!reader.IsEmptyElement) { - using (var subReader = reader.ReadSubtree()) - { - return await ParseBodyTagAsync(subReader).ConfigureAwait(false); - } + using var subReader = reader.ReadSubtree(); + return await ParseBodyTagAsync(subReader).ConfigureAwait(false); } else { @@ -170,11 +166,9 @@ namespace Emby.Dlna.Service if (!reader.IsEmptyElement) { - using (var subReader = reader.ReadSubtree()) - { - await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); - return result; - } + using var subReader = reader.ReadSubtree(); + await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); + return result; } else { From 86ad04b6571a9e44744f135caac768e8f1acdcde Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Wed, 16 Sep 2020 12:02:00 +0100 Subject: [PATCH 011/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 59 +++++++++++++---------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index bca9e81cd0..44b5e070fb 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.Linq; using System.Security; using System.Text; -using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Server @@ -20,8 +19,9 @@ namespace Emby.Dlna.Server private readonly string _serverAddress; private readonly string _serverName; private readonly string _serverId; + private readonly string _customName; - public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId) + public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId, string customName) { if (string.IsNullOrEmpty(serverUdn)) { @@ -38,6 +38,7 @@ namespace Emby.Dlna.Server _serverAddress = serverAddress; _serverName = serverName; _serverId = serverId; + _customName = customName; } private static bool EnableAbsoluteUrls => false; @@ -168,7 +169,12 @@ namespace Emby.Dlna.Server { if (string.IsNullOrEmpty(_profile.FriendlyName)) { - return "Jellyfin - " + _serverName; + if (string.IsNullOrEmpty(_customName)) + { + return "Jellyfin - " + _serverName; + } + + return _customName; } var characterList = new List(); @@ -235,13 +241,13 @@ namespace Emby.Dlna.Server .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty)) .Append(""); builder.Append("") - .Append(BuildUrl(service.ScpdUrl)) + .Append(BuildUrl(service.ScpdUrl, true)) .Append(""); builder.Append("") - .Append(BuildUrl(service.ControlUrl)) + .Append(BuildUrl(service.ControlUrl, true)) .Append(""); builder.Append("") - .Append(BuildUrl(service.EventSubUrl)) + .Append(BuildUrl(service.EventSubUrl, true)) .Append(""); builder.Append(""); @@ -250,7 +256,7 @@ namespace Emby.Dlna.Server builder.Append(""); } - private string BuildUrl(string url) + private string BuildUrl(string url, bool absoluteUrl = false) { if (string.IsNullOrEmpty(url)) { @@ -261,7 +267,7 @@ namespace Emby.Dlna.Server url = "/dlna/" + _serverUdn + "/" + url; - if (EnableAbsoluteUrls) + if (EnableAbsoluteUrls || absoluteUrl) { url = _serverAddress.TrimEnd('/') + url; } @@ -269,7 +275,7 @@ namespace Emby.Dlna.Server return SecurityElement.Escape(url); } - private IEnumerable GetIcons() + private static IEnumerable GetIcons() => new[] { new DeviceIcon @@ -329,25 +335,26 @@ namespace Emby.Dlna.Server private IEnumerable GetServices() { - var list = new List(); - - list.Add(new DeviceService + var list = new List { - ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1", - ServiceId = "urn:upnp-org:serviceId:ContentDirectory", - ScpdUrl = "contentdirectory/contentdirectory.xml", - ControlUrl = "contentdirectory/control", - EventSubUrl = "contentdirectory/events" - }); + new DeviceService + { + ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1", + ServiceId = "urn:upnp-org:serviceId:ContentDirectory", + ScpdUrl = "contentdirectory/contentdirectory.xml", + ControlUrl = "contentdirectory/control", + EventSubUrl = "contentdirectory/events" + }, - list.Add(new DeviceService - { - ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1", - ServiceId = "urn:upnp-org:serviceId:ConnectionManager", - ScpdUrl = "connectionmanager/connectionmanager.xml", - ControlUrl = "connectionmanager/control", - EventSubUrl = "connectionmanager/events" - }); + new DeviceService + { + ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1", + ServiceId = "urn:upnp-org:serviceId:ConnectionManager", + ScpdUrl = "connectionmanager/connectionmanager.xml", + ControlUrl = "connectionmanager/control", + EventSubUrl = "connectionmanager/events" + } + }; if (_profile.EnableMSMediaReceiverRegistrar) { From c2e2e5ac0ce298e550a02eeb2e853497d7f9e647 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Wed, 16 Sep 2020 12:03:17 +0100 Subject: [PATCH 012/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 44b5e070fb..14f47e0417 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -256,6 +256,12 @@ namespace Emby.Dlna.Server builder.Append(""); } + /// + /// Builds a valid url for inclusion in the xml. + /// + /// Url to include. + /// Optional. When set to true, the absolute url is always used. + /// The url to use for the element. private string BuildUrl(string url, bool absoluteUrl = false) { if (string.IsNullOrEmpty(url)) From a6400d12c9bd8e32497f6510fe7491fd0325650a Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Wed, 16 Sep 2020 12:08:23 +0100 Subject: [PATCH 013/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 53 ++++++++++------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 14f47e0417..d449d5fe82 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Security; +using System.Security;a using System.Text; +using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Server @@ -19,9 +20,8 @@ namespace Emby.Dlna.Server private readonly string _serverAddress; private readonly string _serverName; private readonly string _serverId; - private readonly string _customName; - public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId, string customName) + public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId) { if (string.IsNullOrEmpty(serverUdn)) { @@ -38,7 +38,6 @@ namespace Emby.Dlna.Server _serverAddress = serverAddress; _serverName = serverName; _serverId = serverId; - _customName = customName; } private static bool EnableAbsoluteUrls => false; @@ -169,12 +168,7 @@ namespace Emby.Dlna.Server { if (string.IsNullOrEmpty(_profile.FriendlyName)) { - if (string.IsNullOrEmpty(_customName)) - { - return "Jellyfin - " + _serverName; - } - - return _customName; + return "Jellyfin - " + _serverName; } var characterList = new List(); @@ -281,7 +275,7 @@ namespace Emby.Dlna.Server return SecurityElement.Escape(url); } - private static IEnumerable GetIcons() + private IEnumerable GetIcons() => new[] { new DeviceIcon @@ -341,26 +335,25 @@ namespace Emby.Dlna.Server private IEnumerable GetServices() { - var list = new List - { - new DeviceService - { - ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1", - ServiceId = "urn:upnp-org:serviceId:ContentDirectory", - ScpdUrl = "contentdirectory/contentdirectory.xml", - ControlUrl = "contentdirectory/control", - EventSubUrl = "contentdirectory/events" - }, + var list = new List(); - new DeviceService - { - ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1", - ServiceId = "urn:upnp-org:serviceId:ConnectionManager", - ScpdUrl = "connectionmanager/connectionmanager.xml", - ControlUrl = "connectionmanager/control", - EventSubUrl = "connectionmanager/events" - } - }; + list.Add(new DeviceService + { + ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1", + ServiceId = "urn:upnp-org:serviceId:ContentDirectory", + ScpdUrl = "contentdirectory/contentdirectory.xml", + ControlUrl = "contentdirectory/control", + EventSubUrl = "contentdirectory/events" + }); + + list.Add(new DeviceService + { + ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1", + ServiceId = "urn:upnp-org:serviceId:ConnectionManager", + ScpdUrl = "connectionmanager/connectionmanager.xml", + ControlUrl = "connectionmanager/control", + EventSubUrl = "connectionmanager/events" + }); if (_profile.EnableMSMediaReceiverRegistrar) { From d99db543daf770a059830627dbdc255ae6abe42e Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Wed, 16 Sep 2020 12:08:37 +0100 Subject: [PATCH 014/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index d449d5fe82..1f429d0de3 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Security;a +using System.Security; using System.Text; using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; From 5cca8bffea339f1043d692f337ab002dbee3a03b Mon Sep 17 00:00:00 2001 From: spookbits <71300703+spooksbit@users.noreply.github.com> Date: Wed, 16 Sep 2020 13:17:14 -0400 Subject: [PATCH 015/153] Removed browser auto-load functionality from the server. Added profiles in launchSettings to start either the web client or the swagger API page. Removed --noautorunwebapp as this is the default functionality. --- CONTRIBUTORS.md | 1 + .../Browser/BrowserLauncher.cs | 51 ------------ .../EntryPoints/StartupWizard.cs | 83 ------------------- .../IStartupOptions.cs | 5 -- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- .../Properties/launchSettings.json | 8 +- Jellyfin.Server/StartupOptions.cs | 4 - .../Configuration/ServerConfiguration.cs | 3 - .../JellyfinApplicationFactory.cs | 3 +- 9 files changed, 10 insertions(+), 150 deletions(-) delete mode 100644 Emby.Server.Implementations/Browser/BrowserLauncher.cs delete mode 100644 Emby.Server.Implementations/EntryPoints/StartupWizard.cs diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f1fe65064b..2ec147ba2f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -103,6 +103,7 @@ - [sl1288](https://github.com/sl1288) - [sorinyo2004](https://github.com/sorinyo2004) - [sparky8251](https://github.com/sparky8251) + - [spookbits](https://github.com/spookbits) - [stanionascu](https://github.com/stanionascu) - [stevehayles](https://github.com/stevehayles) - [SuperSandro2000](https://github.com/SuperSandro2000) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs deleted file mode 100644 index f8108d1c2d..0000000000 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Browser -{ - /// - /// Assists in opening application URLs in an external browser. - /// - public static class BrowserLauncher - { - /// - /// Opens the home page of the web client. - /// - /// The app host. - public static void OpenWebApp(IServerApplicationHost appHost) - { - TryOpenUrl(appHost, "/web/index.html"); - } - - /// - /// Opens the swagger API page. - /// - /// The app host. - public static void OpenSwaggerPage(IServerApplicationHost appHost) - { - TryOpenUrl(appHost, "/api-docs/swagger"); - } - - /// - /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. - /// - /// The application host. - /// The URL to open, relative to the server base URL. - private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl) - { - try - { - string baseUrl = appHost.GetLocalApiUrl("localhost"); - appHost.LaunchUrl(baseUrl + relativeUrl); - } - catch (Exception ex) - { - var logger = appHost.Resolve>(); - logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); - } - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs deleted file mode 100644 index 2e738deeb5..0000000000 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Threading.Tasks; -using Emby.Server.Implementations.Browser; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Configuration; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// - /// Class StartupWizard. - /// - public sealed class StartupWizard : IServerEntryPoint - { - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _config; - private readonly IStartupOptions _startupOptions; - - /// - /// Initializes a new instance of the class. - /// - /// The application host. - /// The application configuration. - /// The configuration manager. - /// The application startup options. - public StartupWizard( - IServerApplicationHost appHost, - IConfiguration appConfig, - IServerConfigurationManager config, - IStartupOptions startupOptions) - { - _appHost = appHost; - _appConfig = appConfig; - _config = config; - _startupOptions = startupOptions; - } - - /// - public Task RunAsync() - { - Run(); - return Task.CompletedTask; - } - - private void Run() - { - if (!_appHost.CanLaunchWebBrowser) - { - return; - } - - // Always launch the startup wizard if possible when it has not been completed - if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient()) - { - BrowserLauncher.OpenWebApp(_appHost); - return; - } - - // Do nothing if the web app is configured to not run automatically - if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp) - { - return; - } - - // Launch the swagger page if the web client is not hosted, otherwise open the web client - if (_appConfig.HostWebClient()) - { - BrowserLauncher.OpenWebApp(_appHost); - } - else - { - BrowserLauncher.OpenSwaggerPage(_appHost); - } - } - - /// - public void Dispose() - { - } - } -} diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index e7e72c686b..4bef59543f 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -16,11 +16,6 @@ namespace Emby.Server.Implementations /// bool IsService { get; } - /// - /// Gets the value of the --noautorunwebapp command line option. - /// - bool NoAutoRunWebApp { get; } - /// /// Gets the value of the --package-name command line option. /// diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 0ac309a0b0..b66314a8ea 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -1,4 +1,4 @@ - + diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json index b6e2bcf976..ebdd0dedaa 100644 --- a/Jellyfin.Server/Properties/launchSettings.json +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -2,14 +2,20 @@ "profiles": { "Jellyfin.Server": { "commandName": "Project", + "launchBrowser": true, + "launchUrl": "web", + "applicationUrl": "http://localhost:8096", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Jellyfin.Server (nowebclient)": { "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api-docs/swagger", + "applicationUrl": "http://localhost:8096", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" }, "commandLineArgs": "--nowebclient" } diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 41a1430d26..b634340927 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -63,10 +63,6 @@ namespace Jellyfin.Server [Option("service", Required = false, HelpText = "Run as headless service.")] public bool IsService { get; set; } - /// - [Option("noautorunwebapp", Required = false, HelpText = "Run headless if startup wizard is complete.")] - public bool NoAutoRunWebApp { get; set; } - /// [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] public string? PackageName { get; set; } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 48d1a7346a..8b78ad842e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -83,8 +83,6 @@ namespace MediaBrowser.Model.Configuration /// public bool QuickConnectAvailable { get; set; } - public bool AutoRunWebApp { get; set; } - public bool EnableRemoteAccess { get; set; } /// @@ -306,7 +304,6 @@ namespace MediaBrowser.Model.Configuration DisableLiveTvChannelUserDataName = true; EnableNewOmdbSupport = true; - AutoRunWebApp = true; EnableRemoteAccess = true; QuickConnectAvailable = false; diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index 77f1640fa3..bd3d356870 100644 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs @@ -47,8 +47,7 @@ namespace Jellyfin.Api.Tests // Specify the startup command line options var commandLineOpts = new StartupOptions { - NoWebClient = true, - NoAutoRunWebApp = true + NoWebClient = true }; // Use a temporary directory for the application paths From ac32b140122f669e6b8b6b2294b83a96b6842314 Mon Sep 17 00:00:00 2001 From: spooksbit <71300703+spooksbit@users.noreply.github.com> Date: Wed, 16 Sep 2020 15:11:35 -0400 Subject: [PATCH 016/153] Update Jellyfin.Server/Properties/launchSettings.json Co-authored-by: Cody Robibero --- Jellyfin.Server/Properties/launchSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json index ebdd0dedaa..d8be520b7b 100644 --- a/Jellyfin.Server/Properties/launchSettings.json +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -15,7 +15,7 @@ "launchUrl": "api-docs/swagger", "applicationUrl": "http://localhost:8096", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" }, "commandLineArgs": "--nowebclient" } From 246ab260f71731c03e8199cbb52e0a7457aaf997 Mon Sep 17 00:00:00 2001 From: spookbits <71300703+spooksbit@users.noreply.github.com> Date: Wed, 16 Sep 2020 17:09:24 -0400 Subject: [PATCH 017/153] Do not implicitly reference ASP.NET Core Analyzers. Also do not explicitly reference AspNetCore.App (fixes compiler warning). --- Jellyfin.Server/Jellyfin.Server.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index b66314a8ea..ffab18f863 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -13,6 +13,7 @@ true true enable + true @@ -23,10 +24,6 @@ - - - - From 4db5700e18b98cb34784930707dc4ee2833865b6 Mon Sep 17 00:00:00 2001 From: Ryan Petris Date: Wed, 23 Sep 2020 14:12:26 -0700 Subject: [PATCH 018/153] Don't take a lock if there's no intention to manipulate the list of open streams. Instead, use a ConcurrentDictionary so that, in those situations, thread-safe access to the dictionary is ensured. --- .../Library/MediaSourceManager.cs | 51 +++++++------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 67cf8bf5ba..376a155705 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; - private readonly Dictionary _openStreams = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); private IMediaSourceProvider[] _providers; @@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library mediaSource.InferTotalBitrate(); } - public async Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) + public Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) { - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + var info = _openStreams.Values.FirstOrDefault(i => { - var info = _openStreams.Values.FirstOrDefault(i => + var liveStream = i as ILiveStream; + if (liveStream != null) { - var liveStream = i as ILiveStream; - if (liveStream != null) - { - return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); - } + return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); + } - return false; - }); + return false; + }); - return info as IDirectStreamProvider; - } - finally - { - _liveStreamSemaphore.Release(); - } + return Task.FromResult(info as IDirectStreamProvider); } public async Task OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) @@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library return new Tuple(info.MediaSource, info as IDirectStreamProvider); } - private async Task GetLiveStreamInfo(string id, CancellationToken cancellationToken) + private Task GetLiveStreamInfo(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + if (_openStreams.TryGetValue(id, out ILiveStream info)) { - if (_openStreams.TryGetValue(id, out ILiveStream info)) - { - return info; - } - else - { - throw new ResourceNotFoundException(); - } + return Task.FromResult(info); } - finally + else { - _liveStreamSemaphore.Release(); + return Task.FromException(new ResourceNotFoundException()); } } @@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library if (liveStream.ConsumerCount <= 0) { - _openStreams.Remove(id); + _openStreams.TryRemove(id, out _); _logger.LogInformation("Closing live stream {0}", id); From 3fa3a9d57a9e1d8ab0ae34443731a74aceed9563 Mon Sep 17 00:00:00 2001 From: Ryan Petris Date: Wed, 23 Sep 2020 14:23:04 -0700 Subject: [PATCH 019/153] Preemptively throw a LiveTvConflictException when the tracked live streams for a given device/tuner will exceed the number of supported streams. --- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 28e30fac8b..2f4c601172 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -563,6 +563,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun protected override async Task GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List currentLiveStreams, CancellationToken cancellationToken) { + var tunerCount = info.TunerCount; + + if (tunerCount > 0) + { + var tunerHostId = info.Id; + var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); + + if (liveStreams.Count() >= tunerCount) + { + throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached."); + } + } + var profile = streamId.Split('_')[0]; Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile); From 9cdef5b57ce7afc1491c4a3cad64490e0d3652fe Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 24 Sep 2020 22:27:17 +0200 Subject: [PATCH 020/153] Add series image aspect ratio when ep/season is missing an image --- Emby.Server.Implementations/Dto/DtoService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 57c1398e90..677eec3158 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1139,6 +1139,7 @@ namespace Emby.Server.Implementations.Dto if (episodeSeries != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); + AttachPrimaryImageAspectRatio(dto, episodeSeries); } } @@ -1185,6 +1186,7 @@ namespace Emby.Server.Implementations.Dto if (series != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); + AttachPrimaryImageAspectRatio(dto, series); } } } From a9864368c46c3a8c934216052c10c18cbb7d4bdc Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 17:25:50 +0100 Subject: [PATCH 021/153] Update PlayToController.cs --- Emby.Dlna/PlayTo/PlayToController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 328759c5bc..f1eb5b30ea 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -886,7 +886,10 @@ namespace Emby.Dlna.PlayTo return null; } - mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); + if (_mediaSourceManager != null) + { + mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); + } return mediaSource; } From 293237b714835fadd0a7c387815d146a2e4b810b Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 18:40:10 +0100 Subject: [PATCH 022/153] Update BaseControlHandler.cs --- Emby.Dlna/Service/BaseControlHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index d160e33393..ea17773b1a 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -150,7 +150,7 @@ namespace Emby.Dlna.Service } } - return new ControlRequestInfo(); + throw new EndOfStreamException("Stream ended but no body tag found."); } private async Task ParseBodyTagAsync(XmlReader reader) From 12b5f1127e0bef04173a9ad0f80b13e2ec86552f Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 18:46:20 +0100 Subject: [PATCH 023/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 1f429d0de3..98945c2443 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -235,13 +235,13 @@ namespace Emby.Dlna.Server .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty)) .Append(""); builder.Append("") - .Append(BuildUrl(service.ScpdUrl, true)) + .Append(BuildUrl(service.ScpdUrl)) .Append(""); builder.Append("") - .Append(BuildUrl(service.ControlUrl, true)) + .Append(BuildUrl(service.ControlUrl)) .Append(""); builder.Append("") - .Append(BuildUrl(service.EventSubUrl, true)) + .Append(BuildUrl(service.EventSubUrl)) .Append(""); builder.Append(""); From f3a90bab477ed7008c2569dcd3d89f21a92558fb Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 18:46:52 +0100 Subject: [PATCH 024/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 98945c2443..573f7adc0c 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -249,14 +249,8 @@ namespace Emby.Dlna.Server builder.Append(""); } - - /// - /// Builds a valid url for inclusion in the xml. - /// - /// Url to include. - /// Optional. When set to true, the absolute url is always used. - /// The url to use for the element. - private string BuildUrl(string url, bool absoluteUrl = false) + + private string BuildUrl(string ure) { if (string.IsNullOrEmpty(url)) { @@ -267,7 +261,7 @@ namespace Emby.Dlna.Server url = "/dlna/" + _serverUdn + "/" + url; - if (EnableAbsoluteUrls || absoluteUrl) + if (EnableAbsoluteUrls) { url = _serverAddress.TrimEnd('/') + url; } From a52ab69e13d2b924522a21fa4da98f38ec298bd1 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 18:47:24 +0100 Subject: [PATCH 025/153] Update DescriptionXmlBuilder.cs --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 573f7adc0c..bca9e81cd0 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -249,8 +249,8 @@ namespace Emby.Dlna.Server builder.Append(""); } - - private string BuildUrl(string ure) + + private string BuildUrl(string url) { if (string.IsNullOrEmpty(url)) { From 63571578ae04ec3b16f8cc5c5b2f9252de78aeda Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 19:44:16 +0100 Subject: [PATCH 026/153] Update BaseControlHandler.cs --- Emby.Dlna/Service/BaseControlHandler.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index ea17773b1a..39004d3944 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -155,7 +155,7 @@ namespace Emby.Dlna.Service private async Task ParseBodyTagAsync(XmlReader reader) { - var result = new ControlRequestInfo(); + string namespaceURI = null, localName = null; await reader.MoveToContentAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false); @@ -165,11 +165,12 @@ namespace Emby.Dlna.Service { if (reader.NodeType == XmlNodeType.Element) { - result.LocalName = reader.LocalName; - result.NamespaceURI = reader.NamespaceURI; + localName = reader.LocalName; + namespaceURI = reader.NamespaceURI; if (!reader.IsEmptyElement) { + var result = new ControlRequestInfo(localName, namespaceURI); using (var subReader = reader.ReadSubtree()) { await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); @@ -187,7 +188,12 @@ namespace Emby.Dlna.Service } } - return result; + if (localName != null && namespaceURI != null) + { + return new ControlRequestInfo(localName, namespaceURI); + } + + throw new EndOfStreamException("Stream ended but no control found."); } private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary headers) @@ -234,11 +240,18 @@ namespace Emby.Dlna.Service private class ControlRequestInfo { + public ControlRequestInfo(string localName, string namespaceUri) + { + LocalName = localName; + NamespaceURI = namespaceUri; + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + public string LocalName { get; set; } public string NamespaceURI { get; set; } - public Dictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Headers { get; } } } } From 75677791275792e8dcfae1e9361b93af75443a75 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 19:51:37 +0100 Subject: [PATCH 027/153] Update BaseControlHandler.cs --- Emby.Dlna/Service/BaseControlHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 39004d3944..776882aa55 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -188,7 +188,7 @@ namespace Emby.Dlna.Service } } - if (localName != null && namespaceURI != null) + if (localName != null && namespaceURI != null) { return new ControlRequestInfo(localName, namespaceURI); } From 05fa95f149582b3de1881c42aaa0ac00408a9947 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sat, 26 Sep 2020 19:33:59 -0700 Subject: [PATCH 028/153] Increase scan speed for music libraries --- .../Resolvers/Audio/MusicAlbumResolver.cs | 70 ++++++++++--------- .../Resolvers/Audio/MusicArtistResolver.cs | 14 +++- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 79b6dded3b..ff485a18e7 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Emby.Naming.Audio; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -113,50 +115,50 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio IFileSystem fileSystem, ILibraryManager libraryManager) { + // check for audio files before digging down into directories + var firstAudioFile = list + .Where(fileSystemInfo => !fileSystemInfo.IsDirectory) + .FirstOrDefault(fileSystemInfo => libraryManager.IsAudioFile(fileSystemInfo.FullName)); + if (firstAudioFile != null) + { + // at least one audio file exists + return true; + } + + if (!allowSubfolders) + { + // not music since no audio file exists and we're not looking into subfolders + return false; + } + var discSubfolderCount = 0; var notMultiDisc = false; var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var parser = new AlbumParser(namingOptions); - foreach (var fileSystemInfo in list) + + var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory); + + var result = Parallel.ForEach(directories, (fileSystemInfo, state) => { - if (fileSystemInfo.IsDirectory) + var path = fileSystemInfo.FullName; + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); + + if (hasMusic) { - if (allowSubfolders) + if (parser.IsMultiPart(path)) { - if (notMultiDisc) - { - continue; - } - - var path = fileSystemInfo.FullName; - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); - - if (hasMusic) - { - if (parser.IsMultiPart(path)) - { - logger.LogDebug("Found multi-disc folder: " + path); - discSubfolderCount++; - } - else - { - // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album - notMultiDisc = true; - } - } + logger.LogDebug("Found multi-disc folder: " + path); + discSubfolderCount++; + } + else + { + // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album + notMultiDisc = true; + state.Stop(); } } - else - { - var fullName = fileSystemInfo.FullName; - - if (libraryManager.IsAudioFile(fullName)) - { - return true; - } - } - } + }); if (notMultiDisc) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 5f5cd0e928..e9e688fa67 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); // If we contain an album assume we are an artist folder - return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null; + var directories = args.FileSystemChildren.Where(i => i.IsDirectory); + + var result = Parallel.ForEach(directories, (fileSystemInfo, state) => + { + if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService)) + { + // stop once we see a music album + state.Stop(); + } + }); + + return !result.IsCompleted ? new MusicArtist() : null; } } } From 72534f9d667f2457cbe96ea870d140528e366ba2 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 25 Sep 2020 09:25:59 +0200 Subject: [PATCH 029/153] Use SessionMessageType for WebSocket messages --- Emby.Dlna/PlayTo/PlayToController.cs | 8 +-- .../EntryPoints/LibraryChangedNotifier.cs | 7 +-- .../EntryPoints/RecordingNotifier.cs | 11 ++-- .../EntryPoints/UserDataChangeNotifier.cs | 2 +- .../HttpServer/WebSocketConnection.cs | 5 +- .../Session/SessionManager.cs | 28 +++++------ .../Session/SessionWebSocketListener.cs | 3 +- .../Session/WebSocketController.cs | 3 +- .../ActivityLogWebSocketListener.cs | 14 ++++-- .../ScheduledTasksWebSocketListener.cs | 14 ++++-- .../SessionInfoWebSocketListener.cs | 9 +++- .../Consumers/System/TaskCompletedNotifier.cs | 3 +- .../PluginInstallationCancelledNotifier.cs | 3 +- .../PluginInstallationFailedNotifier.cs | 3 +- .../Updates/PluginInstalledNotifier.cs | 3 +- .../Updates/PluginInstallingNotifier.cs | 3 +- .../Updates/PluginUninstalledNotifier.cs | 3 +- .../Consumers/Users/UserDeletedNotifier.cs | 3 +- .../Consumers/Users/UserUpdatedNotifier.cs | 3 +- .../Net/BasePeriodicWebSocketListener.cs | 27 +++++++--- .../Session/ISessionController.cs | 3 +- .../Session/ISessionManager.cs | 8 +-- MediaBrowser.Model/Net/WebSocketMessage.cs | 3 +- .../Session/SessionMessageType.cs | 50 +++++++++++++++++++ 24 files changed, 156 insertions(+), 63 deletions(-) create mode 100644 MediaBrowser.Model/Session/SessionMessageType.cs diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 460ac2d8d1..d09a11930d 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo } /// - public Task SendMessage(string name, Guid messageId, T data, CancellationToken cancellationToken) + public Task SendMessage(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken) { if (_disposed) { @@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo return Task.CompletedTask; } - if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) + if (name == SessionMessageType.Play) { return SendPlayCommand(data as PlayRequest, cancellationToken); } - if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) + if (name == SessionMessageType.PlayState) { return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); } - if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) + if (name == SessionMessageType.GeneralCommand) { return SendGeneralCommand(data as GeneralCommand, cancellationToken); } diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index c9d21d9638..ff64e217a0 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -105,7 +106,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None); + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); } catch { @@ -123,7 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None); + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); } catch { @@ -345,7 +346,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - await _sessionManager.SendMessageToUserSessions(new List { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false); + await _sessionManager.SendMessageToUserSessions(new List { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 44d2580d68..824bb85f44 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs e) { - await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false); } private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs e) { - await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false); } private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs e) { - await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false); } private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs e) { - await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false); } - private async Task SendMessage(string name, TimerEventInfo info) + private async Task SendMessage(SessionMessageType name, TimerEventInfo info) { var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 3618b88c5d..ff09cc81ed 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.EntryPoints private Task SendNotifications(Guid userId, List changedItems, CancellationToken cancellationToken) { - return _sessionManager.SendMessageToUserSessions(new List { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); + return _sessionManager.SendMessageToUserSessions(new List { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); } private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List changedItems) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 7eae4e7646..fed2addf80 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Json; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer Connection = this }; - if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal)) + if (info.MessageType == SessionMessageType.KeepAlive) { await SendKeepAliveResponse().ConfigureAwait(false); } @@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer new WebSocketMessage { MessageId = Guid.NewGuid(), - MessageType = "KeepAlive" + MessageType = SessionMessageType.KeepAlive }, CancellationToken.None); } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e42d478533..df1b1e4525 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1064,10 +1064,10 @@ namespace Emby.Server.Implementations.Session AssertCanControl(session, controllingSession); } - return SendMessageToSession(session, "GeneralCommand", command, cancellationToken); + return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken); } - private static async Task SendMessageToSession(SessionInfo session, string name, T data, CancellationToken cancellationToken) + private static async Task SendMessageToSession(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken) { var controllers = session.SessionControllers; var messageId = Guid.NewGuid(); @@ -1078,7 +1078,7 @@ namespace Emby.Server.Implementations.Session } } - private static Task SendMessageToSessions(IEnumerable sessions, string name, T data, CancellationToken cancellationToken) + private static Task SendMessageToSessions(IEnumerable sessions, SessionMessageType name, T data, CancellationToken cancellationToken) { IEnumerable GetTasks() { @@ -1178,7 +1178,7 @@ namespace Emby.Server.Implementations.Session } } - await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false); } /// @@ -1186,7 +1186,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false); } /// @@ -1194,7 +1194,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false); } private IEnumerable TranslateItemForPlayback(Guid id, User user) @@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session } } - return SendMessageToSession(session, "Playstate", command, cancellationToken); + return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken); } private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession) @@ -1322,7 +1322,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken); + return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken); } /// @@ -1334,7 +1334,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken); + return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken); } /// @@ -1348,7 +1348,7 @@ namespace Emby.Server.Implementations.Session _logger.LogDebug("Beginning SendServerRestartNotification"); - return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken); + return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken); } /// @@ -1866,7 +1866,7 @@ namespace Emby.Server.Implementations.Session } /// - public Task SendMessageToAdminSessions(string name, T data, CancellationToken cancellationToken) + public Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1879,7 +1879,7 @@ namespace Emby.Server.Implementations.Session } /// - public Task SendMessageToUserSessions(List userIds, string name, Func dataFn, CancellationToken cancellationToken) + public Task SendMessageToUserSessions(List userIds, SessionMessageType name, Func dataFn, CancellationToken cancellationToken) { CheckDisposed(); @@ -1894,7 +1894,7 @@ namespace Emby.Server.Implementations.Session } /// - public Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken) + public Task SendMessageToUserSessions(List userIds, SessionMessageType name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1903,7 +1903,7 @@ namespace Emby.Server.Implementations.Session } /// - public Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken) + public Task SendMessageToUserDeviceSessions(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken) { CheckDisposed(); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 15c2af220d..a5f8479537 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -8,6 +8,7 @@ using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -316,7 +317,7 @@ namespace Emby.Server.Implementations.Session return webSocket.SendAsync( new WebSocketMessage { - MessageType = "ForceKeepAlive", + MessageType = SessionMessageType.ForceKeepAlive, Data = WebSocketLostTimeout }, CancellationToken.None); diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index 94604ca1e0..b986ffa1cd 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.Session /// public Task SendMessage( - string name, + SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken) diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 849b3b7095..77d55828d1 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.WebSocketListeners @@ -29,11 +30,14 @@ namespace Jellyfin.Api.WebSocketListeners _activityManager.EntryCreated += OnEntryCreated; } - /// - /// Gets the name. - /// - /// The name. - protected override string Name => "ActivityLogEntry"; + /// + protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry; + + /// + protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart; + + /// + protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop; /// /// Gets the data to send. diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs index 8a966c1376..80314b9236 100644 --- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Session; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -33,11 +34,14 @@ namespace Jellyfin.Api.WebSocketListeners _taskManager.TaskCompleted += OnTaskCompleted; } - /// - /// Gets the name. - /// - /// The name. - protected override string Name => "ScheduledTasksInfo"; + /// + protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo; + + /// + protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart; + + /// + protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop; /// /// Gets the data to send. diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 1fb5dc412c..1cf43a0053 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.WebSocketListeners @@ -34,7 +35,13 @@ namespace Jellyfin.Api.WebSocketListeners } /// - protected override string Name => "Sessions"; + protected override SessionMessageType Type => SessionMessageType.Sessions; + + /// + protected override SessionMessageType StartType => SessionMessageType.SessionsStart; + + /// + protected override SessionMessageType StopType => SessionMessageType.SessionsStop; /// /// Gets the data to send. diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs index 80ed56cd8f..0993c6df7b 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; using MediaBrowser.Model.Tasks; namespace Jellyfin.Server.Implementations.Events.Consumers.System @@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System /// public async Task OnEvent(TaskCompletionEventArgs eventArgs) { - await _sessionManager.SendMessageToAdminSessions("ScheduledTaskEnded", eventArgs.Result, CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendMessageToAdminSessions(SessionMessageType.ScheduledTaskEnded, eventArgs.Result, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs index 1c600683a9..1d790da6b2 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Updates { @@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates /// public async Task OnEvent(PluginInstallationCancelledEventArgs eventArgs) { - await _sessionManager.SendMessageToAdminSessions("PackageInstallationCancelled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCancelled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs index ea0c878d42..a1faf18fc4 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Updates { @@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates /// public async Task OnEvent(InstallationFailedEventArgs eventArgs) { - await _sessionManager.SendMessageToAdminSessions("PackageInstallationFailed", eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationFailed, eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs index 3dda5a04c4..bd1a714045 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Updates { @@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates /// public async Task OnEvent(PluginInstalledEventArgs eventArgs) { - await _sessionManager.SendMessageToAdminSessions("PackageInstallationCompleted", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCompleted, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs index f691d11a7d..b513ac64ae 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Updates { @@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates /// public async Task OnEvent(PluginInstallingEventArgs eventArgs) { - await _sessionManager.SendMessageToAdminSessions("PackageInstalling", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstalling, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs index 709692f6bb..1fd7b9adfc 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Updates { @@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates /// public async Task OnEvent(PluginUninstalledEventArgs eventArgs) { - await _sessionManager.SendMessageToAdminSessions("PluginUninstalled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageUninstalled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs index 10367a939b..303e886210 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Events.Users; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Users { @@ -30,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users { await _sessionManager.SendMessageToUserSessions( new List { eventArgs.Argument.Id }, - "UserDeleted", + SessionMessageType.UserDeleted, eventArgs.Argument.Id.ToString("N", CultureInfo.InvariantCulture), CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs index 6081dd044f..a14911b94a 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs @@ -6,6 +6,7 @@ using Jellyfin.Data.Events.Users; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; namespace Jellyfin.Server.Implementations.Events.Consumers.Users { @@ -33,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users { await _sessionManager.SendMessageToUserSessions( new List { e.Argument.Id }, - "UserUpdated", + SessionMessageType.UserUpdated, _userManager.GetUserDto(e.Argument), CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 916dea58be..28227603b2 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -8,6 +8,7 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Net @@ -28,10 +29,22 @@ namespace MediaBrowser.Controller.Net new List>(); /// - /// Gets the name. + /// Gets the type used for the messages sent to the client. /// - /// The name. - protected abstract string Name { get; } + /// The type. + protected abstract SessionMessageType Type { get; } + + /// + /// Gets the message type received from the client to start sending messages. + /// + /// The type. + protected abstract SessionMessageType StartType { get; } + + /// + /// Gets the message type received from the client to stop sending messages. + /// + /// The type. + protected abstract SessionMessageType StopType { get; } /// /// Gets the data to send. @@ -66,12 +79,12 @@ namespace MediaBrowser.Controller.Net throw new ArgumentNullException(nameof(message)); } - if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase)) + if (message.MessageType == StartType) { Start(message); } - if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase)) + if (message.MessageType == StopType) { Stop(message); } @@ -159,7 +172,7 @@ namespace MediaBrowser.Controller.Net new WebSocketMessage { MessageId = Guid.NewGuid(), - MessageType = Name, + MessageType = Type, Data = data }, cancellationToken).ConfigureAwait(false); @@ -176,7 +189,7 @@ namespace MediaBrowser.Controller.Net } catch (Exception ex) { - Logger.LogError(ex, "Error sending web socket message {Name}", Name); + Logger.LogError(ex, "Error sending web socket message {Name}", Type); DisposeConnection(tuple); } } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index 22d6e2a04e..bc4ccd44ca 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Session { @@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Session /// /// Sends the message. /// - Task SendMessage(string name, Guid messageId, T data, CancellationToken cancellationToken); + Task SendMessage(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 228b2331dc..04c3004ee6 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -188,16 +188,16 @@ namespace MediaBrowser.Controller.Session /// The data. /// The cancellation token. /// Task. - Task SendMessageToAdminSessions(string name, T data, CancellationToken cancellationToken); + Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken); /// /// Sends the message to user sessions. /// /// /// Task. - Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken); + Task SendMessageToUserSessions(List userIds, SessionMessageType name, T data, CancellationToken cancellationToken); - Task SendMessageToUserSessions(List userIds, string name, Func dataFn, CancellationToken cancellationToken); + Task SendMessageToUserSessions(List userIds, SessionMessageType name, Func dataFn, CancellationToken cancellationToken); /// /// Sends the message to user device sessions. @@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Session /// The data. /// The cancellation token. /// Task. - Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken); + Task SendMessageToUserDeviceSessions(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken); /// /// Sends the restart required message. diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs index 660eebeda6..bffbbe612d 100644 --- a/MediaBrowser.Model/Net/WebSocketMessage.cs +++ b/MediaBrowser.Model/Net/WebSocketMessage.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using MediaBrowser.Model.Session; namespace MediaBrowser.Model.Net { @@ -15,7 +16,7 @@ namespace MediaBrowser.Model.Net /// Gets or sets the type of the message. /// /// The type of the message. - public string MessageType { get; set; } + public SessionMessageType MessageType { get; set; } public Guid MessageId { get; set; } diff --git a/MediaBrowser.Model/Session/SessionMessageType.cs b/MediaBrowser.Model/Session/SessionMessageType.cs new file mode 100644 index 0000000000..23c41026dc --- /dev/null +++ b/MediaBrowser.Model/Session/SessionMessageType.cs @@ -0,0 +1,50 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Session +{ + /// + /// The different kinds of messages that are used in the WebSocket api. + /// + public enum SessionMessageType + { + // Server -> Client + ForceKeepAlive, + GeneralCommand, + UserDataChanged, + Sessions, + Play, + SyncPlayCommand, + SyncPlayGroupUpdate, + PlayState, + RestartRequired, + ServerShuttingDown, + ServerRestarting, + LibraryChanged, + UserDeleted, + UserUpdated, + SeriesTimerCreated, + TimerCreated, + SeriesTimerCancelled, + TimerCancelled, + RefreshProgress, + ScheduledTaskEnded, + PackageInstallationCancelled, + PackageInstallationFailed, + PackageInstallationCompleted, + PackageInstalling, + PackageUninstalled, + ActivityLogEntry, + ScheduledTasksInfo, + + // Client -> Server + ActivityLogEntryStart, + ActivityLogEntryStop, + SessionsStart, + SessionsStop, + ScheduledTasksInfoStart, + ScheduledTasksInfoStop, + + // Shared + KeepAlive, + } +} From ac790cd77b81a8235f6d1faf9512c85c96fcd088 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 27 Sep 2020 09:45:11 -0600 Subject: [PATCH 030/153] Properly handle null structs in json --- .../Converters/JsonNullableStructConverter.cs | 39 ++++++++++--------- .../JsonNullableStructConverterFactory.cs | 27 +++++++++++++ MediaBrowser.Common/Json/JsonDefaults.cs | 7 +--- 3 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs index cffc41ba34..0501f7b2a1 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs @@ -8,37 +8,38 @@ namespace MediaBrowser.Common.Json.Converters /// Converts a nullable struct or value to/from JSON. /// Required - some clients send an empty string. /// - /// The struct type. - public class JsonNullableStructConverter : JsonConverter - where T : struct + /// The struct type. + public class JsonNullableStructConverter : JsonConverter + where TStruct : struct { - private readonly JsonConverter _baseJsonConverter; - - /// - /// Initializes a new instance of the class. - /// - /// The base json converter. - public JsonNullableStructConverter(JsonConverter baseJsonConverter) - { - _baseJsonConverter = baseJsonConverter; - } - /// - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - // Handle empty string. + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + // Token is empty string. if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty)) { return null; } - return _baseJsonConverter.Read(ref reader, typeToConvert, options); + return JsonSerializer.Deserialize(ref reader, options); } /// - public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options) { - _baseJsonConverter.Write(writer, value, options); + if (value.HasValue) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + else + { + writer.WriteNullValue(); + } } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs new file mode 100644 index 0000000000..d5b54e3ca8 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// Json nullable struct converter factory. + /// + public class JsonNullableStructConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsGenericType + && typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>) + && typeToConvert.GenericTypeArguments[0].IsValueType; + } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GenericTypeArguments[0]; + return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType)); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 67f7e8f14a..6605ae9624 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -39,14 +39,9 @@ namespace MediaBrowser.Common.Json NumberHandling = JsonNumberHandling.AllowReadingFromString }; - // Get built-in converters for fallback converting. - var baseNullableInt32Converter = (JsonConverter)options.GetConverter(typeof(int?)); - var baseNullableInt64Converter = (JsonConverter)options.GetConverter(typeof(long?)); - options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); - options.Converters.Add(new JsonNullableStructConverter(baseNullableInt32Converter)); - options.Converters.Add(new JsonNullableStructConverter(baseNullableInt64Converter)); + options.Converters.Add(new JsonNullableStructConverterFactory()); return options; } From 75041e7f39f76a39fcfae15d6cd2aee510b4d599 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 12:56:12 -0700 Subject: [PATCH 031/153] interlocked increment --- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index ff485a18e7..d690a383d3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Emby.Naming.Audio; using MediaBrowser.Controller.Entities.Audio; @@ -132,7 +133,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } var discSubfolderCount = 0; - var notMultiDisc = false; var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var parser = new AlbumParser(namingOptions); @@ -149,18 +149,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (parser.IsMultiPart(path)) { logger.LogDebug("Found multi-disc folder: " + path); - discSubfolderCount++; + Interlocked.Increment(ref discSubfolderCount); } else { // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album - notMultiDisc = true; state.Stop(); } } }); - if (notMultiDisc) + if (!result.IsCompleted) { return false; } From 3cfbe6e3401546a56abc826736578d9bfa054d99 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 13:28:19 -0700 Subject: [PATCH 032/153] better audio file check --- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index d690a383d3..18ceb5e761 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -117,10 +117,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio ILibraryManager libraryManager) { // check for audio files before digging down into directories - var firstAudioFile = list - .Where(fileSystemInfo => !fileSystemInfo.IsDirectory) - .FirstOrDefault(fileSystemInfo => libraryManager.IsAudioFile(fileSystemInfo.FullName)); - if (firstAudioFile != null) + var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName)); + if (foundAudioFile) { // at least one audio file exists return true; From 12275e5e7bfe46e407606a977f1ca3a54fb9e62c Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 16:16:19 -0700 Subject: [PATCH 033/153] Fix invalid operation exception in TvdbEpisodeImageProvider.GetImages --- .../TheTvdb/TvdbEpisodeImageProvider.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index de2f6875f8..50a876d6c5 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -57,21 +57,28 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb // Process images try { - var episodeInfo = new EpisodeInfo + string episodeTvdbId = null; + + if (episode.IndexNumber.HasValue && episode.ParentIndexNumber.HasValue) { - IndexNumber = episode.IndexNumber.Value, - ParentIndexNumber = episode.ParentIndexNumber.Value, - SeriesProviderIds = series.ProviderIds, - SeriesDisplayOrder = series.DisplayOrder - }; - string episodeTvdbId = await _tvdbClientManager - .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); + var episodeInfo = new EpisodeInfo + { + IndexNumber = episode.IndexNumber.Value, + ParentIndexNumber = episode.ParentIndexNumber.Value, + SeriesProviderIds = series.ProviderIds, + SeriesDisplayOrder = series.DisplayOrder + }; + + episodeTvdbId = await _tvdbClientManager + .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); + } + if (string.IsNullOrEmpty(episodeTvdbId)) { _logger.LogError( "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - episodeInfo.ParentIndexNumber, - episodeInfo.IndexNumber, + episode.ParentIndexNumber, + episode.IndexNumber, series.GetProviderId(MetadataProvider.Tvdb)); return imageResult; } From 303eccaffeb42eb39b04f3ed372dc9fe84de5455 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 16:22:52 -0700 Subject: [PATCH 034/153] Fix InvalidOperationException in TvdbSeriesProvider --- .../Plugins/TheTvdb/TvdbSeriesProvider.cs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index ca9b1d738f..b637fc8b29 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -123,7 +123,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb await _tvdbClientManager .GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken) .ConfigureAwait(false); - MapSeriesToResult(result, seriesResult.Data, metadataLanguage); + await MapSeriesToResult(result, seriesResult.Data, metadataLanguage).ConfigureAwait(false); } catch (TvDbServerException e) { @@ -297,7 +297,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return name.Trim(); } - private void MapSeriesToResult(MetadataResult result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) + private async Task MapSeriesToResult(MetadataResult result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) { Series series = result.Item; series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString()); @@ -340,20 +340,25 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { try { - var episodeSummary = _tvdbClientManager - .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data; - var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); - var episodeQuery = new EpisodeQuery + var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false); + + if (episodeSummary.Data.AiredSeasons.Length > 0) { - AiredSeason = maxSeasonNumber - }; - var episodesPage = - _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; - result.Item.EndDate = episodesPage.Select(e => + var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); + var episodeQuery = new EpisodeQuery { - DateTime.TryParse(e.FirstAired, out var firstAired); - return firstAired; - }).Max(); + AiredSeason = maxSeasonNumber + }; + var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false); + + var episodeDates = episodesPage.Data + .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null) + .Where(dt => dt.HasValue); + if (episodeDates.Any()) + { + result.Item.EndDate = episodeDates.Max().Value; + } + } } catch (TvDbServerException e) { From 881a7fa90805ac96d1e43c426c50dbe575297a9b Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 16:57:43 -0700 Subject: [PATCH 035/153] update based on suggestions --- .../Plugins/TheTvdb/TvdbSeriesProvider.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index b637fc8b29..cdb159c0f0 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Text; @@ -342,9 +343,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false); - if (episodeSummary.Data.AiredSeasons.Length > 0) + if (episodeSummary.Data.AiredSeasons.Length != 0) { - var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); + var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Max(s => Convert.ToInt32(s, CultureInfo.InvariantCulture)); var episodeQuery = new EpisodeQuery { AiredSeason = maxSeasonNumber @@ -353,10 +354,11 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var episodeDates = episodesPage.Data .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null) - .Where(dt => dt.HasValue); - if (episodeDates.Any()) + .Where(dt => dt.HasValue) + .ToList(); + if (episodeDates.Count != 0) { - result.Item.EndDate = episodeDates.Max().Value; + result.Item.EndDate = episodeDates.Max(); } } } From 89041982c212049469c2e292c6562a15daa7b9b5 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 17:02:10 -0700 Subject: [PATCH 036/153] Use ConcurrentDictionary's in DirectoryService --- .../Providers/DirectoryService.cs | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index f77455485a..c4b58918c8 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.IO; @@ -11,11 +12,11 @@ namespace MediaBrowser.Controller.Providers { private readonly IFileSystem _fileSystem; - private readonly Dictionary _cache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _fileCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _fileCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary> _filePathCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _filePathCache = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); public DirectoryService(IFileSystem fileSystem) { @@ -24,14 +25,7 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata[] GetFileSystemEntries(string path) { - if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries)) - { - entries = _fileSystem.GetFileSystemEntries(path).ToArray(); - - _cache[path] = entries; - } - - return entries; + return _cache.GetOrAdd(path, (p) => _fileSystem.GetFileSystemEntries(p).ToArray()); } public List GetFiles(string path) @@ -51,21 +45,19 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata GetFile(string path) { - if (!_fileCache.TryGetValue(path, out FileSystemMetadata file)) + var result = _fileCache.GetOrAdd(path, (p) => { - file = _fileSystem.GetFileInfo(path); + var file = _fileSystem.GetFileInfo(path); + return (file != null && file.Exists) ? file : null; + }); - if (file != null && file.Exists) - { - _fileCache[path] = file; - } - else - { - return null; - } + if (result == null) + { + // lets not store null results in the cache + _fileCache.TryRemove(path, out FileSystemMetadata removed); } - return file; + return result; } public IReadOnlyList GetFilePaths(string path) @@ -73,14 +65,12 @@ namespace MediaBrowser.Controller.Providers public IReadOnlyList GetFilePaths(string path, bool clearCache) { - if (clearCache || !_filePathCache.TryGetValue(path, out List result)) + if (clearCache) { - result = _fileSystem.GetFilePaths(path).ToList(); - - _filePathCache[path] = result; + _filePathCache.TryRemove(path, out List removed); } - return result; + return _filePathCache.GetOrAdd(path, (p) => _fileSystem.GetFilePaths(path).ToList()); } } } From 449f7e1b1e425e646ee1bcef2b1464df8b01d90e Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 17:24:12 -0700 Subject: [PATCH 037/153] update based on suggestions --- MediaBrowser.Controller/Providers/DirectoryService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index c4b58918c8..53c6f9eb0b 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata[] GetFileSystemEntries(string path) { - return _cache.GetOrAdd(path, (p) => _fileSystem.GetFileSystemEntries(p).ToArray()); + return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray()); } public List GetFiles(string path) @@ -45,7 +45,7 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata GetFile(string path) { - var result = _fileCache.GetOrAdd(path, (p) => + var result = _fileCache.GetOrAdd(path, p => { var file = _fileSystem.GetFileInfo(path); return (file != null && file.Exists) ? file : null; @@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.Providers if (result == null) { // lets not store null results in the cache - _fileCache.TryRemove(path, out FileSystemMetadata removed); + _fileCache.TryRemove(path, out _); } return result; @@ -67,10 +67,10 @@ namespace MediaBrowser.Controller.Providers { if (clearCache) { - _filePathCache.TryRemove(path, out List removed); + _filePathCache.TryRemove(path, out _); } - return _filePathCache.GetOrAdd(path, (p) => _fileSystem.GetFilePaths(path).ToList()); + return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(path).ToList()); } } } From 50c9083bc03c78ae2f87ca7bd960b5c44834c944 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 17:25:08 -0700 Subject: [PATCH 038/153] remove unnecessary parentheses --- MediaBrowser.Controller/Providers/DirectoryService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 53c6f9eb0b..73731783d0 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Controller.Providers var result = _fileCache.GetOrAdd(path, p => { var file = _fileSystem.GetFileInfo(path); - return (file != null && file.Exists) ? file : null; + return file != null && file.Exists ? file : null; }); if (result == null) From eb04773c796bb5858ff37be6552f2ce088ee8f9a Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 17:34:36 -0700 Subject: [PATCH 039/153] Use the get or add argument --- MediaBrowser.Controller/Providers/DirectoryService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 73731783d0..16fd1d42b0 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Providers { var result = _fileCache.GetOrAdd(path, p => { - var file = _fileSystem.GetFileInfo(path); + var file = _fileSystem.GetFileInfo(p); return file != null && file.Exists ? file : null; }); @@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Providers _filePathCache.TryRemove(path, out _); } - return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(path).ToList()); + return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList()); } } } From 3ca9b13f99a863fee2a2b335555311cc1e9665aa Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 21:47:30 -0700 Subject: [PATCH 040/153] Check response status code before saving images --- MediaBrowser.Providers/Manager/ProviderManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index b6fb4267f4..6674ca6761 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -158,6 +158,14 @@ namespace MediaBrowser.Providers.Manager var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new HttpException($"Invalid image received ({response.StatusCode}).") + { + StatusCode = response.StatusCode + }; + } + var contentType = response.Content.Headers.ContentType.MediaType; // Workaround for tvheadend channel icons From 722ec43e258eb9e6af4c0cd09f68a4a969e50cc8 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 23:05:43 -0700 Subject: [PATCH 041/153] remove status code from exception message --- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 6674ca6761..a0c7d4ad09 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -160,7 +160,7 @@ namespace MediaBrowser.Providers.Manager if (response.StatusCode != HttpStatusCode.OK) { - throw new HttpException($"Invalid image received ({response.StatusCode}).") + throw new HttpException("Invalid image received.") { StatusCode = response.StatusCode }; From 61c1bdb6dfaef5e6c34748b495f2537c710d411b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 12:00:38 +0000 Subject: [PATCH 042/153] Bump Swashbuckle.AspNetCore.ReDoc from 5.5.1 to 5.6.3 Bumps [Swashbuckle.AspNetCore.ReDoc](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 5.5.1 to 5.6.3. - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v5.5.1...v5.6.3) Signed-off-by: dependabot[bot] --- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index c27dce8ddf..c3f43b872d 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -18,7 +18,7 @@ - + From c0afaef9856c2f55e4ab6da55f93268f61281cfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 12:00:42 +0000 Subject: [PATCH 043/153] Bump IPNetwork2 from 2.5.224 to 2.5.226 Bumps [IPNetwork2](https://github.com/lduchosal/ipnetwork) from 2.5.224 to 2.5.226. - [Release notes](https://github.com/lduchosal/ipnetwork/releases) - [Commits](https://github.com/lduchosal/ipnetwork/commits) Signed-off-by: dependabot[bot] --- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c84c7b53df..20510922ce 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,7 @@ - + From 5b6b66f7b3cdbba8258d5b408d394eee3b6e2fa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 12:00:43 +0000 Subject: [PATCH 044/153] Bump BlurHashSharp from 1.1.0 to 1.1.1 Bumps [BlurHashSharp](https://github.com/Bond-009/BlurHashSharp) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/Bond-009/BlurHashSharp/releases) - [Commits](https://github.com/Bond-009/BlurHashSharp/commits) Signed-off-by: dependabot[bot] --- Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f86b142449..08bb840f00 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -18,7 +18,7 @@ - + From 45faf5608528e3d7f805df10dbe974a46aa680ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 12:00:45 +0000 Subject: [PATCH 045/153] Bump Serilog.Sinks.Graylog from 2.1.3 to 2.2.1 Bumps [Serilog.Sinks.Graylog](https://github.com/whir1/serilog-sinks-graylog) from 2.1.3 to 2.2.1. - [Release notes](https://github.com/whir1/serilog-sinks-graylog/releases) - [Commits](https://github.com/whir1/serilog-sinks-graylog/commits) Signed-off-by: dependabot[bot] --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 648172fbf7..fa5d29a437 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -53,7 +53,7 @@ - + From 5eb54be1e3045b69e2ef6748867397b7406963ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 12:00:45 +0000 Subject: [PATCH 046/153] Bump TvDbSharper from 3.2.1 to 3.2.2 Bumps [TvDbSharper](https://github.com/HristoKolev/TvDbSharper) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/HristoKolev/TvDbSharper/releases) - [Changelog](https://github.com/HristoKolev/TvDbSharper/blob/master/CHANGELOG.md) - [Commits](https://github.com/HristoKolev/TvDbSharper/compare/v3.2.1...v3.2.2) Signed-off-by: dependabot[bot] --- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 51ca26361d..813dd441f5 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -21,7 +21,7 @@ - + From baa35cfc990ba99bf8223f2bbddf20b8f3131fcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 12:00:47 +0000 Subject: [PATCH 047/153] Bump Mono.Nat from 2.0.2 to 3.0.0 Bumps [Mono.Nat](https://github.com/mono/Mono.Nat) from 2.0.2 to 3.0.0. - [Release notes](https://github.com/mono/Mono.Nat/releases) - [Commits](https://github.com/mono/Mono.Nat/compare/release-v2.0.2...release-v3.0.0) Signed-off-by: dependabot[bot] --- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c84c7b53df..0793526271 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -36,7 +36,7 @@ - + From e84cfc688b214d1f3eaeb63ce6f50276e6ffcc06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 14:48:34 +0000 Subject: [PATCH 048/153] Bump BlurHashSharp.SkiaSharp from 1.1.0 to 1.1.1 Bumps [BlurHashSharp.SkiaSharp](https://github.com/Bond-009/BlurHashSharp) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/Bond-009/BlurHashSharp/releases) - [Commits](https://github.com/Bond-009/BlurHashSharp/commits) Signed-off-by: dependabot[bot] --- Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 08bb840f00..c11ac5fb37 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -19,7 +19,7 @@ - + From a26ff8655311dad11f80d2daccc7e2cd632d1a43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 14:50:39 +0000 Subject: [PATCH 049/153] Bump Swashbuckle.AspNetCore from 5.5.1 to 5.6.3 Bumps [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 5.5.1 to 5.6.3. - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v5.5.1...v5.6.3) Signed-off-by: dependabot[bot] --- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index c3f43b872d..6a00db4b1a 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -17,7 +17,7 @@ - + From 53d5f64e037c13c295d82fcb98b91ef2de8fc842 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Mon, 28 Sep 2020 15:04:31 -0500 Subject: [PATCH 050/153] Fix SA1513, SA1514, SA1507, and SA1508 --- .../Data/SqliteItemRepository.cs | 3 --- Emby.Server.Implementations/Dto/DtoService.cs | 1 - .../EntryPoints/UserDataChangeNotifier.cs | 1 - .../LiveTv/Listings/SchedulesDirect.cs | 8 -------- .../LiveTv/LiveTvManager.cs | 1 + .../Updates/InstallationManager.cs | 1 + .../Channels/InternalChannelFeatures.cs | 2 ++ MediaBrowser.Controller/Entities/Audio/Audio.cs | 1 - MediaBrowser.Controller/Entities/BaseItem.cs | 2 ++ MediaBrowser.Controller/Entities/Folder.cs | 1 - .../Entities/IHasMediaSources.cs | 2 -- MediaBrowser.Controller/Entities/Photo.cs | 1 - MediaBrowser.Controller/Entities/TV/Series.cs | 1 - .../Entities/UserViewBuilder.cs | 1 - MediaBrowser.Controller/IO/FileData.cs | 1 - MediaBrowser.Controller/Library/ILibraryManager.cs | 2 ++ .../Library/IMediaSourceManager.cs | 2 ++ MediaBrowser.Controller/Library/ItemResolveArgs.cs | 1 + MediaBrowser.Controller/LiveTv/ILiveTvManager.cs | 1 + MediaBrowser.Controller/LiveTv/ITunerHost.cs | 1 - .../LiveTv/LiveTvServiceStatusInfo.cs | 1 + MediaBrowser.Controller/LiveTv/ProgramInfo.cs | 7 +++++++ MediaBrowser.Controller/LiveTv/RecordingInfo.cs | 1 + MediaBrowser.Controller/LiveTv/TimerInfo.cs | 1 + .../MediaEncoding/EncodingHelper.cs | 4 +--- .../MediaEncoding/EncodingJobInfo.cs | 2 ++ .../Persistence/IItemRepository.cs | 1 + MediaBrowser.Controller/Resolvers/IItemResolver.cs | 1 + MediaBrowser.Controller/Session/SessionInfo.cs | 1 - MediaBrowser.Model/Dlna/StreamBuilder.cs | 14 +++++++++++++- MediaBrowser.Model/Dlna/StreamInfo.cs | 1 - MediaBrowser.Model/Dto/BaseItemDto.cs | 8 ++++++++ MediaBrowser.Model/Entities/MediaStream.cs | 2 ++ MediaBrowser.Model/Entities/MetadataProvider.cs | 4 ++++ MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs | 1 + MediaBrowser.Model/Providers/RemoteImageInfo.cs | 1 - MediaBrowser.Model/Providers/RemoteSearchResult.cs | 1 - MediaBrowser.Model/Querying/ItemFields.cs | 1 + .../Querying/UpcomingEpisodesQuery.cs | 3 +++ MediaBrowser.Model/Session/PlaybackProgressInfo.cs | 3 +++ MediaBrowser.Model/Sync/SyncCategory.cs | 2 ++ MediaBrowser.Model/System/SystemInfo.cs | 5 ++++- .../Tasks/IConfigurableScheduledTask.cs | 1 + .../Plugins/MusicBrainz/ArtistProvider.cs | 1 + .../MusicBrainz/MusicBrainzAlbumProvider.cs | 4 ++++ .../Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs | 1 - MediaBrowser.Providers/TV/DummySeasonProvider.cs | 1 - 47 files changed, 74 insertions(+), 33 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ab60cee618..d09f84e174 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2263,7 +2263,6 @@ namespace Emby.Server.Implementations.Data return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase); } - private static readonly HashSet _artistExcludeParentTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { "Series", @@ -3291,7 +3290,6 @@ namespace Emby.Server.Implementations.Data } } - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; var statementTexts = new List(); @@ -6006,7 +6004,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - /// /// Gets the chapter. /// diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 94cfed767b..677ad522ac 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -468,7 +468,6 @@ namespace Emby.Server.Implementations.Dto IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, Name = item.Album, Limit = 1 - }); if (parentAlbumIds.Count > 0) diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 3618b88c5d..1da717e752 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -28,7 +28,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly object _syncLock = new object(); private Timer _updateTimer; - public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager) { _userDataManager = userDataManager; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 655ff58537..28aabc1599 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -874,7 +874,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List lineups { get; set; } } - public class Headends { public string headend { get; set; } @@ -886,8 +885,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List lineups { get; set; } } - - public class Map { public string stationID { get; set; } @@ -971,9 +968,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List date { get; set; } } - - - public class Rating { public string body { get; set; } @@ -1017,8 +1011,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public string isPremiereOrFinale { get; set; } } - - public class MetadataSchedule { public string modified { get; set; } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index a898a564fa..5bdd1c23c4 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -2135,6 +2135,7 @@ namespace Emby.Server.Implementations.LiveTv } private bool _disposed = false; + /// /// Releases unmanaged and - optionally - managed resources. /// diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 8e24bf55ce..8a6181be6b 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -393,6 +393,7 @@ namespace Emby.Server.Implementations.Updates // Ignore any exceptions. } } + stream.Position = 0; _zipClient.ExtractAllFromZip(stream, targetDir, true); diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs index 1074ce435e..137f5d095b 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -42,6 +42,7 @@ namespace MediaBrowser.Controller.Channels /// Indicates if a sort ascending/descending toggle is supported or not. /// public bool SupportsSortOrderToggle { get; set; } + /// /// Gets or sets the automatic refresh levels. /// @@ -53,6 +54,7 @@ namespace MediaBrowser.Controller.Channels /// /// The daily download limit. public int? DailyDownloadLimit { get; set; } + /// /// Gets or sets a value indicating whether [supports downloading]. /// diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 2c6dea02cd..8220464b39 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -90,7 +90,6 @@ namespace MediaBrowser.Controller.Entities.Audio var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty; - if (ParentIndexNumber.HasValue) { songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 68126bd8a8..2fc7d45c90 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -197,6 +197,7 @@ namespace MediaBrowser.Controller.Entities public virtual bool SupportsRemoteImageDownloading => true; private string _name; + /// /// Gets or sets the name. /// @@ -661,6 +662,7 @@ namespace MediaBrowser.Controller.Entities } private string _forcedSortName; + /// /// Gets or sets the name of the forced sort. /// diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 11542c1cad..901ea875bc 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1386,7 +1386,6 @@ namespace MediaBrowser.Controller.Entities } } - /// /// Gets the linked children. /// diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index a7b60d1688..0f612262a8 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -21,7 +21,5 @@ namespace MediaBrowser.Controller.Entities List GetMediaSources(bool enablePathSubstitution); List GetMediaStreams(); - - } } diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 1485d4c792..2fc66176f6 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -16,7 +16,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; - [JsonIgnore] public PhotoAlbum AlbumEntity { diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 72c696c1ae..75a746bfb3 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -450,7 +450,6 @@ namespace MediaBrowser.Controller.Entities.TV }); } - protected override bool GetBlockUnratedValue(User user) { return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString()); diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index b384b27d1a..068a767698 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -258,7 +258,6 @@ namespace MediaBrowser.Controller.Entities IncludeItemTypes = new[] { typeof(Movie).Name }, Recursive = true, EnableTotalRecordCount = false - }).Items .SelectMany(i => i.Genres) .DistinctNames() diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 9bc4cac39d..3db60ae0bb 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -111,5 +111,4 @@ namespace MediaBrowser.Controller.IO return returnResult; } } - } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 804170d5c9..332730bcca 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -77,6 +77,7 @@ namespace MediaBrowser.Controller.Library MusicArtist GetArtist(string name); MusicArtist GetArtist(string name, DtoOptions options); + /// /// Gets a Studio. /// @@ -234,6 +235,7 @@ namespace MediaBrowser.Controller.Library /// Occurs when [item updated]. /// event EventHandler ItemUpdated; + /// /// Occurs when [item removed]. /// diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 9e7b1e6085..22bf9488f7 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -28,12 +28,14 @@ namespace MediaBrowser.Controller.Library /// The item identifier. /// IEnumerable<MediaStream>. List GetMediaStreams(Guid itemId); + /// /// Gets the media streams. /// /// The media source identifier. /// IEnumerable<MediaStream>. List GetMediaStreams(string mediaSourceId); + /// /// Gets the media streams. /// diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 6a0dbeba2f..12a311dc33 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -156,6 +156,7 @@ namespace MediaBrowser.Controller.Library } // REVIEW: @bond + /// /// Gets the physical locations. /// diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 55c3309317..6c365caa46 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -231,6 +231,7 @@ namespace MediaBrowser.Controller.LiveTv /// Saves the tuner host. /// Task SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); + /// /// Saves the listing provider. /// diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index ff92bf856c..abca8f2390 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -56,7 +56,6 @@ namespace MediaBrowser.Controller.LiveTv Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken); Task> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken); - } public interface IConfigurableTunerHost diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs index 02178297b1..b629749049 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs @@ -42,6 +42,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// The tuners. public List Tuners { get; set; } + /// /// Gets or sets a value indicating whether this instance is visible. /// diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index bdcffd5cae..f9f559ee96 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -35,6 +35,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// The overview. public string Overview { get; set; } + /// /// Gets or sets the short overview. /// @@ -169,31 +170,37 @@ namespace MediaBrowser.Controller.LiveTv /// /// The production year. public int? ProductionYear { get; set; } + /// /// Gets or sets the home page URL. /// /// The home page URL. public string HomePageUrl { get; set; } + /// /// Gets or sets the series identifier. /// /// The series identifier. public string SeriesId { get; set; } + /// /// Gets or sets the show identifier. /// /// The show identifier. public string ShowId { get; set; } + /// /// Gets or sets the season number. /// /// The season number. public int? SeasonNumber { get; set; } + /// /// Gets or sets the episode number. /// /// The episode number. public int? EpisodeNumber { get; set; } + /// /// Gets or sets the etag. /// diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 303882b7ef..69190694ff 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -187,6 +187,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// null if [has image] contains no value, true if [has image]; otherwise, false. public bool? HasImage { get; set; } + /// /// Gets or sets the show identifier. /// diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index bcef4666d2..aa5170617d 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -113,6 +113,7 @@ namespace MediaBrowser.Controller.LiveTv // Program properties public int? SeasonNumber { get; set; } + /// /// Gets or sets the episode number. /// diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2c30ca4588..c5529ad5bd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -109,7 +109,6 @@ namespace MediaBrowser.Controller.MediaEncoding } return _mediaEncoder.SupportsHwaccel("vaapi"); - } /// @@ -508,6 +507,7 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append("-hwaccel qsv "); } } + // While using SW decoder else { @@ -1441,7 +1441,6 @@ namespace MediaBrowser.Controller.MediaEncoding var codec = outputAudioCodec ?? string.Empty; - int? transcoderChannelLimit; if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) { @@ -2511,7 +2510,6 @@ namespace MediaBrowser.Controller.MediaEncoding return inputModifier; } - public void AttachMediaSourceInfo( EncodingJobInfo state, MediaSourceInfo mediaSource, diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 68bc502a0f..c7ec878d2d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -697,10 +697,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// The progressive. /// Progressive, + /// /// The HLS. /// Hls, + /// /// The dash. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index ebc37bd1f3..45c6805f0e 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -100,6 +100,7 @@ namespace MediaBrowser.Controller.Persistence /// The query. /// IEnumerable<Guid>. QueryResult GetItemIds(InternalItemsQuery query); + /// /// Gets the items. /// diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index b99c468435..eb7fb793a4 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -19,6 +19,7 @@ namespace MediaBrowser.Controller.Resolvers /// The args. /// BaseItem. BaseItem ResolvePath(ItemResolveArgs args); + /// /// Gets the priority. /// diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 054fd33d9d..55e44c19db 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -22,7 +22,6 @@ namespace MediaBrowser.Controller.Session private readonly ISessionManager _sessionManager; private readonly ILogger _logger; - private readonly object _progressLock = new object(); private Timer _progressTimer; private PlaybackProgressInfo _lastProgressInfo; diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index cfe862f5a9..d9e7e4fbb4 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -498,7 +498,6 @@ namespace MediaBrowser.Model.Dlna } } - if (playMethods.Count > 0) { transcodeReasons.Clear(); @@ -1431,6 +1430,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.AudioChannels: { if (string.IsNullOrEmpty(qualifier)) @@ -1466,6 +1466,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.IsAvc: { if (!enableNonQualifiedConditions) @@ -1487,6 +1488,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.IsAnamorphic: { if (!enableNonQualifiedConditions) @@ -1508,6 +1510,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.IsInterlaced: { if (string.IsNullOrEmpty(qualifier)) @@ -1539,6 +1542,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.AudioProfile: case ProfileConditionValue.Has64BitOffsets: case ProfileConditionValue.PacketLength: @@ -1550,6 +1554,7 @@ namespace MediaBrowser.Model.Dlna // Not supported yet break; } + case ProfileConditionValue.RefFrames: { if (string.IsNullOrEmpty(qualifier)) @@ -1585,6 +1590,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.VideoBitDepth: { if (string.IsNullOrEmpty(qualifier)) @@ -1620,6 +1626,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.VideoProfile: { if (string.IsNullOrEmpty(qualifier)) @@ -1643,6 +1650,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.Height: { if (!enableNonQualifiedConditions) @@ -1668,6 +1676,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.VideoBitrate: { if (!enableNonQualifiedConditions) @@ -1693,6 +1702,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.VideoFramerate: { if (!enableNonQualifiedConditions) @@ -1718,6 +1728,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.VideoLevel: { if (string.IsNullOrEmpty(qualifier)) @@ -1743,6 +1754,7 @@ namespace MediaBrowser.Model.Dlna break; } + case ProfileConditionValue.Width: { if (!enableNonQualifiedConditions) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 94d53ab70e..9399d21f16 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -276,7 +276,6 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - if (!item.IsDirectStream) { if (item.RequireNonAnamorphic) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index af3d83adec..fac7541773 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -429,6 +429,7 @@ namespace MediaBrowser.Model.Dto /// /// The album id. public Guid AlbumId { get; set; } + /// /// Gets or sets the album image tag. /// @@ -599,11 +600,13 @@ namespace MediaBrowser.Model.Dto /// /// The trailer count. public int? TrailerCount { get; set; } + /// /// Gets or sets the movie count. /// /// The movie count. public int? MovieCount { get; set; } + /// /// Gets or sets the series count. /// @@ -611,16 +614,19 @@ namespace MediaBrowser.Model.Dto public int? SeriesCount { get; set; } public int? ProgramCount { get; set; } + /// /// Gets or sets the episode count. /// /// The episode count. public int? EpisodeCount { get; set; } + /// /// Gets or sets the song count. /// /// The song count. public int? SongCount { get; set; } + /// /// Gets or sets the album count. /// @@ -628,6 +634,7 @@ namespace MediaBrowser.Model.Dto public int? AlbumCount { get; set; } public int? ArtistCount { get; set; } + /// /// Gets or sets the music video count. /// @@ -768,6 +775,7 @@ namespace MediaBrowser.Model.Dto /// /// The timer identifier. public string TimerId { get; set; } + /// /// Gets or sets the current program. /// diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 2d37618c27..fa3c9aaa2f 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -451,11 +451,13 @@ namespace MediaBrowser.Model.Entities /// /// The method. public SubtitleDeliveryMethod? DeliveryMethod { get; set; } + /// /// Gets or sets the delivery URL. /// /// The delivery URL. public string DeliveryUrl { get; set; } + /// /// Gets or sets a value indicating whether this instance is external URL. /// diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs index 7fecf67b8e..e9c098021d 100644 --- a/MediaBrowser.Model/Entities/MetadataProvider.cs +++ b/MediaBrowser.Model/Entities/MetadataProvider.cs @@ -11,18 +11,22 @@ namespace MediaBrowser.Model.Entities /// The imdb. /// Imdb = 2, + /// /// The TMDB. /// Tmdb = 3, + /// /// The TVDB. /// Tvdb = 4, + /// /// The tvcom. /// Tvcom = 5, + /// /// Tmdb Collection Id. /// diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index ab74aff28b..bcba344cc5 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -84,6 +84,7 @@ namespace MediaBrowser.Model.LiveTv /// /// null if [is kids] contains no value, true if [is kids]; otherwise, false. public bool? IsKids { get; set; } + /// /// Gets or sets a value indicating whether this instance is sports. /// diff --git a/MediaBrowser.Model/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs index 78ab6c706c..fb25999e0a 100644 --- a/MediaBrowser.Model/Providers/RemoteImageInfo.cs +++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs @@ -68,5 +68,4 @@ namespace MediaBrowser.Model.Providers /// The type of the rating. public RatingType RatingType { get; set; } } - } diff --git a/MediaBrowser.Model/Providers/RemoteSearchResult.cs b/MediaBrowser.Model/Providers/RemoteSearchResult.cs index 989741c01a..a29e7ad1c5 100644 --- a/MediaBrowser.Model/Providers/RemoteSearchResult.cs +++ b/MediaBrowser.Model/Providers/RemoteSearchResult.cs @@ -50,6 +50,5 @@ namespace MediaBrowser.Model.Providers public RemoteSearchResult AlbumArtist { get; set; } public RemoteSearchResult[] Artists { get; set; } - } } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 731d22aaf8..ef4698f3f9 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -168,6 +168,7 @@ namespace MediaBrowser.Model.Querying Studios, BasicSyncInfo, + /// /// The synchronize information. /// diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs index 2ef6f7c60b..eb62394605 100644 --- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs +++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs @@ -37,16 +37,19 @@ namespace MediaBrowser.Model.Querying /// /// The fields. public ItemFields[] Fields { get; set; } + /// /// Gets or sets a value indicating whether [enable images]. /// /// null if [enable images] contains no value, true if [enable images]; otherwise, false. public bool? EnableImages { get; set; } + /// /// Gets or sets the image type limit. /// /// The image type limit. public int? ImageTypeLimit { get; set; } + /// /// Gets or sets the enable image types. /// diff --git a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs index 21bcabf1d9..73dbe6a2da 100644 --- a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs +++ b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs @@ -88,16 +88,19 @@ namespace MediaBrowser.Model.Session /// /// The play method. public PlayMethod PlayMethod { get; set; } + /// /// Gets or sets the live stream identifier. /// /// The live stream identifier. public string LiveStreamId { get; set; } + /// /// Gets or sets the play session identifier. /// /// The play session identifier. public string PlaySessionId { get; set; } + /// /// Gets or sets the repeat mode. /// diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs index 80ad5f56ec..1248c2f739 100644 --- a/MediaBrowser.Model/Sync/SyncCategory.cs +++ b/MediaBrowser.Model/Sync/SyncCategory.cs @@ -8,10 +8,12 @@ namespace MediaBrowser.Model.Sync /// The latest. /// Latest = 0, + /// /// The next up. /// NextUp = 1, + /// /// The resume. /// diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 18ca74ee30..4b83fb7e61 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -14,13 +14,16 @@ namespace MediaBrowser.Model.System { /// No path to FFmpeg found. NotFound, + /// Path supplied via command line using switch --ffmpeg. SetByArgument, + /// User has supplied path via Transcoding UI page. Custom, + /// FFmpeg tool found on system $PATH. System - }; + } /// /// Class SystemInfo. diff --git a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs index fbfaed22ef..6212d76f78 100644 --- a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs +++ b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs @@ -9,6 +9,7 @@ namespace MediaBrowser.Model.Tasks /// /// true if this instance is hidden; otherwise, false. bool IsHidden { get; } + /// /// Gets a value indicating whether this instance is enabled. /// diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index 781b716406..f27da7ce61 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -198,6 +198,7 @@ namespace MediaBrowser.Providers.Music result.Name = reader.ReadElementContentAsString(); break; } + case "annotation": { result.Overview = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 46f8988f27..abfa1c6e71 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -444,6 +444,7 @@ namespace MediaBrowser.Providers.Music result.Title = reader.ReadElementContentAsString(); break; } + case "date": { var val = reader.ReadElementContentAsString(); @@ -454,17 +455,20 @@ namespace MediaBrowser.Providers.Music break; } + case "annotation": { result.Overview = reader.ReadElementContentAsString(); break; } + case "release-group": { result.ReleaseGroupId = reader.GetAttribute("id"); reader.Skip(); break; } + case "artist-credit": { using (var subReader = reader.ReadSubtree()) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs index 01a887eed5..3c626f9ebf 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs @@ -302,7 +302,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source), Name = i.Name - }).ToArray(); } } diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs index a0f7f6cfd7..905cbefd32 100644 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ b/MediaBrowser.Providers/TV/DummySeasonProvider.cs @@ -217,7 +217,6 @@ namespace MediaBrowser.Providers.TV new DeleteOptions { DeleteFileLocation = true - }, false); From c912093579952e3cb7f4b4563975024c9e27a097 Mon Sep 17 00:00:00 2001 From: spookbits <71300703+spooksbit@users.noreply.github.com> Date: Fri, 25 Sep 2020 21:52:37 -0400 Subject: [PATCH 051/153] Created a separate API Docs profile to launch the browser at the API docs, and the nowebclient profile no longer launches the browser at all. Don't point to web in the client because it won't redirect properly. Modified the vscode launch.json to automatically launch the browser when debugging the first configuration. The --- .vscode/launch.json | 6 +++++- Jellyfin.Server/Properties/launchSettings.json | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index bf1bd65cbe..05f60cfa69 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,11 @@ "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart" + "internalConsoleOptions": "openOnSessionStart", + "serverReadyAction": { + "action": "openExternally", + "pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'", + } }, { "name": ".NET Core Launch (nowebclient)", diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json index d8be520b7b..20d432afc4 100644 --- a/Jellyfin.Server/Properties/launchSettings.json +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -3,13 +3,19 @@ "Jellyfin.Server": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "web", "applicationUrl": "http://localhost:8096", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Jellyfin.Server (nowebclient)": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "commandLineArgs": "--nowebclient" + }, + "Jellyfin.Server (API Docs)": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api-docs/swagger", From e9911b70ddfd9a9ee1f9b89579f5dbec154129a4 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Mon, 28 Sep 2020 15:37:10 -0700 Subject: [PATCH 052/153] Use EnsureSuccessStatusCode --- MediaBrowser.Providers/Manager/ProviderManager.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index a0c7d4ad09..40fcf7d6f4 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -157,14 +157,7 @@ namespace MediaBrowser.Providers.Manager { var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); - - if (response.StatusCode != HttpStatusCode.OK) - { - throw new HttpException("Invalid image received.") - { - StatusCode = response.StatusCode - }; - } + response.EnsureSuccessStatusCode(); var contentType = response.Content.Headers.ContentType.MediaType; From fd7a36932da16fb1e85160aaa0da792303a4e72f Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Mon, 28 Sep 2020 15:31:28 -0700 Subject: [PATCH 053/153] simplify EndDate query --- .../Plugins/TheTvdb/TvdbSeriesProvider.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index cdb159c0f0..b34e52235a 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -352,14 +352,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb }; var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false); - var episodeDates = episodesPage.Data + result.Item.EndDate = episodesPage.Data .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null) - .Where(dt => dt.HasValue) - .ToList(); - if (episodeDates.Count != 0) - { - result.Item.EndDate = episodeDates.Max(); - } + .Max(); } } catch (TvDbServerException e) From 25d8d85740f984f051cebc5db53466c17688c370 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Tue, 29 Sep 2020 01:19:12 -0700 Subject: [PATCH 054/153] Back to HttpException --- MediaBrowser.Providers/Manager/ProviderManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 40fcf7d6f4..a0c7d4ad09 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -157,7 +157,14 @@ namespace MediaBrowser.Providers.Manager { var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new HttpException("Invalid image received.") + { + StatusCode = response.StatusCode + }; + } var contentType = response.Content.Headers.ContentType.MediaType; From 3b0c6c660d057336e26aa67cb36abaaccb00771d Mon Sep 17 00:00:00 2001 From: Justin LeCheminant Date: Wed, 30 Sep 2020 20:26:59 -0600 Subject: [PATCH 055/153] Removing string we don't use anymore. --- Emby.Server.Implementations/Localization/Core/af.json | 1 - Emby.Server.Implementations/Localization/Core/ar.json | 1 - Emby.Server.Implementations/Localization/Core/bg-BG.json | 1 - Emby.Server.Implementations/Localization/Core/bn.json | 1 - Emby.Server.Implementations/Localization/Core/ca.json | 1 - Emby.Server.Implementations/Localization/Core/cs.json | 1 - Emby.Server.Implementations/Localization/Core/da.json | 1 - Emby.Server.Implementations/Localization/Core/de.json | 1 - Emby.Server.Implementations/Localization/Core/el.json | 1 - Emby.Server.Implementations/Localization/Core/en-GB.json | 1 - Emby.Server.Implementations/Localization/Core/en-US.json | 1 - Emby.Server.Implementations/Localization/Core/es-AR.json | 1 - Emby.Server.Implementations/Localization/Core/es-MX.json | 1 - Emby.Server.Implementations/Localization/Core/es.json | 1 - Emby.Server.Implementations/Localization/Core/es_419.json | 1 - Emby.Server.Implementations/Localization/Core/es_DO.json | 1 - Emby.Server.Implementations/Localization/Core/fa.json | 1 - Emby.Server.Implementations/Localization/Core/fi.json | 1 - Emby.Server.Implementations/Localization/Core/fil.json | 1 - Emby.Server.Implementations/Localization/Core/fr-CA.json | 1 - Emby.Server.Implementations/Localization/Core/fr.json | 1 - Emby.Server.Implementations/Localization/Core/gsw.json | 1 - Emby.Server.Implementations/Localization/Core/he.json | 1 - Emby.Server.Implementations/Localization/Core/hr.json | 1 - Emby.Server.Implementations/Localization/Core/hu.json | 1 - Emby.Server.Implementations/Localization/Core/id.json | 1 - Emby.Server.Implementations/Localization/Core/is.json | 1 - Emby.Server.Implementations/Localization/Core/it.json | 1 - Emby.Server.Implementations/Localization/Core/ja.json | 1 - Emby.Server.Implementations/Localization/Core/kk.json | 1 - Emby.Server.Implementations/Localization/Core/ko.json | 1 - Emby.Server.Implementations/Localization/Core/lt-LT.json | 1 - Emby.Server.Implementations/Localization/Core/lv.json | 1 - Emby.Server.Implementations/Localization/Core/mk.json | 1 - Emby.Server.Implementations/Localization/Core/mr.json | 1 - Emby.Server.Implementations/Localization/Core/ms.json | 1 - Emby.Server.Implementations/Localization/Core/nb.json | 1 - Emby.Server.Implementations/Localization/Core/ne.json | 1 - Emby.Server.Implementations/Localization/Core/nl.json | 1 - Emby.Server.Implementations/Localization/Core/nn.json | 1 - Emby.Server.Implementations/Localization/Core/pl.json | 1 - Emby.Server.Implementations/Localization/Core/pt-BR.json | 1 - Emby.Server.Implementations/Localization/Core/pt-PT.json | 1 - Emby.Server.Implementations/Localization/Core/pt.json | 1 - Emby.Server.Implementations/Localization/Core/ro.json | 1 - Emby.Server.Implementations/Localization/Core/ru.json | 1 - Emby.Server.Implementations/Localization/Core/sk.json | 1 - Emby.Server.Implementations/Localization/Core/sl-SI.json | 1 - Emby.Server.Implementations/Localization/Core/sq.json | 1 - Emby.Server.Implementations/Localization/Core/sr.json | 1 - Emby.Server.Implementations/Localization/Core/sv.json | 1 - Emby.Server.Implementations/Localization/Core/ta.json | 1 - Emby.Server.Implementations/Localization/Core/th.json | 1 - Emby.Server.Implementations/Localization/Core/tr.json | 1 - Emby.Server.Implementations/Localization/Core/uk.json | 1 - Emby.Server.Implementations/Localization/Core/ur_PK.json | 1 - Emby.Server.Implementations/Localization/Core/vi.json | 1 - Emby.Server.Implementations/Localization/Core/zh-CN.json | 1 - Emby.Server.Implementations/Localization/Core/zh-HK.json | 1 - Emby.Server.Implementations/Localization/Core/zh-TW.json | 1 - 60 files changed, 60 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index d33e118931..977a1c2d70 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -85,7 +85,6 @@ "ItemAddedWithName": "{0} is in die versameling", "HomeVideos": "Tuis opnames", "HeaderRecordingGroups": "Groep Opnames", - "HeaderCameraUploads": "Kamera Oplaai", "Genres": "Genres", "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}", "ChapterNameValue": "Hoofstuk", diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 4eac8e75de..4b898e6fe0 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -16,7 +16,6 @@ "Folders": "المجلدات", "Genres": "التضنيفات", "HeaderAlbumArtists": "فناني الألبومات", - "HeaderCameraUploads": "تحميلات الكاميرا", "HeaderContinueWatching": "استئناف", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 3fc7c7dc0f..1fed832768 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -16,7 +16,6 @@ "Folders": "Папки", "Genres": "Жанрове", "HeaderAlbumArtists": "Изпълнители на албуми", - "HeaderCameraUploads": "Качени от камера", "HeaderContinueWatching": "Продължаване на гледането", "HeaderFavoriteAlbums": "Любими албуми", "HeaderFavoriteArtists": "Любими изпълнители", diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 1bd1909827..5667bf337e 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -14,7 +14,6 @@ "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", - "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ", "HeaderAlbumArtists": "এলবাম শিল্পী", "Genres": "জেনার", "Folders": "ফোল্ডারগুলো", diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 2c802a39ef..b7852eccb3 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -16,7 +16,6 @@ "Folders": "Carpetes", "Genres": "Gèneres", "HeaderAlbumArtists": "Artistes del Àlbum", - "HeaderCameraUploads": "Pujades de Càmera", "HeaderContinueWatching": "Continua Veient", "HeaderFavoriteAlbums": "Àlbums Preferits", "HeaderFavoriteArtists": "Artistes Preferits", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 464ca28ca0..b34fad066a 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -16,7 +16,6 @@ "Folders": "Složky", "Genres": "Žánry", "HeaderAlbumArtists": "Umělci alba", - "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovat ve sledování", "HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteArtists": "Oblíbení interpreti", diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index f5397b62cd..b29ad94eff 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -16,7 +16,6 @@ "Folders": "Mapper", "Genres": "Genrer", "HeaderAlbumArtists": "Albumkunstnere", - "HeaderCameraUploads": "Kamera Uploads", "HeaderContinueWatching": "Fortsæt Afspilning", "HeaderFavoriteAlbums": "Favoritalbummer", "HeaderFavoriteArtists": "Favoritkunstnere", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index fcbe9566e2..97ad1694a6 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -16,7 +16,6 @@ "Folders": "Verzeichnisse", "Genres": "Genres", "HeaderAlbumArtists": "Album-Interpreten", - "HeaderCameraUploads": "Kamera-Uploads", "HeaderContinueWatching": "Fortsetzen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Interpreten", diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 0753ea39d4..c45cc11cb8 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -16,7 +16,6 @@ "Folders": "Φάκελοι", "Genres": "Είδη", "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ", - "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας", "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 544c38cfa9..57ff13219f 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -16,7 +16,6 @@ "Folders": "Folders", "Genres": "Genres", "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", "HeaderContinueWatching": "Continue Watching", "HeaderFavoriteAlbums": "Favourite Albums", "HeaderFavoriteArtists": "Favourite Artists", diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 97a843160e..92c54fb0e6 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -16,7 +16,6 @@ "Folders": "Folders", "Genres": "Genres", "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", "HeaderContinueWatching": "Continue Watching", "HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteArtists": "Favorite Artists", diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index ac96c788c1..390074cdd7 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -16,7 +16,6 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas de álbum", - "HeaderCameraUploads": "Subidas de cámara", "HeaderContinueWatching": "Seguir viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 4ba324aa1b..ab54c0ea6a 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -16,7 +16,6 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidas desde la cámara", "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index e7bd3959bf..d4e0b299dd 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -16,7 +16,6 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidas desde la cámara", "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index 0959ef2ca0..dcd30694f0 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -105,7 +105,6 @@ "Inherit": "Heredar", "HomeVideos": "Videos caseros", "HeaderRecordingGroups": "Grupos de grabación", - "HeaderCameraUploads": "Subidas desde la cámara", "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", "DeviceOnlineWithName": "{0} está conectado", "DeviceOfflineWithName": "{0} se ha desconectado", diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 26732eb3f1..b64ffbfbba 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -12,7 +12,6 @@ "Application": "Aplicación", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "HeaderContinueWatching": "Continuar Viendo", - "HeaderCameraUploads": "Subidas de Cámara", "HeaderAlbumArtists": "Artistas del Álbum", "Genres": "Géneros", "Folders": "Carpetas", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 500c292170..1986decf03 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -16,7 +16,6 @@ "Folders": "پوشه‌ها", "Genres": "ژانرها", "HeaderAlbumArtists": "هنرمندان آلبوم", - "HeaderCameraUploads": "آپلودهای دوربین", "HeaderContinueWatching": "ادامه تماشا", "HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه", "HeaderFavoriteArtists": "هنرمندان مورد علاقه", diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index f8d6e0e09b..aae22583ab 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -24,7 +24,6 @@ "HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteEpisodes": "Lempijaksot", - "HeaderCameraUploads": "Kamerasta Lähetetyt", "HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteAlbums": "Lempialbumit", "HeaderContinueWatching": "Jatka katsomista", diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 47daf20442..1a3e188327 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -73,7 +73,6 @@ "HeaderFavoriteArtists": "Paboritong Artista", "HeaderFavoriteAlbums": "Paboritong Albums", "HeaderContinueWatching": "Ituloy Manood", - "HeaderCameraUploads": "Camera Uploads", "HeaderAlbumArtists": "Artista ng Album", "Genres": "Kategorya", "Folders": "Folders", diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index cd1c8144fd..3d7592e3c8 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -16,7 +16,6 @@ "Folders": "Dossiers", "Genres": "Genres", "HeaderAlbumArtists": "Artistes de l'album", - "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes favoris", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 7fc9968219..f4ca8575a1 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -16,7 +16,6 @@ "Folders": "Dossiers", "Genres": "Genres", "HeaderAlbumArtists": "Artistes", - "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes préférés", diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index 8780a884ba..ee1f8775e5 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -16,7 +16,6 @@ "Folders": "Ordner", "Genres": "Genres", "HeaderAlbumArtists": "Album-Künstler", - "HeaderCameraUploads": "Kamera-Uploads", "HeaderContinueWatching": "weiter schauen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Künstler", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index dc3a981540..f906d6e117 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -16,7 +16,6 @@ "Folders": "תיקיות", "Genres": "ז'אנרים", "HeaderAlbumArtists": "אמני האלבום", - "HeaderCameraUploads": "העלאות ממצלמה", "HeaderContinueWatching": "המשך לצפות", "HeaderFavoriteAlbums": "אלבומים מועדפים", "HeaderFavoriteArtists": "אמנים מועדפים", diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 97c77017b5..a3c936240d 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -16,7 +16,6 @@ "Folders": "Mape", "Genres": "Žanrovi", "HeaderAlbumArtists": "Izvođači na albumu", - "HeaderCameraUploads": "Uvoz sa kamere", "HeaderContinueWatching": "Nastavi gledati", "HeaderFavoriteAlbums": "Omiljeni albumi", "HeaderFavoriteArtists": "Omiljeni izvođači", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index c5c3844e3f..343d213d41 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -16,7 +16,6 @@ "Folders": "Könyvtárak", "Genres": "Műfajok", "HeaderAlbumArtists": "Album előadók", - "HeaderCameraUploads": "Kamera feltöltések", "HeaderContinueWatching": "Megtekintés folytatása", "HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteArtists": "Kedvenc előadók", diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 585fc6f027..ef3ed2580a 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -20,7 +20,6 @@ "HeaderFavoriteArtists": "Artis Favorit", "HeaderFavoriteAlbums": "Album Favorit", "HeaderContinueWatching": "Lanjut Menonton", - "HeaderCameraUploads": "Unggahan Kamera", "HeaderAlbumArtists": "Album Artis", "Genres": "Aliran", "Folders": "Folder", diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 0f0f9130b0..0f769eaada 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -13,7 +13,6 @@ "HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderContinueWatching": "Halda áfram að horfa", - "HeaderCameraUploads": "Myndavéla upphal", "HeaderAlbumArtists": "Höfundur plötu", "Genres": "Tegundir", "Folders": "Möppur", diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index bf1a0ef136..0a6238578d 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -16,7 +16,6 @@ "Folders": "Cartelle", "Genres": "Generi", "HeaderAlbumArtists": "Artisti degli Album", - "HeaderCameraUploads": "Caricamenti Fotocamera", "HeaderContinueWatching": "Continua a guardare", "HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti", diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index a4d9f9ef6b..35004f0eb4 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -16,7 +16,6 @@ "Folders": "フォルダー", "Genres": "ジャンル", "HeaderAlbumArtists": "アルバムアーティスト", - "HeaderCameraUploads": "カメラアップロード", "HeaderContinueWatching": "視聴を続ける", "HeaderFavoriteAlbums": "お気に入りのアルバム", "HeaderFavoriteArtists": "お気に入りのアーティスト", diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 5618ff4a8f..91c1fb15bc 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -16,7 +16,6 @@ "Folders": "Qaltalar", "Genres": "Janrlar", "HeaderAlbumArtists": "Álbom oryndaýshylary", - "HeaderCameraUploads": "Kameradan júktelgender", "HeaderContinueWatching": "Qaraýdy jalǵastyrý", "HeaderFavoriteAlbums": "Tańdaýly álbomdar", "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar", diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index a33953c273..fb01e46451 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -16,7 +16,6 @@ "Folders": "폴더", "Genres": "장르", "HeaderAlbumArtists": "앨범 아티스트", - "HeaderCameraUploads": "카메라 업로드", "HeaderContinueWatching": "계속 시청하기", "HeaderFavoriteAlbums": "즐겨찾는 앨범", "HeaderFavoriteArtists": "즐겨찾는 아티스트", diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 35053766b4..d4cb592efc 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -16,7 +16,6 @@ "Folders": "Katalogai", "Genres": "Žanrai", "HeaderAlbumArtists": "Albumo atlikėjai", - "HeaderCameraUploads": "Kameros", "HeaderContinueWatching": "Žiūrėti toliau", "HeaderFavoriteAlbums": "Mėgstami Albumai", "HeaderFavoriteArtists": "Mėgstami Atlikėjai", diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index dbcf172871..5e3acfbe96 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -72,7 +72,6 @@ "ItemAddedWithName": "{0} tika pievienots bibliotēkai", "HeaderLiveTV": "Tiešraides TV", "HeaderContinueWatching": "Turpināt Skatīšanos", - "HeaderCameraUploads": "Kameras augšupielādes", "HeaderAlbumArtists": "Albumu Izpildītāji", "Genres": "Žanri", "Folders": "Mapes", diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index bbdf99abab..b780ef498a 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -51,7 +51,6 @@ "HeaderFavoriteArtists": "Омилени Изведувачи", "HeaderFavoriteAlbums": "Омилени Албуми", "HeaderContinueWatching": "Продолжи со гледање", - "HeaderCameraUploads": "Поставувања од камера", "HeaderAlbumArtists": "Изведувачи од Албуми", "Genres": "Жанрови", "Folders": "Папки", diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index b6db2b0f29..fdb4171b53 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -54,7 +54,6 @@ "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले", "HomeVideos": "घरचे व्हिडीयो", "HeaderRecordingGroups": "रेकॉर्डिंग गट", - "HeaderCameraUploads": "कॅमेरा अपलोड", "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे", "Application": "अ‍ॅप्लिकेशन", "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}", diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 7f8df12895..5e3d095ff1 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -16,7 +16,6 @@ "Folders": "Fail-fail", "Genres": "Genre-genre", "HeaderAlbumArtists": "Album Artis-artis", - "HeaderCameraUploads": "Muatnaik Kamera", "HeaderContinueWatching": "Terus Menonton", "HeaderFavoriteAlbums": "Album-album Kegemaran", "HeaderFavoriteArtists": "Artis-artis Kegemaran", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 07a5991211..245c3cd636 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -16,7 +16,6 @@ "Folders": "Mapper", "Genres": "Sjangre", "HeaderAlbumArtists": "Albumartister", - "HeaderCameraUploads": "Kameraopplastinger", "HeaderContinueWatching": "Fortsett å se", "HeaderFavoriteAlbums": "Favorittalbum", "HeaderFavoriteArtists": "Favorittartister", diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json index 38c0737098..8e820d40c7 100644 --- a/Emby.Server.Implementations/Localization/Core/ne.json +++ b/Emby.Server.Implementations/Localization/Core/ne.json @@ -41,7 +41,6 @@ "HeaderFavoriteArtists": "मनपर्ने कलाकारहरू", "HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू", "HeaderContinueWatching": "हेर्न जारी राख्नुहोस्", - "HeaderCameraUploads": "क्यामेरा अपलोडहरू", "HeaderAlbumArtists": "एल्बमका कलाकारहरू", "Genres": "विधाहरू", "Folders": "फोल्डरहरू", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 41c74d54de..e102b92b90 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -16,7 +16,6 @@ "Folders": "Mappen", "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", - "HeaderCameraUploads": "Camera-uploads", "HeaderContinueWatching": "Kijken hervatten", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index fb6e81beb3..6236515b2c 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -19,7 +19,6 @@ "HeaderFavoriteArtists": "Favoritt Artistar", "HeaderFavoriteAlbums": "Favoritt Album", "HeaderContinueWatching": "Fortsett å sjå", - "HeaderCameraUploads": "Kamera Opplastingar", "HeaderAlbumArtists": "Album Artist", "Genres": "Sjangrar", "Folders": "Mapper", diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index bdc0d0169b..003e591b37 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -16,7 +16,6 @@ "Folders": "Foldery", "Genres": "Gatunki", "HeaderAlbumArtists": "Wykonawcy albumów", - "HeaderCameraUploads": "Przekazane obrazy", "HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderFavoriteAlbums": "Ulubione albumy", "HeaderFavoriteArtists": "Ulubieni wykonawcy", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 275195640b..5e49ca702e 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -16,7 +16,6 @@ "Folders": "Pastas", "Genres": "Gêneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderCameraUploads": "Envios da Câmera", "HeaderContinueWatching": "Continuar Assistindo", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index c1fb65743d..8aa1ed5497 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -16,7 +16,6 @@ "Folders": "Pastas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderCameraUploads": "Envios a partir da câmara", "HeaderContinueWatching": "Continuar a Ver", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index b534d0bbeb..2079940cd1 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -83,7 +83,6 @@ "Playlists": "Listas de Reprodução", "Photos": "Fotografias", "Movies": "Filmes", - "HeaderCameraUploads": "Carregamentos a partir da câmara", "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}", "DeviceOnlineWithName": "{0} está connectado", "DeviceOfflineWithName": "{0} desconectou-se", diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 699dd26daa..bc008df3b4 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -74,7 +74,6 @@ "HeaderFavoriteArtists": "Artiști Favoriți", "HeaderFavoriteAlbums": "Albume Favorite", "HeaderContinueWatching": "Vizionează în continuare", - "HeaderCameraUploads": "Incărcări Cameră Foto", "HeaderAlbumArtists": "Album Artiști", "Genres": "Genuri", "Folders": "Dosare", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 648aa384b0..95b93afb82 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -16,7 +16,6 @@ "Folders": "Папки", "Genres": "Жанры", "HeaderAlbumArtists": "Исполнители альбома", - "HeaderCameraUploads": "Камеры", "HeaderContinueWatching": "Продолжение просмотра", "HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteArtists": "Избранные исполнители", diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 0ee652637c..8e50269444 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -16,7 +16,6 @@ "Folders": "Priečinky", "Genres": "Žánre", "HeaderAlbumArtists": "Umelci albumu", - "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovať v pozeraní", "HeaderFavoriteAlbums": "Obľúbené albumy", "HeaderFavoriteArtists": "Obľúbení umelci", diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 329c562e73..ff4b9e84fd 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -16,7 +16,6 @@ "Folders": "Mape", "Genres": "Zvrsti", "HeaderAlbumArtists": "Izvajalci albuma", - "HeaderCameraUploads": "Posnetki kamere", "HeaderContinueWatching": "Nadaljuj gledanje", "HeaderFavoriteAlbums": "Priljubljeni albumi", "HeaderFavoriteArtists": "Priljubljeni izvajalci", diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 347ba5f970..ecca5af4cc 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -96,7 +96,6 @@ "HeaderFavoriteArtists": "Artistët e preferuar", "HeaderFavoriteAlbums": "Albumet e preferuar", "HeaderContinueWatching": "Vazhdo të shikosh", - "HeaderCameraUploads": "Ngarkimet nga Kamera", "HeaderAlbumArtists": "Artistët e albumeve", "Genres": "Zhanre", "Folders": "Dosje", diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 5f3cbb1c8b..2b1eccfaf3 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -74,7 +74,6 @@ "HeaderFavoriteArtists": "Омиљени извођачи", "HeaderFavoriteAlbums": "Омиљени албуми", "HeaderContinueWatching": "Настави гледање", - "HeaderCameraUploads": "Слања са камере", "HeaderAlbumArtists": "Извођачи албума", "Genres": "Жанрови", "Folders": "Фасцикле", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index c8662b2cab..12fda8a986 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -16,7 +16,6 @@ "Folders": "Mappar", "Genres": "Genrer", "HeaderAlbumArtists": "Albumartister", - "HeaderCameraUploads": "Kamerauppladdningar", "HeaderContinueWatching": "Fortsätt kolla", "HeaderFavoriteAlbums": "Favoritalbum", "HeaderFavoriteArtists": "Favoritartister", diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 810b1b9abe..ae38f45e1c 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -20,7 +20,6 @@ "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது", "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", - "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்", "Folders": "கோப்புறைகள்", "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 3b77215a30..71dd2c7a30 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -50,7 +50,6 @@ "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ", "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ", "HeaderContinueWatching": "ดูต่อ", - "HeaderCameraUploads": "อัปโหลดรูปถ่าย", "HeaderAlbumArtists": "อัลบั้มศิลปิน", "Genres": "ประเภท", "Folders": "โฟลเดอร์", diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 3cf3482eba..1e5f2cf198 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -16,7 +16,6 @@ "Folders": "Klasörler", "Genres": "Türler", "HeaderAlbumArtists": "Albüm Sanatçıları", - "HeaderCameraUploads": "Kamera Yüklemeleri", "HeaderContinueWatching": "İzlemeye Devam Et", "HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteArtists": "Favori Sanatçılar", diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index e673465a42..06cc5f633e 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -16,7 +16,6 @@ "HeaderFavoriteArtists": "Улюблені виконавці", "HeaderFavoriteAlbums": "Улюблені альбоми", "HeaderContinueWatching": "Продовжити перегляд", - "HeaderCameraUploads": "Завантажено з камери", "HeaderAlbumArtists": "Виконавці альбому", "Genres": "Жанри", "Folders": "Каталоги", diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json index 9a5874e295..fa7b2d4d0c 100644 --- a/Emby.Server.Implementations/Localization/Core/ur_PK.json +++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json @@ -105,7 +105,6 @@ "Inherit": "وراثت میں", "HomeVideos": "ہوم ویڈیو", "HeaderRecordingGroups": "ریکارڈنگ گروپس", - "HeaderCameraUploads": "کیمرہ اپلوڈز", "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}", "DeviceOnlineWithName": "{0} متصل ھو چکا ھے", "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے", diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 2392c83479..ac74deff82 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -103,7 +103,6 @@ "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích", "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích", "HeaderFavoriteAlbums": "Album Ưa Thích", - "HeaderCameraUploads": "Máy Ảnh Tải Lên", "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}", "DeviceOnlineWithName": "{0} đã kết nối", "DeviceOfflineWithName": "{0} đã ngắt kết nối", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 6b563a9b13..53a902de28 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -16,7 +16,6 @@ "Folders": "文件夹", "Genres": "风格", "HeaderAlbumArtists": "专辑作家", - "HeaderCameraUploads": "相机上传", "HeaderContinueWatching": "继续观影", "HeaderFavoriteAlbums": "收藏的专辑", "HeaderFavoriteArtists": "最爱的艺术家", diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 1ac62baca9..435e294ef2 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -16,7 +16,6 @@ "Folders": "檔案夾", "Genres": "風格", "HeaderAlbumArtists": "專輯藝人", - "HeaderCameraUploads": "相機上載", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteArtists": "最愛的藝人", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 7b6540c3e3..30f7266300 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -16,7 +16,6 @@ "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯演出者", - "HeaderCameraUploads": "相機上傳", "HeaderContinueWatching": "繼續觀賞", "HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteArtists": "最愛演出者", From 5ee6f4920497ec9f4d6c305e9d0828d21074a773 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 1 Oct 2020 08:10:47 -0600 Subject: [PATCH 056/153] Manually register models used in websocket messages. --- .../ApiServiceCollectionExtensions.cs | 1 + .../Filters/WebsocketModelFilter.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 Jellyfin.Server/Filters/WebsocketModelFilter.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 5bcf6d5f07..e180d0cd7a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -260,6 +260,7 @@ namespace Jellyfin.Server.Extensions c.AddSwaggerTypeMappings(); c.OperationFilter(); + c.DocumentFilter(); }); } diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs new file mode 100644 index 0000000000..90fca5583c --- /dev/null +++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters +{ + /// + /// Add models used in websocket messaging. + /// + public class WebsocketModelFilter : IDocumentFilter + { + /// + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository); + + context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); + } + } +} From 9585fea51e13c628f8d1223d03ffc7f05a570bbb Mon Sep 17 00:00:00 2001 From: Erwin de Haan <1627021+EraYaN@users.noreply.github.com> Date: Thu, 1 Oct 2020 17:38:36 +0200 Subject: [PATCH 057/153] Update azure-pipelines.yml --- .ci/azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index b417aae678..347918e0b0 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -34,6 +34,12 @@ jobs: Linux: 'ubuntu-latest' Windows: 'windows-latest' macOS: 'macos-latest' + +- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: + - template: azure-pipelines-test.yml + parameters: + ImageNames: + Linux: 'ubuntu-latest' - ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-abi.yml From 4a3e0062f9e4c353a79f5cba74a0499aa68565c8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 1 Oct 2020 09:39:57 -0600 Subject: [PATCH 058/153] Add missing PlaystateRequest and remove additional GroupUpdate types --- Jellyfin.Server/Filters/WebsocketModelFilter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs index 90fca5583c..81dfdfba7e 100644 --- a/Jellyfin.Server/Filters/WebsocketModelFilter.cs +++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs @@ -19,13 +19,12 @@ namespace Jellyfin.Server.Filters context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<>), context.SchemaRepository); } } } From c0de26f69a5856b5d4c8f821fdf7e1a5aa571969 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Thu, 1 Oct 2020 17:57:40 +0200 Subject: [PATCH 059/153] Publish OpenAPI Spec to repository server --- .ci/azure-pipelines-package.yml | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index cc845afd43..67aac45c9d 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -63,7 +63,38 @@ jobs: sshEndpoint: repository sourceFolder: '$(Build.SourcesDirectory)/deployment/dist' contents: '**' - targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' + +- job: OpenAPISpec + dependsOn: Test + condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) + displayName: 'Push OpenAPI Spec to repository' + + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download OpenAPI Spec' + inputs: + source: 'current' + artifact: "OpenAPI Spec" + path: "$(System.ArtifactsDirectory)/openapispec" + runVersion: "latest" + + - task: SSH@0 + displayName: 'Create target directory on repository server' + inputs: + sshEndpoint: repository + runOptions: 'inline' + inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)' + + - task: CopyFilesOverSSH@0 + displayName: 'Upload artifacts to repository server' + inputs: + sshEndpoint: repository + sourceFolder: '$(System.ArtifactsDirectory)/openapispec' + contents: 'openapi.json' + targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)' - job: BuildDocker displayName: 'Build Docker' From 2b75af987395c242b69f57b636121302eadaee30 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 1 Oct 2020 10:40:58 -0600 Subject: [PATCH 060/153] set type of GroupUpdate --- Jellyfin.Server/Filters/WebsocketModelFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs index 81dfdfba7e..2488028576 100644 --- a/Jellyfin.Server/Filters/WebsocketModelFilter.cs +++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Server.Filters context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository); - context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<>), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); } } } From dd4f3a7c5184afbada50a038564c95fa780e04f8 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Thu, 1 Oct 2020 18:43:44 +0200 Subject: [PATCH 061/153] feat: convert supportedCommands strings to enums --- Emby.Dlna/PlayTo/PlayToManager.cs | 18 +++++++++--------- Jellyfin.Api/Controllers/SessionController.cs | 6 +++--- MediaBrowser.Controller/Session/SessionInfo.cs | 4 ++-- .../Session/ClientCapabilities.cs | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 21877f121f..e93aef3043 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo SupportedCommands = new[] { - GeneralCommandType.VolumeDown.ToString(), - GeneralCommandType.VolumeUp.ToString(), - GeneralCommandType.Mute.ToString(), - GeneralCommandType.Unmute.ToString(), - GeneralCommandType.ToggleMute.ToString(), - GeneralCommandType.SetVolume.ToString(), - GeneralCommandType.SetAudioStreamIndex.ToString(), - GeneralCommandType.SetSubtitleStreamIndex.ToString(), - GeneralCommandType.PlayMediaSource.ToString() + GeneralCommandType.VolumeDown, + GeneralCommandType.VolumeUp, + GeneralCommandType.Mute, + GeneralCommandType.Unmute, + GeneralCommandType.ToggleMute, + GeneralCommandType.SetVolume, + GeneralCommandType.SetAudioStreamIndex, + GeneralCommandType.SetSubtitleStreamIndex, + GeneralCommandType.PlayMediaSource }, SupportsMediaControl = true diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 39bf6e6dc7..2ed7019e5b 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -366,7 +366,7 @@ namespace Jellyfin.Api.Controllers /// /// The session id. /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. - /// A list of supported remote control commands, comma delimited. + /// A list of supported remote control commands. /// Determines whether media can be played remotely.. /// Determines whether sync is supported. /// Determines whether the device supports a unique identifier. @@ -378,7 +378,7 @@ namespace Jellyfin.Api.Controllers public ActionResult PostCapabilities( [FromQuery] string? id, [FromQuery] string? playableMediaTypes, - [FromQuery] string? supportedCommands, + [FromQuery] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, [FromQuery] bool supportsPersistentIdentifier = true) @@ -391,7 +391,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.ReportCapabilities(id, new ClientCapabilities { PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), - SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true), + SupportedCommands = supportedCommands == null ? Array.Empty() : supportedCommands, SupportsMediaControl = supportsMediaControl, SupportsSync = supportsSync, SupportsPersistentIdentifier = supportsPersistentIdentifier diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 55e44c19db..ce58a60b9a 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -230,8 +230,8 @@ namespace MediaBrowser.Controller.Session /// Gets or sets the supported commands. /// /// The supported commands. - public string[] SupportedCommands - => Capabilities == null ? Array.Empty() : Capabilities.SupportedCommands; + public GeneralCommandType[] SupportedCommands + => Capabilities == null ? Array.Empty() : Capabilities.SupportedCommands; public Tuple EnsureController(Func factory) { diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index d3878ca308..a85e6ff2a4 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Session { public string[] PlayableMediaTypes { get; set; } - public string[] SupportedCommands { get; set; } + public GeneralCommandType[] SupportedCommands { get; set; } public bool SupportsMediaControl { get; set; } @@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Session public ClientCapabilities() { PlayableMediaTypes = Array.Empty(); - SupportedCommands = Array.Empty(); + SupportedCommands = Array.Empty(); SupportsPersistentIdentifier = true; } } From f314be9d8513829a5de21eeb2ef19e10943b2a0e Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Thu, 1 Oct 2020 18:44:22 +0200 Subject: [PATCH 062/153] chore(CONTRIBUTORS.md): add skyfrk --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 99060d0b09..7b4772730a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -137,6 +137,7 @@ - [KristupasSavickas](https://github.com/KristupasSavickas) - [Pusta](https://github.com/pusta) - [nielsvanvelzen](https://github.com/nielsvanvelzen) + - [skyfrk](https://github.com/skyfrk) # Emby Contributors From 0655928ab14452dde97192ead66b33c927a75d5a Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Thu, 1 Oct 2020 19:56:59 +0200 Subject: [PATCH 063/153] feat: add CommaDelimitedArrayModelBinder --- .../CommaDelimitedArrayModelBinder.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs new file mode 100644 index 0000000000..1bfd741fd9 --- /dev/null +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -0,0 +1,42 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.ModelBinders +{ + /// + /// Comma delimited array model binder. + /// Returns an empty array of specified type if there is no query parameter. + /// + public class CommaDelimitedArrayModelBinder : IModelBinder + { + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + var input = valueProviderResult.FirstValue; + var elementType = bindingContext.ModelType.GetElementType(); + + if (input != null) + { + var converter = TypeDescriptor.GetConverter(elementType); + var values = Array.ConvertAll( + input.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), + x => { return converter.ConvertFromString(x != null ? x.Trim() : x); }); + + var typedValues = Array.CreateInstance(elementType, values.Length); + values.CopyTo(typedValues, 0); + + bindingContext.Result = ModelBindingResult.Success(typedValues); + } + else + { + var emptyResult = Array.CreateInstance(elementType, 0); + bindingContext.Result = ModelBindingResult.Success(emptyResult); + } + + return Task.CompletedTask; + } + } +} From ba12ea7f4a0bb4804bafa335d374d45bac37ea84 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Thu, 1 Oct 2020 19:57:31 +0200 Subject: [PATCH 064/153] feat: use CommaDelimitedArrayModelBinder to retain API --- Jellyfin.Api/Controllers/SessionController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 2ed7019e5b..68cec14151 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; @@ -366,7 +367,7 @@ namespace Jellyfin.Api.Controllers /// /// The session id. /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. - /// A list of supported remote control commands. + /// A list of supported remote control commands, comma delimited. /// Determines whether media can be played remotely.. /// Determines whether sync is supported. /// Determines whether the device supports a unique identifier. @@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers public ActionResult PostCapabilities( [FromQuery] string? id, [FromQuery] string? playableMediaTypes, - [FromQuery] GeneralCommandType[] supportedCommands, + [FromQuery][ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, [FromQuery] bool supportsPersistentIdentifier = true) From d91a4f0c6d81177b03f0543056456058604e0ce1 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 1 Oct 2020 20:25:40 +0100 Subject: [PATCH 065/153] Update ApplicationHost.cs --- .../ApplicationHost.cs | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7a46fdf2e7..b4f7319f8e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -819,38 +819,6 @@ namespace Emby.Server.Implementations { try { - if (plugin is IPluginAssembly assemblyPlugin) - { - var assembly = plugin.GetType().Assembly; - var assemblyName = assembly.GetName(); - var assemblyFilePath = assembly.Location; - - var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); - - assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); - - try - { - var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); - if (idAttributes.Length > 0) - { - var attribute = (GuidAttribute)idAttributes[0]; - var assemblyId = new Guid(attribute.Value); - - assemblyPlugin.SetId(assemblyId); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName); - } - } - - if (plugin is IHasPluginConfiguration hasPluginConfiguration) - { - hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); - } - plugin.RegisterServices(ServiceCollection); } catch (Exception ex) From a69731b5e32d63042d18188f90dec800b3b1a2b9 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 1 Oct 2020 20:30:12 +0100 Subject: [PATCH 066/153] Update BasePlugin.cs Moved initialisation ApplicationHost.cs /LoadPlugin() --- MediaBrowser.Common/Plugins/BasePlugin.cs | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 4b2918d085..8d9917cd63 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -140,6 +140,37 @@ namespace MediaBrowser.Common.Plugins { ApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; + if (this is IPluginAssembly assemblyPlugin) + { + var assembly = GetType().Assembly; + var assemblyName = assembly.GetName(); + var assemblyFilePath = assembly.Location; + + var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); + + assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); + + try + { + var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); + if (idAttributes.Length > 0) + { + var attribute = (GuidAttribute)idAttributes[0]; + var assemblyId = new Guid(attribute.Value); + + assemblyPlugin.SetId(assemblyId); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName); + } + } + + if (this is IHasPluginConfiguration hasPluginConfiguration) + { + hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); + } } /// From dff2674b27da65c0ff7a82575df77be856985b96 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 1 Oct 2020 20:42:48 +0100 Subject: [PATCH 067/153] Update BasePlugin.cs --- MediaBrowser.Common/Plugins/BasePlugin.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 8d9917cd63..8545fd5dcf 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; @@ -150,20 +151,13 @@ namespace MediaBrowser.Common.Plugins assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); - try + var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); + if (idAttributes.Length > 0) { - var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); - if (idAttributes.Length > 0) - { - var attribute = (GuidAttribute)idAttributes[0]; - var assemblyId = new Guid(attribute.Value); + var attribute = (GuidAttribute)idAttributes[0]; + var assemblyId = new Guid(attribute.Value); - assemblyPlugin.SetId(assemblyId); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName); + assemblyPlugin.SetId(assemblyId); } } From 4b4c74bdcd2ffd119f930226179360907c15fd74 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Thu, 1 Oct 2020 22:04:53 +0200 Subject: [PATCH 068/153] feat: extend CommaDelimitedArrayModelBinder to support auto generated openAPI spec --- .../CommaDelimitedArrayModelBinder.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 1bfd741fd9..92bbd96633 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -15,25 +15,42 @@ namespace Jellyfin.Api.ModelBinders public Task BindModelAsync(ModelBindingContext bindingContext) { var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); - var input = valueProviderResult.FirstValue; var elementType = bindingContext.ModelType.GetElementType(); + var converter = TypeDescriptor.GetConverter(elementType); - if (input != null) + if (valueProviderResult.Length > 1) { - var converter = TypeDescriptor.GetConverter(elementType); - var values = Array.ConvertAll( - input.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), - x => { return converter.ConvertFromString(x != null ? x.Trim() : x); }); + var result = Array.CreateInstance(elementType, valueProviderResult.Length); - var typedValues = Array.CreateInstance(elementType, values.Length); - values.CopyTo(typedValues, 0); + for (int i = 0; i < valueProviderResult.Length; i++) + { + var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim()); - bindingContext.Result = ModelBindingResult.Success(typedValues); + result.SetValue(value, i); + } + + bindingContext.Result = ModelBindingResult.Success(result); } else { - var emptyResult = Array.CreateInstance(elementType, 0); - bindingContext.Result = ModelBindingResult.Success(emptyResult); + var value = valueProviderResult.FirstValue; + + if (value != null) + { + var values = Array.ConvertAll( + value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), + x => { return converter.ConvertFromString(x != null ? x.Trim() : x); }); + + var typedValues = Array.CreateInstance(elementType, values.Length); + values.CopyTo(typedValues, 0); + + bindingContext.Result = ModelBindingResult.Success(typedValues); + } + else + { + var emptyResult = Array.CreateInstance(elementType, 0); + bindingContext.Result = ModelBindingResult.Success(emptyResult); + } } return Task.CompletedTask; From d10090b394371e6b588a08b453a1dfb177e90ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20E=C3=9Flinger?= Date: Thu, 1 Oct 2020 22:48:42 +0200 Subject: [PATCH 069/153] fix: remove unused null check Co-authored-by: Cody Robibero --- Jellyfin.Api/Controllers/SessionController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 68cec14151..0ae49ea982 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -392,7 +392,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.ReportCapabilities(id, new ClientCapabilities { PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), - SupportedCommands = supportedCommands == null ? Array.Empty() : supportedCommands, + SupportedCommands = supportedCommands, SupportsMediaControl = supportsMediaControl, SupportsSync = supportsSync, SupportsPersistentIdentifier = supportsPersistentIdentifier From 21b39a207dd4a6864e9d7bfe1c1e9253cbfc0f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20E=C3=9Flinger?= Date: Fri, 2 Oct 2020 01:33:15 +0200 Subject: [PATCH 070/153] refactor: simplify null check Co-authored-by: Cody Robibero --- Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 92bbd96633..208566dc8e 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -39,7 +39,7 @@ namespace Jellyfin.Api.ModelBinders { var values = Array.ConvertAll( value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), - x => { return converter.ConvertFromString(x != null ? x.Trim() : x); }); + x => converter.ConvertFromString(x?.Trim())); var typedValues = Array.CreateInstance(elementType, values.Length); values.CopyTo(typedValues, 0); From 810ec0b672c06baa34782ccfb4b0b5cd51196662 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 07:00:57 -0600 Subject: [PATCH 071/153] Fix download api spec --- .ci/azure-pipelines-api-client.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index babee15b5a..0c741495f7 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -17,13 +17,11 @@ jobs: - task: DownloadPipelineArtifact@2 displayName: 'Download OpenAPI Spec Artifact' inputs: - source: "specific" + source: 'current' artifact: "OpenAPI Spec" - path: "$(System.ArtifactsDirectory)/openapi" - project: "$(System.TeamProjectId)" - pipeline: "29" # The main server CI build - runVersion: "latestFromBranch" - runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" + path: "$(System.ArtifactsDirectory)/openapispec" + runVersion: "latest" + dependsOn: Test - task: CmdLine@2 displayName: 'Download OpenApi Generator' @@ -68,5 +66,5 @@ jobs: inputs: command: publish publishRegistry: useExternalRegistry - publishEndpoint: + publishEndpoint: workingDir: ./apiclient/generated/typescript/axios From fcc9482ff94254dc853157f200f4a03c92f0366e Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 07:07:48 -0600 Subject: [PATCH 072/153] Generate document file for openapi spec in CI --- .ci/azure-pipelines-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index eca8aa90f9..6a36698b56 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -56,7 +56,7 @@ jobs: inputs: command: "test" projects: ${{ parameters.TestProjects }} - arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"' + arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal' publishTestResults: true testRunTitle: $(Agent.JobName) workingDirectory: "$(Build.SourcesDirectory)" From 520acc11e6e1def24939527be8ccc186ded1c7b4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 07:19:18 -0600 Subject: [PATCH 073/153] scope npm package name --- apiclient/templates/typescript/package.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiclient/templates/typescript/package.mustache b/apiclient/templates/typescript/package.mustache index 5127917a14..251a403836 100644 --- a/apiclient/templates/typescript/package.mustache +++ b/apiclient/templates/typescript/package.mustache @@ -1,5 +1,5 @@ { - "name": "jellyfin-apiclient-{{npmName}}", + "name": "@jellyfin/client-{{npmName}}", "version": "10.7.0{{snapshotVersion}}", "description": "Jellyfin api client using {{npmName}}", "author": "Jellyfin Contributors", From 7d992798fdbd1dbba7242d83f79d7fba0499d15c Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 07:40:44 -0600 Subject: [PATCH 074/153] specify client specifically instead of through template --- .ci/azure-pipelines-api-client.yml | 4 ++-- .../templates/typescript/{ => axios}/package.mustache | 6 +++--- apiclient/templates/typescript/{ => axios}/stable.sh | 9 ++++----- apiclient/templates/typescript/axios/unstable.sh | 9 +++++++++ apiclient/templates/typescript/unstable.sh | 10 ---------- 5 files changed, 18 insertions(+), 20 deletions(-) rename apiclient/templates/typescript/{ => axios}/package.mustache (83%) rename apiclient/templates/typescript/{ => axios}/stable.sh (58%) create mode 100644 apiclient/templates/typescript/axios/unstable.sh delete mode 100644 apiclient/templates/typescript/unstable.sh diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index 0c741495f7..cbedcc6079 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -38,7 +38,7 @@ jobs: displayName: 'Build unstable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: - script: 'bash ./apiclient/templates/typescript/unstable.sh axios' + script: 'bash ./apiclient/templates/typescript/axios/unstable.sh' - task: Npm@1 displayName: 'Publish unstable typescript axios client' @@ -58,7 +58,7 @@ jobs: displayName: 'Build stable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: - script: 'bash ./apiclient/templates/typescript/stable.sh axios' + script: 'bash ./apiclient/templates/typescript/axios/stable.sh' - task: Npm@1 displayName: 'Publish stable typescript axios client' diff --git a/apiclient/templates/typescript/package.mustache b/apiclient/templates/typescript/axios/package.mustache similarity index 83% rename from apiclient/templates/typescript/package.mustache rename to apiclient/templates/typescript/axios/package.mustache index 251a403836..7bfab08cbb 100644 --- a/apiclient/templates/typescript/package.mustache +++ b/apiclient/templates/typescript/axios/package.mustache @@ -1,10 +1,10 @@ { - "name": "@jellyfin/client-{{npmName}}", + "name": "@jellyfin/client-axios", "version": "10.7.0{{snapshotVersion}}", - "description": "Jellyfin api client using {{npmName}}", + "description": "Jellyfin api client using axios", "author": "Jellyfin Contributors", "keywords": [ - "{{npmName}}", + "axios", "typescript", "jellyfin" ], diff --git a/apiclient/templates/typescript/stable.sh b/apiclient/templates/typescript/axios/stable.sh similarity index 58% rename from apiclient/templates/typescript/stable.sh rename to apiclient/templates/typescript/axios/stable.sh index f23a85cc96..ecc55d2e79 100644 --- a/apiclient/templates/typescript/stable.sh +++ b/apiclient/templates/typescript/axios/stable.sh @@ -1,10 +1,9 @@ #!/bin/bash -CLIENT=$1 java -jar openapi-generator-cli.jar generate \ --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ - --generator-name typescript-${CLIENT} \ - --output ./apiclient/generated/typescript/${CLIENT} \ - --template-dir ./apiclient/templates/typescript \ + --generator-name typescript-axios \ + --output ./apiclient/generated/typescript/axios \ + --template-dir ./apiclient/templates/typescript/axios \ --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",npmName="${CLIENT}" + --additional-properties=useSingleRequestParameter="true",npmName="axios" diff --git a/apiclient/templates/typescript/axios/unstable.sh b/apiclient/templates/typescript/axios/unstable.sh new file mode 100644 index 0000000000..615eb5b228 --- /dev/null +++ b/apiclient/templates/typescript/axios/unstable.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +java -jar openapi-generator-cli.jar generate \ + --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ + --generator-name typescript-axios \ + --output ./apiclient/generated/typescript/axios \ + --template-dir ./apiclient/templates/typescript/axios \ + --ignore-file-override ./apiclient/.openapi-generator-ignore \ + --additional-properties=useSingleRequestParameter="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://dev.azure.com/jellyfin-project/jellyfin/_packaging" diff --git a/apiclient/templates/typescript/unstable.sh b/apiclient/templates/typescript/unstable.sh deleted file mode 100644 index 3571c8ad5c..0000000000 --- a/apiclient/templates/typescript/unstable.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -CLIENT=$1 -java -jar openapi-generator-cli.jar generate \ - --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ - --generator-name typescript-${CLIENT} \ - --output ./apiclient/generated/typescript/${CLIENT} \ - --template-dir ./apiclient/templates/typescript \ - --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",npmName="${CLIENT}",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://dev.azure.com/jellyfin-project/jellyfin/_packaging" From 75dada6308c2be5d0c35f679cbcd49f6b7bd0cb9 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 07:47:14 -0600 Subject: [PATCH 075/153] add withSeparateModelsAndApi --- apiclient/templates/typescript/axios/stable.sh | 2 +- apiclient/templates/typescript/axios/unstable.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apiclient/templates/typescript/axios/stable.sh b/apiclient/templates/typescript/axios/stable.sh index ecc55d2e79..70c6409fb6 100644 --- a/apiclient/templates/typescript/axios/stable.sh +++ b/apiclient/templates/typescript/axios/stable.sh @@ -6,4 +6,4 @@ java -jar openapi-generator-cli.jar generate \ --output ./apiclient/generated/typescript/axios \ --template-dir ./apiclient/templates/typescript/axios \ --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",npmName="axios" + --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios" diff --git a/apiclient/templates/typescript/axios/unstable.sh b/apiclient/templates/typescript/axios/unstable.sh index 615eb5b228..83e4ba228a 100644 --- a/apiclient/templates/typescript/axios/unstable.sh +++ b/apiclient/templates/typescript/axios/unstable.sh @@ -6,4 +6,4 @@ java -jar openapi-generator-cli.jar generate \ --output ./apiclient/generated/typescript/axios \ --template-dir ./apiclient/templates/typescript/axios \ --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://dev.azure.com/jellyfin-project/jellyfin/_packaging" + --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://dev.azure.com/jellyfin-project/jellyfin/_packaging" From 3d20ff69d4c711fcde794830c27f53df5d51941b Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 07:50:38 -0600 Subject: [PATCH 076/153] Fix repo connections --- .ci/azure-pipelines-api-client.yml | 12 ++---------- apiclient/templates/typescript/axios/unstable.sh | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index cbedcc6079..dbd70c7028 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -30,10 +30,6 @@ jobs: # Generate npm api client # Unstable - - task: npmAuthenticate@0 - displayName: 'Authenticate to unstable npm feed' - condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') - - task: CmdLine@2 displayName: 'Build unstable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') @@ -46,14 +42,10 @@ jobs: inputs: command: publish publishRegistry: useFeed - publishFeed: jellyfin/unstable + publishFeed: unstable workingDir: ./apiclient/generated/typescript/axios # Stable - - task: npmAuthenticate@0 - displayName: 'Authenticate to stable npm feed' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - task: CmdLine@2 displayName: 'Build stable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') @@ -66,5 +58,5 @@ jobs: inputs: command: publish publishRegistry: useExternalRegistry - publishEndpoint: + publishEndpoint: 'jellyfin-bot for NPM' workingDir: ./apiclient/generated/typescript/axios diff --git a/apiclient/templates/typescript/axios/unstable.sh b/apiclient/templates/typescript/axios/unstable.sh index 83e4ba228a..5d85dc726c 100644 --- a/apiclient/templates/typescript/axios/unstable.sh +++ b/apiclient/templates/typescript/axios/unstable.sh @@ -6,4 +6,4 @@ java -jar openapi-generator-cli.jar generate \ --output ./apiclient/generated/typescript/axios \ --template-dir ./apiclient/templates/typescript/axios \ --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://dev.azure.com/jellyfin-project/jellyfin/_packaging" + --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/" From 86e06369a93e6d7047646470e3755ad5df4c8974 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 08:00:57 -0600 Subject: [PATCH 077/153] fix liniting errors --- .ci/azure-pipelines-api-client.yml | 52 +++++++++++++++--------------- .ci/azure-pipelines.yml | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index dbd70c7028..f6e870674d 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -9,6 +9,7 @@ jobs: - job: GenerateApiClients displayName: 'Generate Api Clients' + dependsOn: Test pool: vmImage: "${{ parameters.LinuxImage }}" @@ -21,42 +22,41 @@ jobs: artifact: "OpenAPI Spec" path: "$(System.ArtifactsDirectory)/openapispec" runVersion: "latest" - dependsOn: Test - task: CmdLine@2 - displayName: 'Download OpenApi Generator' - inputs: - scripts: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" + displayName: 'Download OpenApi Generator' + inputs: + script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" # Generate npm api client # Unstable - task: CmdLine@2 - displayName: 'Build unstable typescript axios client' - condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') - inputs: - script: 'bash ./apiclient/templates/typescript/axios/unstable.sh' + displayName: 'Build unstable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + script: 'bash ./apiclient/templates/typescript/axios/unstable.sh' - task: Npm@1 - displayName: 'Publish unstable typescript axios client' - condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') - inputs: - command: publish - publishRegistry: useFeed - publishFeed: unstable - workingDir: ./apiclient/generated/typescript/axios + displayName: 'Publish unstable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + command: publish + publishRegistry: useFeed + publishFeed: unstable + workingDir: ./apiclient/generated/typescript/axios # Stable - task: CmdLine@2 - displayName: 'Build stable typescript axios client' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - inputs: - script: 'bash ./apiclient/templates/typescript/axios/stable.sh' + displayName: 'Build stable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + script: 'bash ./apiclient/templates/typescript/axios/stable.sh' - task: Npm@1 - displayName: 'Publish stable typescript axios client' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - inputs: - command: publish - publishRegistry: useExternalRegistry - publishEndpoint: 'jellyfin-bot for NPM' - workingDir: ./apiclient/generated/typescript/axios + displayName: 'Publish stable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + command: publish + publishRegistry: useExternalRegistry + publishEndpoint: 'jellyfin-bot for NPM' + workingDir: ./apiclient/generated/typescript/axios diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index f3e515447d..4c5db80c10 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -57,4 +57,4 @@ jobs: - template: azure-pipelines-package.yml - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - - template: azure-pipelines-api-client.yml + - template: azure-pipelines-api-client.yml From b95533d6a947306e92b02ff77d17fe07ca78e87c Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 08:06:06 -0600 Subject: [PATCH 078/153] there I go changing paths again --- apiclient/templates/typescript/axios/stable.sh | 2 +- apiclient/templates/typescript/axios/unstable.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apiclient/templates/typescript/axios/stable.sh b/apiclient/templates/typescript/axios/stable.sh index 70c6409fb6..118ef219fe 100644 --- a/apiclient/templates/typescript/axios/stable.sh +++ b/apiclient/templates/typescript/axios/stable.sh @@ -1,7 +1,7 @@ #!/bin/bash java -jar openapi-generator-cli.jar generate \ - --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ + --input-spec $(System.ArtifactsDirectory)/openapispec/openapi.json \ --generator-name typescript-axios \ --output ./apiclient/generated/typescript/axios \ --template-dir ./apiclient/templates/typescript/axios \ diff --git a/apiclient/templates/typescript/axios/unstable.sh b/apiclient/templates/typescript/axios/unstable.sh index 5d85dc726c..be9f9be438 100644 --- a/apiclient/templates/typescript/axios/unstable.sh +++ b/apiclient/templates/typescript/axios/unstable.sh @@ -1,7 +1,7 @@ #!/bin/bash java -jar openapi-generator-cli.jar generate \ - --input-spec $(System.ArtifactsDirectory)/openapi/openapi.json \ + --input-spec $(System.ArtifactsDirectory)/openapispec/openapi.json \ --generator-name typescript-axios \ --output ./apiclient/generated/typescript/axios \ --template-dir ./apiclient/templates/typescript/axios \ From 0ffc58e25509c375654430e64a2208baf6d65be9 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 2 Oct 2020 08:21:28 -0600 Subject: [PATCH 079/153] Update .ci/azure-pipelines-api-client.yml Co-authored-by: Cameron --- .ci/azure-pipelines-api-client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index f6e870674d..7f428aec15 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -4,7 +4,7 @@ default: "ubuntu-latest" - name: GeneratorVersion type: string - default: "4.3.1" + default: "5.0.0-beta2" jobs: - job: GenerateApiClients From 9aad772288145645d51f93b26a2493782f55f2d3 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Fri, 2 Oct 2020 18:26:48 +0200 Subject: [PATCH 080/153] feat: implement CommaDelimitedArrayModelBinderProvider --- Jellyfin.Api/Controllers/SessionController.cs | 2 +- .../CommaDelimitedArrayModelBinderProvider.cs | 29 +++++++++++++++++++ .../ApiServiceCollectionExtensions.cs | 3 ++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 68cec14151..3dbf7ba0b7 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers public ActionResult PostCapabilities( [FromQuery] string? id, [FromQuery] string? playableMediaTypes, - [FromQuery][ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, + [FromQuery] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, [FromQuery] bool supportsPersistentIdentifier = true) diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs new file mode 100644 index 0000000000..b9785a73b8 --- /dev/null +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.ModelBinders +{ + /// + /// Comma delimited array model binder provider. + /// + public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider + { + private readonly IModelBinder _binder; + + /// + /// Initializes a new instance of the class. + /// + public CommaDelimitedArrayModelBinderProvider() + { + _binder = new CommaDelimitedArrayModelBinder(); + } + + /// + public IModelBinder? GetBinder(ModelBinderProviderContext context) + { + return context.Metadata.ModelType.IsArray ? _binder : null; + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 5bcf6d5f07..f867143df6 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; @@ -166,6 +167,8 @@ namespace Jellyfin.Server.Extensions opts.OutputFormatters.Add(new CssOutputFormatter()); opts.OutputFormatters.Add(new XmlOutputFormatter()); + + opts.ModelBinderProviders.Insert(0, new CommaDelimitedArrayModelBinderProvider()); }) // Clear app parts to avoid other assemblies being picked up From 754e859f6e13c0fad0ed91d694b7fca163f58ce1 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Fri, 2 Oct 2020 12:05:39 -0500 Subject: [PATCH 081/153] Convert strings to ImageFormat --- Jellyfin.Api/Controllers/ImageController.cs | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7afec1219e..6f925dec58 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -364,7 +364,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? quality, [FromQuery] string? tag, [FromQuery] bool? cropWhitespace, - [FromQuery] string? format, + [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, @@ -443,7 +443,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? quality, [FromRoute, Required] string tag, [FromQuery] bool? cropWhitespace, - [FromRoute, Required] string format, + [FromRoute, Required] ImageFormat format, [FromQuery] bool? addPlayedIndicator, [FromRoute, Required] double percentPlayed, [FromRoute, Required] int unplayedCount, @@ -516,7 +516,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -595,7 +595,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -674,7 +674,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -753,7 +753,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -832,7 +832,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] string tag, - [FromRoute, Required] string format, + [FromRoute, Required] ImageFormat format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -911,7 +911,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, - [FromQuery] string? format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -1038,7 +1038,7 @@ namespace Jellyfin.Api.Controllers ImageType imageType, int? imageIndex, string? tag, - string? format, + ImageFormat? format, int? maxWidth, int? maxHeight, double? percentPlayed, @@ -1128,12 +1128,11 @@ namespace Jellyfin.Api.Controllers isHeadRequest).ConfigureAwait(false); } - private ImageFormat[] GetOutputFormats(string? format) + private ImageFormat[] GetOutputFormats(ImageFormat? format) { - if (!string.IsNullOrWhiteSpace(format) - && Enum.TryParse(format, true, out ImageFormat parsedFormat)) + if (format.HasValue) { - return new[] { parsedFormat }; + return new[] { format.Value }; } return GetClientSupportedFormats(); @@ -1157,7 +1156,7 @@ namespace Jellyfin.Api.Controllers var acceptParam = Request.Query[HeaderNames.Accept]; - var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false); + var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false); if (!supportsWebP) { @@ -1179,7 +1178,7 @@ namespace Jellyfin.Api.Controllers formats.Add(ImageFormat.Jpg); formats.Add(ImageFormat.Png); - if (SupportsFormat(supportedFormats, acceptParam, "gif", true)) + if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true)) { formats.Add(ImageFormat.Gif); } @@ -1187,9 +1186,10 @@ namespace Jellyfin.Api.Controllers return formats.ToArray(); } - private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string acceptParam, string format, bool acceptAll) + private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll) { - var mimeType = "image/" + format; + var normalized = format.ToString().ToLowerInvariant(); + var mimeType = "image/" + normalized; if (requestAcceptTypes.Contains(mimeType)) { @@ -1201,7 +1201,7 @@ namespace Jellyfin.Api.Controllers return true; } - return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase); + return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase); } private async Task GetImageResult( From 8535a718b7e9307dfcb2e04d198b0cabbdb15205 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 2 Oct 2020 19:43:34 +0200 Subject: [PATCH 082/153] Add tests for deserializing guids --- .../Json/JsonGuidConverterTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs new file mode 100644 index 0000000000..aef5b3a7d7 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json; +using MediaBrowser.Common.Json.Converters; +using Xunit; + +namespace Jellyfin.Common.Tests.Extensions +{ + public static class JsonGuidConverterTests + { + [Fact] + public static void Deserialize_Valid_Success() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonGuidConverter()); + Guid value = JsonSerializer.Deserialize(@"""a852a27afe324084ae66db579ee3ee18""", options); + Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); + + value = JsonSerializer.Deserialize(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", options); + Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); + } + } +} From 6a32385588889074496dd94a6160614d01bdd63d Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 2 Oct 2020 13:30:31 -0600 Subject: [PATCH 083/153] Allow server to return .data files --- Jellyfin.Server/Startup.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 2f4620aa63..62ffe174cd 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Net.Http.Headers; +using System.Net.Mime; using Jellyfin.Api.TypeConverters; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; @@ -11,6 +12,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -123,10 +125,15 @@ namespace Jellyfin.Server mainApp.UseStaticFiles(); if (appConfig.HostWebClient()) { + var extensionProvider = new FileExtensionContentTypeProvider(); + + // subtitles octopus requires .data files. + extensionProvider.Mappings.Add(".data", MediaTypeNames.Application.Octet); mainApp.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath), - RequestPath = "/web" + RequestPath = "/web", + ContentTypeProvider = extensionProvider }); } From 33f80dc3c167f07348cd817f38a33a78302bda6b Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Sat, 3 Oct 2020 01:09:15 +0200 Subject: [PATCH 084/153] feat(CommaDelimitedArrayModelBinder): add none result check --- Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 208566dc8e..13469194a0 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -18,6 +18,11 @@ namespace Jellyfin.Api.ModelBinders var elementType = bindingContext.ModelType.GetElementType(); var converter = TypeDescriptor.GetConverter(elementType); + if (valueProviderResult == ValueProviderResult.None) + { + return Task.CompletedTask; + } + if (valueProviderResult.Length > 1) { var result = Array.CreateInstance(elementType, valueProviderResult.Length); From b3b98a5cc8d49174dda4d4784a7e9297b5961f68 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Sat, 3 Oct 2020 01:09:28 +0200 Subject: [PATCH 085/153] test: add TestType enum --- .../Jellyfin.Api.Tests/ModelBinders/TestType.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs new file mode 100644 index 0000000000..544a74637a --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Jellyfin.Api.Tests.ModelBinders +{ + public enum TestType + { +#pragma warning disable SA1602 // Enumeration items should be documented + How, + Much, + Is, + The, + Fish +#pragma warning restore SA1602 // Enumeration items should be documented + } +} From 1bd80a634fc941c51b13b624530ab12ce6551820 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Sat, 3 Oct 2020 01:09:45 +0200 Subject: [PATCH 086/153] test: add CommaDelimitedArrayModelBinder tests --- .../CommaDelimitedArrayModelBinderTests.cs | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs new file mode 100644 index 0000000000..b05be6a16a --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Api.ModelBinders; +using MediaBrowser.Controller.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; +using Xunit.Sdk; + +namespace Jellyfin.Api.Tests.ModelBinders +{ + public sealed class CommaDelimitedArrayModelBinderTests + { + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery() + { + var queryParamName = "test"; + var queryParamValues = new string[] { "lol", "xd" }; + var queryParamString = "lol,xd"; + var queryParamType = typeof(string[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((string[])bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery() + { + var queryParamName = "test"; + var queryParamValues = new int[] { 42, 0 }; + var queryParamString = "42,0"; + var queryParamType = typeof(int[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((int[])bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery() + { + var queryParamName = "test"; + var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamString = "How,Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery2() + { + var queryParamName = "test"; + var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamString = "How,,Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() + { + var queryParamName = "test"; + var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamString1 = "How"; + var queryParamString2 = "Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() + { + { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery2() + { + var queryParamName = "test"; + var queryParamValues = Array.Empty(); + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() + { + { queryParamName, new StringValues(value: null) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + await modelBinder.BindModelAsync(bindingContextMock.Object); + + Assert.False(bindingContextMock.Object.Result.IsModelSet); + } + + [Fact] + public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid() + { + var queryParamName = "test"; + var queryParamString = "🔥,😢"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + Func act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object); + + await Assert.ThrowsAsync(act); + } + + [Fact] + public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2() + { + var queryParamName = "test"; + var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamString1 = "How"; + var queryParamString2 = "😱"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedArrayModelBinder(); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary() + { + { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); + bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName); + bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); + bindingContextMock.SetupProperty(b => b.Result); + + Func act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object); + + await Assert.ThrowsAsync(act); + } + } +} From 86cbefb059e55680bbf40769cdf13849e19b31bb Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Fri, 2 Oct 2020 20:32:52 -0400 Subject: [PATCH 087/153] Remove Windows legacy files --- windows/build-jellyfin.ps1 | 190 --------- windows/dependencies.txt | 2 - windows/dialogs/confirmation.nsddef | 24 -- windows/dialogs/confirmation.nsdinc | 61 --- windows/dialogs/service-config.nsddef | 13 - windows/dialogs/service-config.nsdinc | 56 --- windows/dialogs/setuptype.nsddef | 12 - windows/dialogs/setuptype.nsdinc | 50 --- windows/helpers/ShowError.nsh | 10 - windows/helpers/StrSlash.nsh | 47 --- windows/jellyfin.nsi | 575 -------------------------- windows/legacy/install-jellyfin.ps1 | 460 --------------------- windows/legacy/install.bat | 1 - 13 files changed, 1501 deletions(-) delete mode 100644 windows/build-jellyfin.ps1 delete mode 100644 windows/dependencies.txt delete mode 100644 windows/dialogs/confirmation.nsddef delete mode 100644 windows/dialogs/confirmation.nsdinc delete mode 100644 windows/dialogs/service-config.nsddef delete mode 100644 windows/dialogs/service-config.nsdinc delete mode 100644 windows/dialogs/setuptype.nsddef delete mode 100644 windows/dialogs/setuptype.nsdinc delete mode 100644 windows/helpers/ShowError.nsh delete mode 100644 windows/helpers/StrSlash.nsh delete mode 100644 windows/jellyfin.nsi delete mode 100644 windows/legacy/install-jellyfin.ps1 delete mode 100644 windows/legacy/install.bat diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1 deleted file mode 100644 index b65e619ee1..0000000000 --- a/windows/build-jellyfin.ps1 +++ /dev/null @@ -1,190 +0,0 @@ -[CmdletBinding()] -param( - [switch]$MakeNSIS, - [switch]$InstallNSIS, - [switch]$InstallFFMPEG, - [switch]$InstallNSSM, - [switch]$SkipJellyfinBuild, - [switch]$GenerateZip, - [string]$InstallLocation = "./dist/jellyfin-win-nsis", - [string]$UXLocation = "../jellyfin-ux", - [switch]$InstallTrayApp, - [ValidateSet('Debug','Release')][string]$BuildType = 'Release', - [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal', - [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win', - [ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64' -) - -$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars. - -#PowershellCore and *nix check to make determine which temp dir to use. -if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){ - $TempDir = mktemp -d -}else{ - $TempDir = $env:Temp -} -#Create staging dir -New-Item -ItemType Directory -Force -Path $InstallLocation -$ResolvedInstallLocation = Resolve-Path $InstallLocation -$ResolvedUXLocation = Resolve-Path $UXLocation - -function Build-JellyFin { - if(($Architecture -eq 'arm64') -and ($WindowsVersion -ne 'win10')){ - Write-Error "arm64 only supported with Windows10 Version" - exit - } - if(($Architecture -eq 'arm') -and ($WindowsVersion -notin @('win10','win81','win8'))){ - Write-Error "arm only supported with Windows 8 or higher" - exit - } - Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture" - Write-Verbose "InstallLocation: $ResolvedInstallLocation" - Write-Verbose "DotNetVerbosity: $DotNetVerbosity" - dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=true -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server -} - -function Install-FFMPEG { - param( - [string]$ResolvedInstallLocation, - [string]$Architecture, - [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared" - ) - Write-Verbose "Checking Architecture" - if($Architecture -notin @('x86','x64')){ - Write-Warning "No builds available for your selected architecture of $Architecture" - Write-Warning "FFMPEG will not be installed" - }elseif($Architecture -eq 'x64'){ - Write-Verbose "Downloading 64 bit FFMPEG" - Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose - }else{ - Write-Verbose "Downloading 32 bit FFMPEG" - Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose - } - - Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose - if($Architecture -eq 'x64'){ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/ffmpeg" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - }else{ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - } - Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose - Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose -} - -function Install-NSSM { - param( - [string]$ResolvedInstallLocation, - [string]$Architecture - ) - Write-Verbose "Checking Architecture" - if($Architecture -notin @('x86','x64')){ - Write-Warning "No builds available for your selected architecture of $Architecture" - Write-Warning "NSSM will not be installed" - }else{ - Write-Verbose "Downloading NSSM" - # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - # Temporary workaround, file is hosted in an azure blob with a custom domain in front for brevity - Invoke-WebRequest -Uri http://files.evilt.win/nssm/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose - } - - Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" -Force | Write-Verbose - if($Architecture -eq 'x64'){ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win64" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - }else{ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win32" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - } - Remove-Item "$tempdir/nssm/" -Recurse -Force -ErrorAction Continue | Write-Verbose - Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose -} - -function Make-NSIS { - param( - [string]$ResolvedInstallLocation - ) - - $env:InstallLocation = $ResolvedInstallLocation - if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ - & "$tempdir/nsis/nsis-3.04/makensis.exe" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi" - } else { - & "makensis" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi" - } - Copy-Item .\deployment\windows\jellyfin_*.exe $ResolvedInstallLocation\..\ -} - - -function Install-NSIS { - Write-Verbose "Downloading NSIS" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose - - Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose -} - -function Cleanup-NSIS { - Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose - Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose -} - -function Install-TrayApp { - param( - [string]$ResolvedInstallLocation, - [string]$Architecture - ) - Write-Verbose "Checking Architecture" - if($Architecture -ne 'x64'){ - Write-Warning "No builds available for your selected architecture of $Architecture" - Write-Warning "The tray app will not be available." - }else{ - Write-Verbose "Downloading Tray App and copying to Jellyfin location" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri https://github.com/jellyfin/jellyfin-windows-tray/releases/latest/download/JellyfinTray.exe -UseBasicParsing -OutFile "$installLocation/JellyfinTray.exe" | Write-Verbose - } -} - -if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){ - Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture" - Build-JellyFin -} -if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){ - Write-Verbose "Starting FFMPEG Install" - Install-FFMPEG $ResolvedInstallLocation $Architecture -} -if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){ - Write-Verbose "Starting NSSM Install" - Install-NSSM $ResolvedInstallLocation $Architecture -} -if($InstallTrayApp.IsPresent -or ($InstallTrayApp -eq $true)){ - Write-Verbose "Downloading Windows Tray App" - Install-TrayApp $ResolvedInstallLocation $Architecture -} -#Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1 -#Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat -Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE -if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ - Write-Verbose "Installing NSIS" - Install-NSIS -} -if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){ - Write-Verbose "Starting NSIS Package creation" - Make-NSIS $ResolvedInstallLocation -} -if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ - Write-Verbose "Cleanup NSIS" - Cleanup-NSIS -} -if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){ - Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force -} -Write-Verbose "Finished" diff --git a/windows/dependencies.txt b/windows/dependencies.txt deleted file mode 100644 index 16f77cce7c..0000000000 --- a/windows/dependencies.txt +++ /dev/null @@ -1,2 +0,0 @@ -dotnet -nsis diff --git a/windows/dialogs/confirmation.nsddef b/windows/dialogs/confirmation.nsddef deleted file mode 100644 index 969ebacd62..0000000000 --- a/windows/dialogs/confirmation.nsddef +++ /dev/null @@ -1,24 +0,0 @@ - - - - !include "helpers\StrSlash.nsh" - ${StrSlash} '$0' $INSTDIR - - ${StrSlash} '$1' $_JELLYFINDATADIR_ - - ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ - \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ - Installation Folder:\b0 $0\line\b \ - Service install:\b0 $_INSTALLSERVICE_\line\b \ - Service start:\b0 $_SERVICESTART_\line\b \ - Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \ - Jellyfin Data Folder:\b0 $1\par \ -\ - \pard\sa200\sl276\slmult1\f1\lang1043\par \ - }" - - diff --git a/windows/dialogs/confirmation.nsdinc b/windows/dialogs/confirmation.nsdinc deleted file mode 100644 index f00e9b43ab..0000000000 --- a/windows/dialogs/confirmation.nsdinc +++ /dev/null @@ -1,61 +0,0 @@ -; ========================================================= -; This file was generated by NSISDialogDesigner 1.4.4.0 -; http://coolsoft.altervista.org/nsisdialogdesigner -; -; Do not edit it manually, use NSISDialogDesigner instead! -; Modified by EraYaN (2019-09-01) -; ========================================================= - -; handle variables -Var hCtl_confirmation -Var hCtl_confirmation_ConfirmRichText - -; HeaderCustomScript -!include "helpers\StrSlash.nsh" - - - -; dialog create function -Function fnc_confirmation_Create - - ; === confirmation (type: Dialog) === - nsDialogs::Create 1018 - Pop $hCtl_confirmation - ${If} $hCtl_confirmation == error - Abort - ${EndIf} - !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation" - - ; === ConfirmRichText (type: RichText) === - nsDialogs::CreateControl /NOUNLOAD "RichEdit20A" ${ES_READONLY}|${WS_VISIBLE}|${WS_CHILD}|${WS_TABSTOP}|${WS_VSCROLL}|${ES_MULTILINE}|${ES_WANTRETURN} ${WS_EX_STATICEDGE} 8u 7u 280u 126u "" - Pop $hCtl_confirmation_ConfirmRichText - ${NSD_AddExStyle} $hCtl_confirmation_ConfirmRichText ${WS_EX_STATICEDGE} - - ; CreateFunctionCustomScript - ${StrSlash} '$0' $INSTDIR - - ${StrSlash} '$1' $_JELLYFINDATADIR_ - - ${If} $_INSTALLSERVICE_ == "Yes" - ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ - \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ - Installation Folder:\b0 $0\line\b \ - Service install:\b0 $_INSTALLSERVICE_\line\b \ - Service start:\b0 $_SERVICESTART_\line\b \ - Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \ - Jellyfin Data Folder:\b0 $1\par \ - \ - \pard\sa200\sl276\slmult1\f1\lang1043\par \ - }" - ${Else} - ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ - \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ - Installation Folder:\b0 $0\line\b \ - Service install:\b0 $_INSTALLSERVICE_\line\b \ - Jellyfin Data Folder:\b0 $1\par \ - \ - \pard\sa200\sl276\slmult1\f1\lang1043\par \ - }" - ${EndIf} - -FunctionEnd diff --git a/windows/dialogs/service-config.nsddef b/windows/dialogs/service-config.nsddef deleted file mode 100644 index 3509ada249..0000000000 --- a/windows/dialogs/service-config.nsddef +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/windows/dialogs/service-config.nsdinc b/windows/dialogs/service-config.nsdinc deleted file mode 100644 index 58c350f2ec..0000000000 --- a/windows/dialogs/service-config.nsdinc +++ /dev/null @@ -1,56 +0,0 @@ -; ========================================================= -; This file was generated by NSISDialogDesigner 1.4.4.0 -; http://coolsoft.altervista.org/nsisdialogdesigner -; -; Do not edit it manually, use NSISDialogDesigner instead! -; ========================================================= - -; handle variables -Var hCtl_service_config -Var hCtl_service_config_StartServiceAfterInstall -Var hCtl_service_config_LocalSystemAccountLabel -Var hCtl_service_config_NetworkServiceAccountLabel -Var hCtl_service_config_UseLocalSystemAccount -Var hCtl_service_config_UseNetworkServiceAccount -Var hCtl_service_config_Font1 - - -; dialog create function -Function fnc_service_config_Create - - ; custom font definitions - CreateFont $hCtl_service_config_Font1 "Microsoft Sans Serif" "8.25" "700" - - ; === service_config (type: Dialog) === - nsDialogs::Create 1018 - Pop $hCtl_service_config - ${If} $hCtl_service_config == error - Abort - ${EndIf} - !insertmacro MUI_HEADER_TEXT "Configure the service" "This controls what type of access the server gets to this system." - - ; === StartServiceAfterInstall (type: Checkbox) === - ${NSD_CreateCheckbox} 8u 118u 280u 15u "Start Service after Install" - Pop $hCtl_service_config_StartServiceAfterInstall - ${NSD_Check} $hCtl_service_config_StartServiceAfterInstall - - ; === LocalSystemAccountLabel (type: Label) === - ${NSD_CreateLabel} 8u 71u 280u 28u "The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." - Pop $hCtl_service_config_LocalSystemAccountLabel - - ; === NetworkServiceAccountLabel (type: Label) === - ${NSD_CreateLabel} 8u 24u 280u 28u "The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." - Pop $hCtl_service_config_NetworkServiceAccountLabel - - ; === UseLocalSystemAccount (type: RadioButton) === - ${NSD_CreateRadioButton} 8u 54u 280u 15u "Use Local System account" - Pop $hCtl_service_config_UseLocalSystemAccount - ${NSD_AddStyle} $hCtl_service_config_UseLocalSystemAccount ${WS_GROUP} - - ; === UseNetworkServiceAccount (type: RadioButton) === - ${NSD_CreateRadioButton} 8u 7u 280u 15u "Use Network Service account (Recommended)" - Pop $hCtl_service_config_UseNetworkServiceAccount - SendMessage $hCtl_service_config_UseNetworkServiceAccount ${WM_SETFONT} $hCtl_service_config_Font1 0 - ${NSD_Check} $hCtl_service_config_UseNetworkServiceAccount - -FunctionEnd diff --git a/windows/dialogs/setuptype.nsddef b/windows/dialogs/setuptype.nsddef deleted file mode 100644 index b55ceeaaa6..0000000000 --- a/windows/dialogs/setuptype.nsddef +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/windows/dialogs/setuptype.nsdinc b/windows/dialogs/setuptype.nsdinc deleted file mode 100644 index 8746ad2cc6..0000000000 --- a/windows/dialogs/setuptype.nsdinc +++ /dev/null @@ -1,50 +0,0 @@ -; ========================================================= -; This file was generated by NSISDialogDesigner 1.4.4.0 -; http://coolsoft.altervista.org/nsisdialogdesigner -; -; Do not edit it manually, use NSISDialogDesigner instead! -; ========================================================= - -; handle variables -Var hCtl_setuptype -Var hCtl_setuptype_InstallasaServiceLabel -Var hCtl_setuptype_InstallasaService -Var hCtl_setuptype_BasicInstallLabel -Var hCtl_setuptype_BasicInstall -Var hCtl_setuptype_Font1 - - -; dialog create function -Function fnc_setuptype_Create - - ; custom font definitions - CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700" - - ; === setuptype (type: Dialog) === - nsDialogs::Create 1018 - Pop $hCtl_setuptype - ${If} $hCtl_setuptype == error - Abort - ${EndIf} - !insertmacro MUI_HEADER_TEXT "Setup Type" "Control how Jellyfin is installed." - - ; === InstallasaServiceLabel (type: Label) === - ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." - Pop $hCtl_setuptype_InstallasaServiceLabel - - ; === InstallasaService (type: RadioButton) === - ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)" - Pop $hCtl_setuptype_InstallasaService - ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP} - - ; === BasicInstallLabel (type: Label) === - ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." - Pop $hCtl_setuptype_BasicInstallLabel - - ; === BasicInstall (type: RadioButton) === - ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)" - Pop $hCtl_setuptype_BasicInstall - SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0 - ${NSD_Check} $hCtl_setuptype_BasicInstall - -FunctionEnd diff --git a/windows/helpers/ShowError.nsh b/windows/helpers/ShowError.nsh deleted file mode 100644 index 6e09b1e407..0000000000 --- a/windows/helpers/ShowError.nsh +++ /dev/null @@ -1,10 +0,0 @@ -; Show error -!macro ShowError TEXT RETRYLABEL - MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP "${TEXT}" IDIGNORE +2 IDRETRY ${RETRYLABEL} - Abort -!macroend - -!macro ShowErrorFinal TEXT - MessageBox MB_OK|MB_ICONSTOP "${TEXT}" - Abort -!macroend diff --git a/windows/helpers/StrSlash.nsh b/windows/helpers/StrSlash.nsh deleted file mode 100644 index b8aa771aa6..0000000000 --- a/windows/helpers/StrSlash.nsh +++ /dev/null @@ -1,47 +0,0 @@ -; Adapted from: https://nsis.sourceforge.io/Another_String_Replace_(and_Slash/BackSlash_Converter) (2019-08-31) - -!macro _StrSlashConstructor out in - Push "${in}" - Push "\" - Call StrSlash - Pop ${out} -!macroend - -!define StrSlash '!insertmacro "_StrSlashConstructor"' - -; Push $filenamestring (e.g. 'c:\this\and\that\filename.htm') -; Push "\" -; Call StrSlash -; Pop $R0 -; ;Now $R0 contains 'c:/this/and/that/filename.htm' -Function StrSlash - Exch $R3 ; $R3 = needle ("\" or "/") - Exch - Exch $R1 ; $R1 = String to replacement in (haystack) - Push $R2 ; Replaced haystack - Push $R4 ; $R4 = not $R3 ("/" or "\") - Push $R6 - Push $R7 ; Scratch reg - StrCpy $R2 "" - StrLen $R6 $R1 - StrCpy $R4 "\" - StrCmp $R3 "/" loop - StrCpy $R4 "/" -loop: - StrCpy $R7 $R1 1 - StrCpy $R1 $R1 $R6 1 - StrCmp $R7 $R3 found - StrCpy $R2 "$R2$R7" - StrCmp $R1 "" done loop -found: - StrCpy $R2 "$R2$R4" - StrCmp $R1 "" done loop -done: - StrCpy $R3 $R2 - Pop $R7 - Pop $R6 - Pop $R4 - Pop $R2 - Pop $R1 - Exch $R3 -FunctionEnd diff --git a/windows/jellyfin.nsi b/windows/jellyfin.nsi deleted file mode 100644 index fada62d981..0000000000 --- a/windows/jellyfin.nsi +++ /dev/null @@ -1,575 +0,0 @@ -!verbose 3 -SetCompressor /SOLID bzip2 -ShowInstDetails show -ShowUninstDetails show -Unicode True - -;-------------------------------- -!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh - - !include "MUI2.nsh" - !include "Sections.nsh" - !include "LogicLib.nsh" - - !include "helpers\ShowError.nsh" - -; Global variables that we'll use - Var _JELLYFINVERSION_ - Var _JELLYFINDATADIR_ - Var _SETUPTYPE_ - Var _INSTALLSERVICE_ - Var _SERVICESTART_ - Var _SERVICEACCOUNTTYPE_ - Var _EXISTINGINSTALLATION_ - Var _EXISTINGSERVICE_ - Var _MAKESHORTCUTS_ - Var _FOLDEREXISTS_ -; -!ifdef x64 - !define ARCH "x64" - !define NAMESUFFIX "(64 bit)" - !define INSTALL_DIRECTORY "$PROGRAMFILES64\Jellyfin\Server" -!endif - -!ifdef x84 - !define ARCH "x86" - !define NAMESUFFIX "(32 bit)" - !define INSTALL_DIRECTORY "$PROGRAMFILES32\Jellyfin\Server" -!endif - -!ifndef ARCH - !error "Set the Arch with /Dx86 or /Dx64" -!endif - -;-------------------------------- - - !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\JellyfinServer" ;Registry to show up in Add/Remove Programs - !define REG_CONFIG_KEY "Software\Jellyfin\Server" ;Registry to store all configuration - - !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version - - Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} ${NAMESUFFIX}" ; This is referred in various header text labels - OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows-${ARCH}.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe - BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons - -; installer attributes, these show up in details tab on installer properties - VIProductVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIProductVersion format, should be X.X.X.X - VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X - VIAddVersionKey "ProductName" "Jellyfin Server" - VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0" - VIAddVersionKey "LegalCopyright" "(c) 2019 Jellyfin Contributors. Code released under the GNU General Public License" - VIAddVersionKey "FileDescription" "Jellyfin Server: The Free Software Media System" - -;TODO, check defaults - InstallDir ${INSTALL_DIRECTORY} ;Default installation folder - InstallDirRegKey HKLM "${REG_CONFIG_KEY}" "InstallFolder" ;Read the registry for install folder, - - RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders - - CRCCheck on ; make sure the installer wasn't corrupted while downloading - - !define MUI_ABORTWARNING ;Prompts user in case of aborting install - -; TODO: Replace with nice Jellyfin Icons -!ifdef UXPATH - !define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon - !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon - - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp" - !define MUI_WELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp" - !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp" -!endif - -;-------------------------------- -;Pages - -; Welcome Page - !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server." - !insertmacro MUI_PAGE_WELCOME -; License Page - !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL - -; Setup Type Page - Page custom ShowSetupTypePage SetupTypePage_Config - -; Components Page - !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage - !insertmacro MUI_PAGE_COMPONENTS - !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show - !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog - !insertmacro MUI_PAGE_DIRECTORY - -; Data folder Page - !define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show - !define MUI_PAGE_HEADER_TEXT "Choose Data Location" - !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server data." - !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server data. To install in a different folder, click Browse and select another folder. Please make sure the folder exists and is accessible. Click Next to continue." - !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data folder" - !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_ - !insertmacro MUI_PAGE_DIRECTORY - -; Custom Dialogs - !include "dialogs\setuptype.nsdinc" - !include "dialogs\service-config.nsdinc" - !include "dialogs\confirmation.nsdinc" - -; Select service account type - #!define MUI_PAGE_CUSTOMFUNCTION_PRE HideServiceConfigPage ; Controls when to hide / show (This does not work for Page, might need to go PageEx) - #!define MUI_PAGE_CUSTOMFUNCTION_SHOW fnc_service_config_Show - #!define MUI_PAGE_CUSTOMFUNCTION_LEAVE ServiceConfigPage_Config - #!insertmacro MUI_PAGE_CUSTOM ServiceAccountType - Page custom ShowServiceConfigPage ServiceConfigPage_Config - -; Confirmation Page - Page custom ShowConfirmationPage ; just letting the user know what they chose to install - -; Actual Installion Page - !insertmacro MUI_PAGE_INSTFILES - - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - #!insertmacro MUI_UNPAGE_FINISH - -;-------------------------------- -;Languages; Add more languages later here if needed - !insertmacro MUI_LANGUAGE "English" - -;-------------------------------- -;Installer Sections -Section "!Jellyfin Server (required)" InstallJellyfinServer - SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer. - - StrCmp "$_EXISTINGINSTALLATION_" "Yes" RunUninstaller CarryOn ; Silently uninstall in case of previous installation - - RunUninstaller: - DetailPrint "Looking for uninstaller at $INSTDIR" - FindFirst $0 $1 "$INSTDIR\Uninstall.exe" - FindClose $0 - StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found - - DetailPrint "Silently running the uninstaller at $INSTDIR" - ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0 - DetailPrint "Uninstall finished, $0" - - CarryOn: - ${If} $_EXISTINGSERVICE_ == 'Yes' - ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0 - ${If} $0 <> 0 - MessageBox MB_OK|MB_ICONSTOP "Could not stop the Jellyfin Server service." - Abort - ${EndIf} - DetailPrint "Stopped Jellyfin Server service, $0" - ${EndIf} - - SetOutPath "$INSTDIR" - - File "/oname=icon.ico" "${UXPATH}\branding\NSIS\modern-install.ico" - File /r $%InstallLocation%\* - - -; Write the InstallFolder, DataFolder, Network Service info into the registry for later use - WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR" - WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "DataFolder" "$_JELLYFINDATADIR_" - WriteRegStr HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" "$_SERVICEACCOUNTTYPE_" - - !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ - StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ; - -; Write the uninstall keys for Windows - WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin Server $_JELLYFINVERSION_ ${NAMESUFFIX}" - WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"' - WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0' - WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project" - WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/" - WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_" - WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1 - WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1 - -;Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" -SectionEnd - -Section "Jellyfin Server Service" InstallService -${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service! - ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 - DetailPrint "Jellyfin Server service statuscode, $0" - ${If} $0 == 0 - InstallRetry: - ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --service --datadir \"$_JELLYFINDATADIR_\"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry - ${EndIf} - DetailPrint "Jellyfin Server Service install, $0" - ${Else} - DetailPrint "Jellyfin Server Service exists, updating..." - - ConfigureApplicationRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Application "$INSTDIR\jellyfin.exe"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureApplicationRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (Application), $0" - - ConfigureAppParametersRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --service --datadir \"$_JELLYFINDATADIR_\"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (AppParameters), $0" - ${EndIf} - - - Sleep 3000 ; Give time for Windows to catchup - ConfigureStartRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Start SERVICE_DELAYED_AUTO_START' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureStartRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (Start), $0" - - ConfigureDescriptionRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Description "Jellyfin Server: The Free Software Media System"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDescriptionRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (Description), $0" - ConfigureDisplayNameRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer DisplayName "Jellyfin Server"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDisplayNameRetry - - ${EndIf} - DetailPrint "Jellyfin Server Service setting (DisplayName), $0" - - Sleep 3000 - ${If} $_SERVICEACCOUNTTYPE_ == "NetworkService" ; the default install using NSSM is Local System - ConfigureNetworkServiceRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Objectname "Network Service"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service account." ConfigureNetworkServiceRetry - ${EndIf} - DetailPrint "Jellyfin Server service account change, $0" - ${EndIf} - - Sleep 3000 - ConfigureDefaultAppExit: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppExit Default Exit' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service app exit action." ConfigureDefaultAppExit - ${EndIf} - DetailPrint "Jellyfin Server service exit action set, $0" -${EndIf} - -SectionEnd - -Section "-start service" StartService -${If} $_SERVICESTART_ == "Yes" -${AndIf} $_INSTALLSERVICE_ == "Yes" - StartRetry: - ExecWait '"$INSTDIR\nssm.exe" start JellyfinServer' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not start the Jellyfin Server service." StartRetry - ${EndIf} - DetailPrint "Jellyfin Server service start, $0" -${EndIf} -SectionEnd - -Section "Create Shortcuts" CreateWinShortcuts - ${If} $_MAKESHORTCUTS_ == "Yes" - CreateDirectory "$SMPROGRAMS\Jellyfin Server" - CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin (View Console).lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMAXIMIZED - CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin Tray App.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0 - ;CreateShortCut "$DESKTOP\Jellyfin Server.lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMINIMIZED - CreateShortCut "$DESKTOP\Jellyfin Server\Jellyfin Server.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0 - ${EndIf} -SectionEnd - -;-------------------------------- -;Descriptions - -;Language strings - LangString DESC_InstallJellyfinServer ${LANG_ENGLISH} "Install Jellyfin Server" - LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service" - -;Assign language strings to sections - !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfinServer} $(DESC_InstallJellyfinServer) - !insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService) - !insertmacro MUI_FUNCTION_DESCRIPTION_END - -;-------------------------------- -;Uninstaller Section - -Section "Uninstall" - - ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder" ; read the installation folder - ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; read the data folder - ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; read the account name - - DetailPrint "Jellyfin Install location: $INSTDIR" - DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_" - - MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain the Jellyfin Server data folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData - - RMDir /r /REBOOTOK "$_JELLYFINDATADIR_" - - PreserveData: - - ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 - DetailPrint "Jellyfin Server service statuscode, $0" - IntCmp $0 0 NoServiceUninstall ; service doesn't exist, may be run from desktop shortcut - - Sleep 3000 ; Give time for Windows to catchup - - UninstallStopRetry: - ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not stop the Jellyfin Server service." UninstallStopRetry - ${EndIf} - DetailPrint "Stopped Jellyfin Server service, $0" - - UninstallRemoveRetry: - ExecWait '"$INSTDIR\nssm.exe" remove JellyfinServer confirm' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not remove the Jellyfin Server service." UninstallRemoveRetry - ${EndIf} - DetailPrint "Removed Jellyfin Server service, $0" - - Sleep 3000 ; Give time for Windows to catchup - - NoServiceUninstall: ; existing install was present but no service was detected. Remove shortcuts if account is set to none - ${If} $_SERVICEACCOUNTTYPE_ == "None" - RMDir /r "$SMPROGRAMS\Jellyfin Server" - Delete "$DESKTOP\Jellyfin Server.lnk" - DetailPrint "Removed old shortcuts..." - ${EndIf} - - Delete "$INSTDIR\*.*" - RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web" - Delete "$INSTDIR\Uninstall.exe" - RMDir /r /REBOOTOK "$INSTDIR" - - DeleteRegKey HKLM "Software\Jellyfin" - DeleteRegKey HKLM "${REG_UNINST_KEY}" - -SectionEnd - -Function .onInit -; Setting up defaults - StrCpy $_INSTALLSERVICE_ "Yes" - StrCpy $_SERVICESTART_ "Yes" - StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" - StrCpy $_EXISTINGINSTALLATION_ "No" - StrCpy $_EXISTINGSERVICE_ "No" - StrCpy $_MAKESHORTCUTS_ "No" - - SetShellVarContext current - StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" - - System::Call 'kernel32::CreateMutex(p 0, i 0, t "JellyfinServerMutex") p .r1 ?e' - Pop $R0 - - StrCmp $R0 0 +3 - !insertmacro ShowErrorFinal "The installer is already running." - -;Detect if Jellyfin is already installed. -; In case it is installed, let the user choose either -; 1. Exit installer -; 2. Upgrade without messing with data -; 2a. Don't ask for any details, uninstall and install afresh with old settings - -; Read Registry for previous installation - ClearErrors - ReadRegStr "$0" HKLM "${REG_CONFIG_KEY}" "InstallFolder" - IfErrors NoExisitingInstall - - DetailPrint "Existing Jellyfin Server detected at: $0" - StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default - - StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later - SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade" - - ; check if service was run using Network Service account - ClearErrors - ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default - - ClearErrors - ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds - - ; Hide sections which will not be needed in case of previous install - ; SectionSetText ${InstallService} "" - -; check if there is a service called Jellyfin, there should be -; hack : nssm statuscode Jellyfin will return non zero return code in case it exists - ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 - DetailPrint "Jellyfin Server service statuscode, $0" - IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut - - ; if service was detected, set defaults going forward. - StrCpy $_EXISTINGSERVICE_ "Yes" - StrCpy $_INSTALLSERVICE_ "Yes" - StrCpy $_SERVICESTART_ "Yes" - StrCpy $_MAKESHORTCUTS_ "No" - SectionSetText ${CreateWinShortcuts} "" - - - NoService: ; existing install was present but no service was detected - ${If} $_SERVICEACCOUNTTYPE_ == "None" - StrCpy $_SETUPTYPE_ "Basic" - StrCpy $_INSTALLSERVICE_ "No" - StrCpy $_SERVICESTART_ "No" - StrCpy $_MAKESHORTCUTS_ "Yes" - ${EndIf} - -; Let the user know that we'll upgrade and provide an option to quit. - MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \ - $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade - Quit ; Quit if the user is not sure about upgrade - - ProceedWithUpgrade: - - NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details - -FunctionEnd - -Function HideInstallDirectoryPage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideDataDirectoryPage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideServiceConfigPage - ${If} $_INSTALLSERVICE_ == "No" ; Not running as a service, don't ask for service type - ${OrIf} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideConfirmationPage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideSetupTypePage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType - Abort - ${EndIf} -FunctionEnd - -Function HideComponentsPage - ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice - Abort - ${EndIf} -FunctionEnd - -; Setup Type dialog show function -Function ShowSetupTypePage - Call HideSetupTypePage - Call fnc_setuptype_Create - nsDialogs::Show -FunctionEnd - -; Service Config dialog show function -Function ShowServiceConfigPage - Call HideServiceConfigPage - Call fnc_service_config_Create - nsDialogs::Show -FunctionEnd - -; Confirmation dialog show function -Function ShowConfirmationPage - Call HideConfirmationPage - Call fnc_confirmation_Create - nsDialogs::Show -FunctionEnd - -; Declare temp variables to read the options from the custom page. -Var StartServiceAfterInstall -Var UseNetworkServiceAccount -Var UseLocalSystemAccount -Var BasicInstall - - -Function SetupTypePage_Config -${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall - IfFileExists "$LOCALAPPDATA\Jellyfin" folderfound foldernotfound ; if the folder exists, use this, otherwise, go with new default - folderfound: - StrCpy $_FOLDEREXISTS_ "Yes" - Goto InstallCheck - foldernotfound: - StrCpy $_FOLDEREXISTS_ "No" - Goto InstallCheck - -InstallCheck: -${If} $BasicInstall == 1 - StrCpy $_SETUPTYPE_ "Basic" - StrCpy $_INSTALLSERVICE_ "No" - StrCpy $_SERVICESTART_ "No" - StrCpy $_SERVICEACCOUNTTYPE_ "None" - StrCpy $_MAKESHORTCUTS_ "Yes" - ${If} $_FOLDEREXISTS_ == "Yes" - StrCpy $_JELLYFINDATADIR_ "$LOCALAPPDATA\Jellyfin\" - ${EndIf} -${Else} - StrCpy $_SETUPTYPE_ "Advanced" - StrCpy $_INSTALLSERVICE_ "Yes" - StrCpy $_MAKESHORTCUTS_ "No" - ${If} $_FOLDEREXISTS_ == "Yes" - MessageBox MB_OKCANCEL|MB_ICONINFORMATION "An existing data folder was detected.\ - $\r$\nBasic Setup is highly recommended.\ - $\r$\nIf you proceed, you will need to set up Jellyfin again." IDOK GoAhead IDCANCEL GoBack - GoBack: - Abort - ${EndIf} - GoAhead: - StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" - SectionSetText ${CreateWinShortcuts} "" -${EndIf} - -FunctionEnd - -Function ServiceConfigPage_Config -${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall -${If} $StartServiceAfterInstall == 1 - StrCpy $_SERVICESTART_ "Yes" -${Else} - StrCpy $_SERVICESTART_ "No" -${EndIf} -${NSD_GetState} $hCtl_service_config_UseNetworkServiceAccount $UseNetworkServiceAccount -${NSD_GetState} $hCtl_service_config_UseLocalSystemAccount $UseLocalSystemAccount - -${If} $UseNetworkServiceAccount == 1 - StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" -${ElseIf} $UseLocalSystemAccount == 1 - StrCpy $_SERVICEACCOUNTTYPE_ "LocalSystem" -${Else} - !insertmacro ShowErrorFinal "Service account type not properly configured." -${EndIf} - -FunctionEnd - -; This function handles the choices during component selection -Function .onSelChange - -; If we are not installing service, we don't need to set the NetworkService account or StartService - SectionGetFlags ${InstallService} $0 - ${If} $0 = ${SF_SELECTED} - StrCpy $_INSTALLSERVICE_ "Yes" - ${Else} - StrCpy $_INSTALLSERVICE_ "No" - StrCpy $_SERVICESTART_ "No" - StrCpy $_SERVICEACCOUNTTYPE_ "None" - ${EndIf} -FunctionEnd - -Function .onInstSuccess - #ExecShell "open" "http://localhost:8096" -FunctionEnd diff --git a/windows/legacy/install-jellyfin.ps1 b/windows/legacy/install-jellyfin.ps1 deleted file mode 100644 index e909a0468e..0000000000 --- a/windows/legacy/install-jellyfin.ps1 +++ /dev/null @@ -1,460 +0,0 @@ -[CmdletBinding()] - -param( - [Switch]$Quiet, - [Switch]$InstallAsService, - [System.Management.Automation.pscredential]$ServiceUser, - [switch]$CreateDesktopShorcut, - [switch]$LaunchJellyfin, - [switch]$MigrateEmbyLibrary, - [string]$InstallLocation, - [string]$EmbyLibraryLocation, - [string]$JellyfinLibraryLocation -) -<# This form was created using POSHGUI.com a free online gui designer for PowerShell -.NAME - Install-Jellyfin -#> - -#This doesn't need to be used by default anymore, but I am keeping it in as a function for future use. -function Elevate-Window { - # Get the ID and security principal of the current user account - $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() - $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) - - # Get the security principal for the Administrator role - $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator - - # Check to see if we are currently running "as Administrator" - if ($myWindowsPrincipal.IsInRole($adminRole)) - { - # We are running "as Administrator" - so change the title and background color to indicate this - $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)" - $Host.UI.RawUI.BackgroundColor = "DarkBlue" - clear-host - } - else - { - # We are not running "as Administrator" - so relaunch as administrator - - # Create a new process object that starts PowerShell - $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"; - - # Specify the current script path and name as a parameter - $newProcess.Arguments = $myInvocation.MyCommand.Definition; - - # Indicate that the process should be elevated - $newProcess.Verb = "runas"; - - # Start the new process - [System.Diagnostics.Process]::Start($newProcess); - - # Exit from the current, unelevated, process - exit - } -} - -#FIXME The install methods should be a function that takes all the params, the quiet flag should be a paramset - -if($Quiet.IsPresent -or $Quiet -eq $true){ - if([string]::IsNullOrEmpty($JellyfinLibraryLocation)){ - $Script:JellyfinDataDir = "$env:LOCALAPPDATA\jellyfin\" - }else{ - $Script:JellyfinDataDir = $JellyfinLibraryLocation - } - if([string]::IsNullOrEmpty($InstallLocation)){ - $Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" - }else{ - $Script:DefaultJellyfinInstallDirectory = $InstallLocation - } - - if([string]::IsNullOrEmpty($EmbyLibraryLocation)){ - $Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\data\" - }else{ - $Script:defaultEmbyDataDir = $EmbyLibraryLocation - } - - if($InstallAsService.IsPresent -or $InstallAsService -eq $true){ - $Script:InstallAsService = $true - }else{$Script:InstallAsService = $false} - if($null -eq $ServiceUser){ - $Script:InstallServiceAsUser = $false - }else{ - $Script:InstallServiceAsUser = $true - $Script:UserCredentials = $ServiceUser - $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Local\jellyfin\"} - if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false} - if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false} - if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false} - - if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){ - mkdir $Script:DefaultJellyfinInstallDirectory - } - Copy-Item -Path $PSScriptRoot/* -DestinationPath "$Script:DefaultJellyfinInstallDirectory/" -Force -Recurse - if($Script:InstallAsService){ - if($Script:InstallServiceAsUser){ - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 500 - &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - }else{ - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 500 - #&"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin ObjectName $Script:UserCredentials.UserName $Script:UserCredentials.GetNetworkCredential().Password - #Set-Service -Name Jellyfin -Credential $Script:UserCredentials - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - } - } - if($Script:MigrateLibrary){ - Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse - } - if($Script:CreateShortcut){ - $WshShell = New-Object -comObject WScript.Shell - $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk") - $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe" - $Shortcut.Save() - } - if($Script:StartJellyfin){ - if($Script:InstallAsService){ - Get-Service Jellyfin | Start-Service - }else{ - Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru - } - } -}else{ - -} -Add-Type -AssemblyName System.Windows.Forms -[System.Windows.Forms.Application]::EnableVisualStyles() - -$Script:JellyFinDataDir = "$env:LOCALAPPDATA\jellyfin\" -$Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" -$Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\" -$Script:InstallAsService = $False -$Script:InstallServiceAsUser = $false -$Script:CreateShortcut = $false -$Script:MigrateLibrary = $false -$Script:StartJellyfin = $false - -function InstallJellyfin { - Write-Host "Install as service: $Script:InstallAsService" - Write-Host "Install as serviceuser: $Script:InstallServiceAsUser" - Write-Host "Create Shortcut: $Script:CreateShortcut" - Write-Host "MigrateLibrary: $Script:MigrateLibrary" - $GUIElementsCollection | ForEach-Object { - $_.Enabled = $false - } - Write-Host "Making Jellyfin directory" - $ProgressBar.Minimum = 1 - $ProgressBar.Maximum = 100 - $ProgressBar.Value = 1 - if($Script:DefaultJellyfinInstallDirectory -ne $InstallLocationBox.Text){ - Write-Host "Custom Install Location Chosen: $($InstallLocationBox.Text)" - $Script:DefaultJellyfinInstallDirectory = $InstallLocationBox.Text - } - if($Script:JellyfinDataDir -ne $CustomLibraryBox.Text){ - Write-Host "Custom Library Location Chosen: $($CustomLibraryBox.Text)" - $Script:JellyfinDataDir = $CustomLibraryBox.Text - } - if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){ - mkdir $Script:DefaultJellyfinInstallDirectory - } - Write-Host "Copying Jellyfin Data" - $progressbar.Value = 10 - Copy-Item -Path $PSScriptRoot/* -Destination $Script:DefaultJellyfinInstallDirectory/ -Force -Recurse - Write-Host "Finished Copying" - $ProgressBar.Value = 50 - if($Script:InstallAsService){ - if($Script:InstallServiceAsUser){ - Write-Host "Installing Service as user $($Script:UserCredentials.UserName)" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 2000 - &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - }else{ - Write-Host "Installing Service as LocalSystem" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 2000 - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - } - } - $progressbar.Value = 60 - if($Script:MigrateLibrary){ - if($Script:defaultEmbyDataDir -ne $LibraryLocationBox.Text){ - Write-Host "Custom location defined for emby library: $($LibraryLocationBox.Text)" - $Script:defaultEmbyDataDir = $LibraryLocationBox.Text - } - Write-Host "Copying emby library from $Script:defaultEmbyDataDir to $Script:JellyFinDataDir" - Write-Host "This could take a while depending on the size of your library. Please be patient" - Write-Host "Copying config" - Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying cache" - Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying data" - Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying metadata" - Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying root dir" - Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse - } - $progressbar.Value = 80 - if($Script:CreateShortcut){ - Write-Host "Creating Shortcut" - $WshShell = New-Object -comObject WScript.Shell - $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk") - $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe" - $Shortcut.Save() - } - $ProgressBar.Value = 90 - if($Script:StartJellyfin){ - if($Script:InstallAsService){ - Write-Host "Starting Jellyfin Service" - Get-Service Jellyfin | Start-Service - }else{ - Write-Host "Starting Jellyfin" - Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru - } - } - $progressbar.Value = 100 - Write-Host Finished - $wshell = New-Object -ComObject Wscript.Shell - $wshell.Popup("Operation Completed",0,"Done",0x1) - $InstallForm.Close() -} -function ServiceBoxCheckChanged { - if($InstallAsServiceCheck.Checked){ - $Script:InstallAsService = $true - $ServiceUserLabel.Visible = $true - $ServiceUserLabel.Enabled = $true - $ServiceUserBox.Visible = $true - $ServiceUserBox.Enabled = $true - }else{ - $Script:InstallAsService = $false - $ServiceUserLabel.Visible = $false - $ServiceUserLabel.Enabled = $false - $ServiceUserBox.Visible = $false - $ServiceUserBox.Enabled = $false - } -} -function UserSelect { - if($ServiceUserBox.Text -eq 'Local System') - { - $Script:InstallServiceAsUser = $false - $Script:UserCredentials = $null - $ServiceUserBox.Items.RemoveAt(1) - $ServiceUserBox.Items.Add("Custom User") - }elseif($ServiceUserBox.Text -eq 'Custom User'){ - $Script:InstallServiceAsUser = $true - $Script:UserCredentials = Get-Credential -Message "Please enter the credentials of the user you with to run Jellyfin Service as" -UserName $env:USERNAME - $ServiceUserBox.Items[1] = "$($Script:UserCredentials.UserName)" - } -} -function CreateShortcutBoxCheckChanged { - if($CreateShortcutCheck.Checked){ - $Script:CreateShortcut = $true - }else{ - $Script:CreateShortcut = $False - } -} -function StartJellyFinBoxCheckChanged { - if($StartProgramCheck.Checked){ - $Script:StartJellyfin = $true - }else{ - $Script:StartJellyfin = $false - } -} - -function CustomLibraryCheckChanged { - if($CustomLibraryCheck.Checked){ - $Script:UseCustomLibrary = $true - $CustomLibraryBox.Enabled = $true - }else{ - $Script:UseCustomLibrary = $false - $CustomLibraryBox.Enabled = $false - } -} - -function MigrateLibraryCheckboxChanged { - - if($MigrateLibraryCheck.Checked){ - $Script:MigrateLibrary = $true - $LibraryMigrationLabel.Visible = $true - $LibraryMigrationLabel.Enabled = $true - $LibraryLocationBox.Visible = $true - $LibraryLocationBox.Enabled = $true - }else{ - $Script:MigrateLibrary = $false - $LibraryMigrationLabel.Visible = $false - $LibraryMigrationLabel.Enabled = $false - $LibraryLocationBox.Visible = $false - $LibraryLocationBox.Enabled = $false - } - -} - - -#region begin GUI{ - -$InstallForm = New-Object system.Windows.Forms.Form -$InstallForm.ClientSize = '320,240' -$InstallForm.text = "Terrible Jellyfin Installer" -$InstallForm.TopMost = $false - -$GUIElementsCollection = @() - -$InstallButton = New-Object system.Windows.Forms.Button -$InstallButton.text = "Install" -$InstallButton.width = 60 -$InstallButton.height = 30 -$InstallButton.location = New-Object System.Drawing.Point(5,5) -$InstallButton.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallButton - -$ProgressBar = New-Object system.Windows.Forms.ProgressBar -$ProgressBar.width = 245 -$ProgressBar.height = 30 -$ProgressBar.location = New-Object System.Drawing.Point(70,5) - -$InstallLocationLabel = New-Object system.Windows.Forms.Label -$InstallLocationLabel.text = "Install Location" -$InstallLocationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$InstallLocationLabel.AutoSize = $true -$InstallLocationLabel.width = 100 -$InstallLocationLabel.height = 20 -$InstallLocationLabel.location = New-Object System.Drawing.Point(5,50) -$InstallLocationLabel.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallLocationLabel - -$InstallLocationBox = New-Object system.Windows.Forms.TextBox -$InstallLocationBox.multiline = $false -$InstallLocationBox.width = 205 -$InstallLocationBox.height = 20 -$InstallLocationBox.location = New-Object System.Drawing.Point(110,50) -$InstallLocationBox.Text = $Script:DefaultJellyfinInstallDirectory -$InstallLocationBox.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallLocationBox - -$CustomLibraryCheck = New-Object system.Windows.Forms.CheckBox -$CustomLibraryCheck.text = "Custom Library Location:" -$CustomLibraryCheck.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$CustomLibraryCheck.AutoSize = $false -$CustomLibraryCheck.width = 180 -$CustomLibraryCheck.height = 20 -$CustomLibraryCheck.location = New-Object System.Drawing.Point(5,75) -$CustomLibraryCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $CustomLibraryCheck - -$CustomLibraryBox = New-Object system.Windows.Forms.TextBox -$CustomLibraryBox.multiline = $false -$CustomLibraryBox.width = 130 -$CustomLibraryBox.height = 20 -$CustomLibraryBox.location = New-Object System.Drawing.Point(185,75) -$CustomLibraryBox.Text = $Script:JellyFinDataDir -$CustomLibraryBox.Font = 'Microsoft Sans Serif,10' -$CustomLibraryBox.Enabled = $false -$GUIElementsCollection += $CustomLibraryBox - -$InstallAsServiceCheck = New-Object system.Windows.Forms.CheckBox -$InstallAsServiceCheck.text = "Install as Service" -$InstallAsServiceCheck.AutoSize = $false -$InstallAsServiceCheck.width = 140 -$InstallAsServiceCheck.height = 20 -$InstallAsServiceCheck.location = New-Object System.Drawing.Point(5,125) -$InstallAsServiceCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallAsServiceCheck - -$ServiceUserLabel = New-Object system.Windows.Forms.Label -$ServiceUserLabel.text = "Run Service As:" -$ServiceUserLabel.AutoSize = $true -$ServiceUserLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$ServiceUserLabel.width = 100 -$ServiceUserLabel.height = 20 -$ServiceUserLabel.location = New-Object System.Drawing.Point(15,145) -$ServiceUserLabel.Font = 'Microsoft Sans Serif,10' -$ServiceUserLabel.Visible = $false -$ServiceUserLabel.Enabled = $false -$GUIElementsCollection += $ServiceUserLabel - -$ServiceUserBox = New-Object system.Windows.Forms.ComboBox -$ServiceUserBox.text = "Run Service As" -$ServiceUserBox.width = 195 -$ServiceUserBox.height = 20 -@('Local System','Custom User') | ForEach-Object {[void] $ServiceUserBox.Items.Add($_)} -$ServiceUserBox.location = New-Object System.Drawing.Point(120,145) -$ServiceUserBox.Font = 'Microsoft Sans Serif,10' -$ServiceUserBox.Visible = $false -$ServiceUserBox.Enabled = $false -$ServiceUserBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList -$GUIElementsCollection += $ServiceUserBox - -$MigrateLibraryCheck = New-Object system.Windows.Forms.CheckBox -$MigrateLibraryCheck.text = "Import Emby/Old JF Library" -$MigrateLibraryCheck.AutoSize = $false -$MigrateLibraryCheck.width = 160 -$MigrateLibraryCheck.height = 20 -$MigrateLibraryCheck.location = New-Object System.Drawing.Point(5,170) -$MigrateLibraryCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $MigrateLibraryCheck - -$LibraryMigrationLabel = New-Object system.Windows.Forms.Label -$LibraryMigrationLabel.text = "Emby/Old JF Library Path" -$LibraryMigrationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$LibraryMigrationLabel.AutoSize = $false -$LibraryMigrationLabel.width = 120 -$LibraryMigrationLabel.height = 20 -$LibraryMigrationLabel.location = New-Object System.Drawing.Point(15,190) -$LibraryMigrationLabel.Font = 'Microsoft Sans Serif,10' -$LibraryMigrationLabel.Visible = $false -$LibraryMigrationLabel.Enabled = $false -$GUIElementsCollection += $LibraryMigrationLabel - -$LibraryLocationBox = New-Object system.Windows.Forms.TextBox -$LibraryLocationBox.multiline = $false -$LibraryLocationBox.width = 175 -$LibraryLocationBox.height = 20 -$LibraryLocationBox.location = New-Object System.Drawing.Point(140,190) -$LibraryLocationBox.Text = $Script:defaultEmbyDataDir -$LibraryLocationBox.Font = 'Microsoft Sans Serif,10' -$LibraryLocationBox.Visible = $false -$LibraryLocationBox.Enabled = $false -$GUIElementsCollection += $LibraryLocationBox - -$CreateShortcutCheck = New-Object system.Windows.Forms.CheckBox -$CreateShortcutCheck.text = "Desktop Shortcut" -$CreateShortcutCheck.AutoSize = $false -$CreateShortcutCheck.width = 150 -$CreateShortcutCheck.height = 20 -$CreateShortcutCheck.location = New-Object System.Drawing.Point(5,215) -$CreateShortcutCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $CreateShortcutCheck - -$StartProgramCheck = New-Object system.Windows.Forms.CheckBox -$StartProgramCheck.text = "Start Jellyfin" -$StartProgramCheck.AutoSize = $false -$StartProgramCheck.width = 160 -$StartProgramCheck.height = 20 -$StartProgramCheck.location = New-Object System.Drawing.Point(160,215) -$StartProgramCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $StartProgramCheck - -$InstallForm.controls.AddRange($GUIElementsCollection) -$InstallForm.Controls.Add($ProgressBar) - -#region gui events { -$InstallButton.Add_Click({ InstallJellyfin }) -$CustomLibraryCheck.Add_CheckedChanged({CustomLibraryCheckChanged}) -$InstallAsServiceCheck.Add_CheckedChanged({ServiceBoxCheckChanged}) -$ServiceUserBox.Add_SelectedValueChanged({ UserSelect }) -$MigrateLibraryCheck.Add_CheckedChanged({MigrateLibraryCheckboxChanged}) -$CreateShortcutCheck.Add_CheckedChanged({CreateShortcutBoxCheckChanged}) -$StartProgramCheck.Add_CheckedChanged({StartJellyFinBoxCheckChanged}) -#endregion events } - -#endregion GUI } - - -[void]$InstallForm.ShowDialog() diff --git a/windows/legacy/install.bat b/windows/legacy/install.bat deleted file mode 100644 index e21479a79a..0000000000 --- a/windows/legacy/install.bat +++ /dev/null @@ -1 +0,0 @@ -powershell.exe -executionpolicy Bypass -file install-jellyfin.ps1 From ec0ff5d02fa53ca5b902d0bd5b477199170f3d28 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Sat, 3 Oct 2020 12:40:28 +0200 Subject: [PATCH 088/153] test: use descriptive test method names --- .../ModelBinders/CommaDelimitedArrayModelBinderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs index b05be6a16a..c801b4a523 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -93,7 +93,7 @@ namespace Jellyfin.Api.Tests.ModelBinders } [Fact] - public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery2() + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas() { var queryParamName = "test"; var queryParamValues = new TestType[] { TestType.How, TestType.Much }; @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Tests.ModelBinders } [Fact] - public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery2() + public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() { var queryParamName = "test"; var queryParamValues = Array.Empty(); From 211c9cd60850c6c33d1211cc5a7e35a94b19bab4 Mon Sep 17 00:00:00 2001 From: KonH Date: Sat, 3 Oct 2020 22:03:23 +0700 Subject: [PATCH 089/153] Remove unnecessary null checks in some places Related to https://github.com/jellyfin/jellyfin/issues/2149 --- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 5 +---- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 1207fb5134..e78f63b256 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -554,7 +554,7 @@ namespace Jellyfin.Api.Helpers private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress) { var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; + var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; if (remoteClientMaxBitrate <= 0) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 64d1227f7c..0db1fabffe 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -740,10 +740,7 @@ namespace Jellyfin.Api.Helpers /// The state. private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state) { - if (job != null) - { - job.HasExited = true; - } + job.HasExited = true; _logger.LogDebug("Disposing stream resources"); state.Dispose(); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8f04baa089..dfd7ee99da 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -799,7 +799,7 @@ namespace Jellyfin.Server.Implementations.Users private IList GetPasswordResetProviders(User user) { - var passwordResetProviderId = user?.PasswordResetProviderId; + var passwordResetProviderId = user.PasswordResetProviderId; var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); if (!string.IsNullOrEmpty(passwordResetProviderId)) From e01209a6f5305084fa4857cf634a0287fd916fd3 Mon Sep 17 00:00:00 2001 From: Maxr1998 Date: Sat, 3 Oct 2020 17:14:09 +0200 Subject: [PATCH 090/153] Log stream type and codec for missing direct play profile --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index d9e7e4fbb4..fc0aad0727 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -455,9 +455,10 @@ namespace MediaBrowser.Model.Dlna if (directPlayProfile == null) { - _logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}", + _logger.LogInformation("Profile: {0}, No audio direct play profiles found for {1} with codec {2}", options.Profile.Name ?? "Unknown Profile", - item.Path ?? "Unknown path"); + item.Path ?? "Unknown path", + audioStream.Codec ?? "Unknown codec"); return (Enumerable.Empty(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); } @@ -972,9 +973,10 @@ namespace MediaBrowser.Model.Dlna if (directPlay == null) { - _logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}", + _logger.LogInformation("Profile: {0}, No video direct play profiles found for {1} with codec {2}", profile.Name ?? "Unknown Profile", - mediaSource.Path ?? "Unknown path"); + mediaSource.Path ?? "Unknown path", + videoStream.Codec ?? "Unknown codec"); return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); } From e9524f89d63894ea9af62cd1b61ddd89cb8b9e82 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 24 Sep 2020 19:49:35 +0200 Subject: [PATCH 091/153] Migrate the TMDb providers to the TMDbLib library --- .../ApplicationHost.cs | 2 + .../Extensions/EnumerableExtensions.cs | 46 ++ .../MediaBrowser.Providers.csproj | 1 + .../Tmdb/BoxSets/TmdbBoxSetImageProvider.cs | 143 ++--- .../Tmdb/BoxSets/TmdbBoxSetProvider.cs | 260 ++------ .../Models/Collections/CollectionImages.cs | 14 - .../Models/Collections/CollectionResult.cs | 23 - .../Plugins/Tmdb/Models/Collections/Part.cs | 17 - .../Plugins/Tmdb/Models/General/Backdrop.cs | 21 - .../Plugins/Tmdb/Models/General/Crew.cs | 19 - .../Tmdb/Models/General/ExternalIds.cs | 17 - .../Plugins/Tmdb/Models/General/Genre.cs | 11 - .../Plugins/Tmdb/Models/General/Images.cs | 13 - .../Plugins/Tmdb/Models/General/Keyword.cs | 11 - .../Plugins/Tmdb/Models/General/Keywords.cs | 11 - .../Plugins/Tmdb/Models/General/Poster.cs | 21 - .../Plugins/Tmdb/Models/General/Profile.cs | 17 - .../Plugins/Tmdb/Models/General/Still.cs | 23 - .../Tmdb/Models/General/StillImages.cs | 11 - .../Plugins/Tmdb/Models/General/Video.cs | 23 - .../Plugins/Tmdb/Models/General/Videos.cs | 11 - .../Tmdb/Models/Movies/BelongsToCollection.cs | 15 - .../Plugins/Tmdb/Models/Movies/Cast.cs | 19 - .../Plugins/Tmdb/Models/Movies/Casts.cs | 14 - .../Plugins/Tmdb/Models/Movies/Country.cs | 15 - .../Plugins/Tmdb/Models/Movies/MovieResult.cs | 80 --- .../Tmdb/Models/Movies/ProductionCompany.cs | 11 - .../Tmdb/Models/Movies/ProductionCountry.cs | 11 - .../Plugins/Tmdb/Models/Movies/Releases.cs | 11 - .../Tmdb/Models/Movies/SpokenLanguage.cs | 11 - .../Plugins/Tmdb/Models/Movies/Trailers.cs | 11 - .../Plugins/Tmdb/Models/Movies/Youtube.cs | 13 - .../Tmdb/Models/People/PersonImages.cs | 12 - .../Tmdb/Models/People/PersonResult.cs | 38 -- .../Models/Search/ExternalIdLookupResult.cs | 11 - .../Plugins/Tmdb/Models/Search/MovieResult.cs | 78 --- .../Tmdb/Models/Search/PersonSearchResult.cs | 31 - .../Tmdb/Models/Search/TmdbSearchResult.cs | 33 - .../Plugins/Tmdb/Models/Search/TvResult.cs | 25 - .../Plugins/Tmdb/Models/TV/Cast.cs | 19 - .../Plugins/Tmdb/Models/TV/ContentRating.cs | 11 - .../Plugins/Tmdb/Models/TV/ContentRatings.cs | 11 - .../Plugins/Tmdb/Models/TV/CreatedBy.cs | 13 - .../Plugins/Tmdb/Models/TV/Credits.cs | 14 - .../Plugins/Tmdb/Models/TV/Episode.cs | 23 - .../Plugins/Tmdb/Models/TV/EpisodeCredits.cs | 16 - .../Plugins/Tmdb/Models/TV/EpisodeResult.cs | 38 -- .../Plugins/Tmdb/Models/TV/GuestStar.cs | 19 - .../Plugins/Tmdb/Models/TV/Network.cs | 11 - .../Plugins/Tmdb/Models/TV/Season.cs | 17 - .../Plugins/Tmdb/Models/TV/SeasonImages.cs | 12 - .../Plugins/Tmdb/Models/TV/SeasonResult.cs | 33 - .../Plugins/Tmdb/Models/TV/SeriesResult.cs | 71 --- .../Tmdb/Movies/GenericTmdbMovieInfo.cs | 309 ---------- .../Plugins/Tmdb/Movies/TmdbImageProvider.cs | 212 ------- .../Plugins/Tmdb/Movies/TmdbImageSettings.cs | 22 - .../Tmdb/Movies/TmdbMovieExternalId.cs | 3 +- .../Tmdb/Movies/TmdbMovieImageProvider.cs | 128 ++++ .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 574 ++++++++---------- .../Plugins/Tmdb/Movies/TmdbSearch.cs | 302 --------- .../Tmdb/Music/TmdbMusicVideoProvider.cs | 34 -- .../Tmdb/People/TmdbPersonImageProvider.cs | 104 +--- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 248 ++------ .../Tmdb/TV/TmdbEpisodeImageProvider.cs | 112 ++-- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 277 +++++---- .../Tmdb/TV/TmdbEpisodeProviderBase.cs | 156 ----- .../Tmdb/TV/TmdbSeasonImageProvider.cs | 112 +--- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 259 +++----- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 165 ++--- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 525 ++++++---------- .../Plugins/Tmdb/TmdbClientManager.cs | 469 ++++++++++++++ .../Plugins/Tmdb/TmdbUtils.cs | 90 ++- .../Tmdb/Trailers/TmdbTrailerProvider.cs | 43 -- 73 files changed, 1677 insertions(+), 3929 deletions(-) create mode 100644 MediaBrowser.Model/Extensions/EnumerableExtensions.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs create mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs create mode 100644 MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7a46fdf2e7..0c8b0339bd 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -97,6 +97,7 @@ using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; +using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Mvc; @@ -537,6 +538,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(_networkManager); diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000000..712fa381ee --- /dev/null +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Model.Extensions +{ + /// + /// Extension methods for . + /// + public static class EnumerableExtensions + { + /// + /// Orders by requested language in descending order, prioritizing "en" over other non-matches. + /// + /// The remote image infos. + /// The requested language for the images. + /// The ordered remote image infos. + public static IEnumerable OrderByLanguageDescending(this IEnumerable remoteImageInfos, string requestedLanguage) + { + var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase); + + return remoteImageInfos.OrderByDescending(i => + { + if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + + if (!isRequestedLanguageEn && string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + + if (string.IsNullOrEmpty(i.Language)) + { + return isRequestedLanguageEn ? 3 : 2; + } + + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0); + } + } +} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 813dd441f5..11e30940fd 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -21,6 +21,7 @@ + diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index f6592afe46..df1e12240d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -12,25 +13,25 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory) + public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } - public string Name => ProviderName; + public string Name => TmdbUtils.ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + public int Order => 0; public bool Supports(BaseItem item) { @@ -48,112 +49,60 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (!string.IsNullOrEmpty(tmdbId)) + if (tmdbId <= 0) { - var language = item.GetPreferredMetadataLanguage(); - - var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false); - - if (mainResult != null) - { - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - return GetImages(mainResult, language, tmdbImageUrl); - } + return Enumerable.Empty(); } - return new List(); - } + var language = item.GetPreferredMetadataLanguage(); - private IEnumerable GetImages(CollectionResult obj, string language, string baseUrl) - { - var list = new List(); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - var images = obj.Images ?? new CollectionImages(); - - list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo + if (collection?.Images == null) { - Url = baseUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); + return Enumerable.Empty(); + } - list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo + var remoteImages = new List(); + + for (var i = 0; i < collection.Images.Posters.Count; i++) { - Url = baseUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - ProviderName = Name, - Type = ImageType.Backdrop, - RatingType = RatingType.Score - })); + var poster = collection.Images.Posters[i]; + remoteImages.Add(new RemoteImageInfo + { + Url = _tmdbClientManager.GetPosterUrl(poster.FilePath), + CommunityRating = poster.VoteAverage, + VoteCount = poster.VoteCount, + Width = poster.Width, + Height = poster.Height, + Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }); + } - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => + for (var i = 0; i < collection.Images.Backdrops.Count; i++) { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + var backdrop = collection.Images.Backdrops[i]; + remoteImages.Add(new RemoteImageInfo { - return 3; - } + Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath), + CommunityRating = backdrop.VoteAverage, + VoteCount = backdrop.VoteCount, + Width = backdrop.Width, + Height = backdrop.Height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + }); + } - 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); + return remoteImages.OrderByLanguageDescending(language); } - /// - /// Gets the posters. - /// - /// The images. - /// IEnumerable{MovieDbProvider.Poster}. - private IEnumerable GetPosters(CollectionImages images) - { - return images.Posters ?? new List(); - } - - /// - /// Gets the backdrops. - /// - /// The images. - /// IEnumerable{MovieDbProvider.Backdrop}. - private IEnumerable GetBackdrops(CollectionImages images) - { - var eligibleBackdrops = images.Backdrops == null ? new List() : - images.Backdrops; - - return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) - .ThenByDescending(i => i.Vote_Count); - } - - public int Order => 0; - public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index e7328b5535..fcd8e614c1 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -3,270 +3,118 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { public class TmdbBoxSetProvider : IRemoteMetadataProvider { - private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images"; - - internal static TmdbBoxSetProvider Current; - - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; private readonly IHttpClientFactory _httpClientFactory; - private readonly ILibraryManager _libraryManager; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbBoxSetProvider( - ILogger logger, - IJsonSerializer json, - IServerConfigurationManager config, - IFileSystem fileSystem, - IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager) + public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _logger = logger; - _json = json; - _config = config; - _fileSystem = fileSystem; _httpClientFactory = httpClientFactory; - _libraryManager = libraryManager; - Current = this; + _tmdbClientManager = tmdbClientManager; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + public string Name => TmdbUtils.ProviderName; public async Task> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + var language = searchInfo.MetadataLanguage; - if (!string.IsNullOrEmpty(tmdbId)) + if (tmdbId > 0) { - await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage); - var info = _json.DeserializeFromFile(dataFilePath); - - var images = (info.Images ?? new CollectionImages()).Posters ?? new List(); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + if (collection == null) + { + return Enumerable.Empty(); + } var result = new RemoteSearchResult { - Name = info.Name, - SearchProviderName = Name, - ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) + Name = collection.Name, + SearchProviderName = Name }; - result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); + if (collection.Images != null) + { + result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath); + } + + result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); return new[] { result }; } - return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false); + + var collections = new List(); + for (var i = 0; i < collectionSearchResults.Count; i++) + { + var collection = new RemoteSearchResult + { + Name = collectionSearchResults[i].Name, + SearchProviderName = Name + }; + collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture)); + + collections.Add(collection); + } + + return collections; } public async Task> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken) { - var tmdbId = id.GetProviderId(MetadataProvider.Tmdb); - + var tmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); + var language = id.MetadataLanguage; // We don't already have an Id, need to fetch it - if (string.IsNullOrEmpty(tmdbId)) + if (tmdbId <= 0) { - var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false); + var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false); - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) + if (searchResults != null && searchResults.Count > 0) { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); + tmdbId = searchResults[0].Id; } } var result = new MetadataResult(); - if (!string.IsNullOrEmpty(tmdbId)) + if (tmdbId > 0) { - var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - if (mainResult != null) + if (collection != null) { + var item = new BoxSet + { + Name = collection.Name, + Overview = collection.Overview + }; + + item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); + result.HasMetadata = true; - result.Item = GetItem(mainResult); + result.Item = item; } } return result; } - internal async Task GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language); - - if (!string.IsNullOrEmpty(dataFilePath)) - { - return _json.DeserializeFromFile(dataFilePath); - } - - return null; - } - - private BoxSet GetItem(CollectionResult obj) - { - var item = new BoxSet - { - Name = obj.Name, - Overview = obj.Overview - }; - - item.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture)); - - return item; - } - - private async Task DownloadInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(tmdbId, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult == null) - { - return; - } - - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _json.SerializeToFile(mainResult, dataFilePath); - } - - private async Task FetchMainResult(string id, string language, CancellationToken cancellationToken) - { - var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); - - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var mainResult = await _json.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - if (mainResult != null && string.IsNullOrEmpty(mainResult.Name)) - { - if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) - { - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); - } - - using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - mainResult = await _json.DeserializeFromStreamAsync(langStream).ConfigureAwait(false); - } - } - - return mainResult; - } - - internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken); - } - - public string Name => TmdbUtils.ProviderName; - - private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage) - { - var path = GetDataPath(appPaths, tmdbId); - - var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty); - - return Path.Combine(path, filename); - } - - private static string GetDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetCollectionsDataPath(appPaths); - - return Path.Combine(dataPath, tmdbId); - } - - private static string GetCollectionsDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-collections"); - - return dataPath; - } - public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs deleted file mode 100644 index 0a8994d540..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections -{ - public class CollectionImages - { - public List Backdrops { get; set; } - - public List Posters { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs deleted file mode 100644 index c6b851c237..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections -{ - public class CollectionResult - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Overview { get; set; } - - public string Poster_Path { get; set; } - - public string Backdrop_Path { get; set; } - - public List Parts { get; set; } - - public CollectionImages Images { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs deleted file mode 100644 index a48124b3e1..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections -{ - public class Part - { - public string Title { get; set; } - - public int Id { get; set; } - - public string Release_Date { get; set; } - - public string Poster_Path { get; set; } - - public string Backdrop_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs deleted file mode 100644 index 5b7627f6e8..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Backdrop - { - public double Aspect_Ratio { get; set; } - - public string File_Path { get; set; } - - public int Height { get; set; } - - public string Iso_639_1 { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public int Width { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs deleted file mode 100644 index 339ecb6285..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Crew - { - public int Id { get; set; } - - public string Credit_Id { get; set; } - - public string Name { get; set; } - - public string Department { get; set; } - - public string Job { get; set; } - - public string Profile_Path { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs deleted file mode 100644 index aac4420e8b..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class ExternalIds - { - public string Imdb_Id { get; set; } - - public object Freebase_Id { get; set; } - - public string Freebase_Mid { get; set; } - - public int? Tvdb_Id { get; set; } - - public int? Tvrage_Id { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs deleted file mode 100644 index 9ba1c15c65..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Genre - { - public int Id { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs deleted file mode 100644 index 0538cf174d..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Images - { - public List Backdrops { get; set; } - - public List Posters { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs deleted file mode 100644 index fff86931be..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Keyword - { - public int Id { get; set; } - - public string Name { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs deleted file mode 100644 index 235ecb5682..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Keywords - { - public List Results { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs deleted file mode 100644 index 4f61e978be..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Poster - { - public double Aspect_Ratio { get; set; } - - public string File_Path { get; set; } - - public int Height { get; set; } - - public string Iso_639_1 { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public int Width { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs deleted file mode 100644 index 0a1f8843eb..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Profile - { - public string File_Path { get; set; } - - public int Width { get; set; } - - public int Height { get; set; } - - public object Iso_639_1 { get; set; } - - public double Aspect_Ratio { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs deleted file mode 100644 index 61de819b93..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Still - { - public double Aspect_Ratio { get; set; } - - public string File_Path { get; set; } - - public int Height { get; set; } - - public string Id { get; set; } - - public string Iso_639_1 { get; set; } - - public double Vote_Average { get; set; } - - public int Vote_Count { get; set; } - - public int Width { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs deleted file mode 100644 index 59ab18b7bf..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class StillImages - { - public List Stills { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs deleted file mode 100644 index ebd5c7acee..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Video - { - public string Id { get; set; } - - public string Iso_639_1 { get; set; } - - public string Iso_3166_1 { get; set; } - - public string Key { get; set; } - - public string Name { get; set; } - - public string Site { get; set; } - - public string Size { get; set; } - - public string Type { get; set; } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs deleted file mode 100644 index 1c673fdbd6..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General -{ - public class Videos - { - public IReadOnlyList public class TmdbMovieProvider : IRemoteMetadataProvider, IHasOrder { - private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}"; - private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers"; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _configurationManager; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - private readonly IApplicationHost _appHost; + private readonly TmdbClientManager _tmdbClientManager; - /// - /// The _TMDB settings task. - /// - private TmdbSettingsResult _tmdbSettings; - - public TmdbMovieProvider( - IJsonSerializer jsonSerializer, - IHttpClientFactory httpClientFactory, - IFileSystem fileSystem, - IServerConfigurationManager configurationManager, - ILogger logger, - ILibraryManager libraryManager, - IApplicationHost appHost) - { - _jsonSerializer = jsonSerializer; - _httpClientFactory = httpClientFactory; - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _logger = logger; - _libraryManager = libraryManager; - _appHost = appHost; - Current = this; - } - - internal static TmdbMovieProvider Current { get; private set; } - - /// public string Name => TmdbUtils.ProviderName; /// public int Order => 1; - public Task> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) + internal static TmdbMovieProvider Current { get; private set; } + + public TmdbMovieProvider( + ILogger logger, + ILibraryManager libraryManager, + TmdbClientManager tmdbClientManager, + IHttpClientFactory httpClientFactory) { - return GetMovieSearchResults(searchInfo, cancellationToken); + _logger = logger; + _libraryManager = libraryManager; + _tmdbClientManager = tmdbClientManager; + _httpClientFactory = httpClientFactory; + Current = this; } - public async Task> GetMovieSearchResults(ItemLookupInfo searchInfo, CancellationToken cancellationToken) + public async Task> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (!string.IsNullOrEmpty(tmdbId)) + if (tmdbId == 0) { - cancellationToken.ThrowIfCancellationRequested(); - - await EnsureMovieInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); - - var obj = _jsonSerializer.DeserializeFromFile(dataFilePath); - - var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - var remoteResult = new RemoteSearchResult + var movieResults = await _tmdbClientManager + .SearchMovieAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + var remoteSearchResults = new List(); + for (var i = 0; i < movieResults.Count; i++) { - Name = obj.GetTitle(), - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path + var movieResult = movieResults[i]; + var remoteSearchResult = new RemoteSearchResult + { + Name = movieResult.Title ?? movieResult.OriginalTitle, + ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath), + Overview = movieResult.Overview, + SearchProviderName = Name + }; + + var releaseDate = movieResult.ReleaseDate?.ToUniversalTime(); + remoteSearchResult.PremiereDate = releaseDate; + remoteSearchResult.ProductionYear = releaseDate?.Year; + + remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture)); + remoteSearchResults.Add(remoteSearchResult); + } + + return remoteSearchResults; + } + + var movie = await _tmdbClientManager + .GetMovieAsync(tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + + var remoteResult = new RemoteSearchResult + { + Name = movie.Title ?? movie.OriginalTitle, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath), + Overview = movie.Overview + }; + + if (movie.ReleaseDate != null) + { + var releaseDate = movie.ReleaseDate.Value.ToUniversalTime(); + remoteResult.PremiereDate = releaseDate; + remoteResult.ProductionYear = releaseDate.Year; + } + + remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); + + if (!string.IsNullOrWhiteSpace(movie.ImdbId)) + { + remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId); + } + + return new[] { remoteResult }; + } + + public async Task> GetMetadata(MovieInfo info, CancellationToken cancellationToken) + { + var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); + var imdbId = info.GetProviderId(MetadataProvider.Imdb); + + if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId)) + { + // ParseName is required here. + // Caller provides the filename with extension stripped and NOT the parsed filename + var parsedName = _libraryManager.ParseName(info.Name); + var searchResults = await _tmdbClientManager.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + if (searchResults.Count > 0) + { + tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); + } + } + + if (string.IsNullOrEmpty(tmdbId)) + { + return new MetadataResult(); + } + + var movieResult = await _tmdbClientManager + .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + + var movie = new Movie + { + Name = movieResult.Title ?? movieResult.OriginalTitle, + Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), + Tagline = movieResult.Tagline, + ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray() + }; + var metadataResult = new MetadataResult + { + HasMetadata = true, + ResultLanguage = info.MetadataLanguage, + Item = movie + }; + + movie.SetProviderId(MetadataProvider.Tmdb, tmdbId); + movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); + if (movieResult.BelongsToCollection != null) + { + movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture)); + movie.CollectionName = movieResult.BelongsToCollection.Name; + } + + movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage); + + if (movieResult.Releases?.Countries != null) + { + var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList(); + + var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); + var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); + + if (ourRelease != null) + { + var ratingPrefix = string.Equals(info.MetadataCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? string.Empty : info.MetadataCountryCode + "-"; + var newRating = ratingPrefix + ourRelease.Certification; + + newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase); + + movie.OfficialRating = newRating; + } + else if (usRelease != null) + { + movie.OfficialRating = usRelease.Certification; + } + } + + movie.PremiereDate = movieResult.ReleaseDate; + movie.ProductionYear = movieResult.ReleaseDate?.Year; + + if (movieResult.ProductionCompanies != null) + { + movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name)); + } + + var genres = movieResult.Genres; + + foreach (var genre in genres.Select(g => g.Name)) + { + movie.AddGenre(genre); + } + + if (movieResult.Credits?.Cast != null) + { + // TODO configurable + foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) + { + var personInfo = new PersonInfo + { + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order + }; + + if (!string.IsNullOrWhiteSpace(actor.ProfilePath)) + { + personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath); + } + + if (actor.Id > 0) + { + personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); + } + + metadataResult.AddPerson(personInfo); + } + } + + if (movieResult.Credits?.Crew != null) + { + var keepTypes = new[] + { + PersonType.Director, + PersonType.Writer, + PersonType.Producer }; - if (!string.IsNullOrWhiteSpace(obj.Release_Date)) + foreach (var person in movieResult.Credits.Crew) { - // These dates are always in this exact format - if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r)) + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); + + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && + !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - remoteResult.PremiereDate = r.ToUniversalTime(); - remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; + continue; } + + var personInfo = new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }; + + if (!string.IsNullOrWhiteSpace(person.ProfilePath)) + { + personInfo.ImageUrl = _tmdbClientManager.GetPosterUrl(person.ProfilePath); + } + + if (person.Id > 0) + { + personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); + } + + metadataResult.AddPerson(personInfo); } + } - remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture)); - if (!string.IsNullOrWhiteSpace(obj.Imdb_Id)) + if (movieResult.Videos?.Results != null) + { + var trailers = new List(); + for (var i = 0; i < movieResult.Videos.Results.Count; i++) { - remoteResult.SetProviderId(MetadataProvider.Imdb, obj.Imdb_Id); + var video = movieResult.Videos.Results[0]; + if (!TmdbUtils.IsTrailerType(video)) + { + continue; + } + + trailers.Add(new MediaUrl + { + Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", video.Key), + Name = video.Name + }); } - return new[] { remoteResult }; + movie.RemoteTrailers = trailers; } - return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - } - - public Task> GetMetadata(MovieInfo info, CancellationToken cancellationToken) - { - return GetItemMetadata(info, cancellationToken); - } - - public Task> GetItemMetadata(ItemLookupInfo id, CancellationToken cancellationToken) - where T : BaseItem, new() - { - var movieDb = new GenericTmdbMovieInfo(_logger, _jsonSerializer, _libraryManager, _fileSystem); - - return movieDb.GetMetadata(id, cancellationToken); - } - - /// - /// Gets the TMDB settings. - /// - /// Task{TmdbSettingsResult}. - internal async Task GetTmdbSettings(CancellationToken cancellationToken) - { - if (_tmdbSettings != null) - { - return _tmdbSettings; - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey)); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - return _tmdbSettings; - } - - /// - /// Gets the movie data path. - /// - /// The app paths. - /// The TMDB id. - /// System.String. - internal static string GetMovieDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetMoviesDataPath(appPaths); - - return Path.Combine(dataPath, tmdbId); - } - - internal static string GetMoviesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-movies2"); - - return dataPath; - } - - /// - /// Downloads the movie info. - /// - /// The id. - /// The preferred metadata language. - /// The cancellation token. - /// Task. - internal async Task DownloadMovieInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(id, true, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult == null) - { - return; - } - - var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal Task EnsureMovieInfo(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - var path = GetDataFilePath(tmdbId, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadMovieInfo(tmdbId, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId); - - if (string.IsNullOrWhiteSpace(preferredLanguage)) - { - preferredLanguage = "alllang"; - } - - var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage); - - return Path.Combine(path, filename); - } - - public static string GetImageLanguagesParam(string preferredLanguage) - { - var languages = new List(); - - if (!string.IsNullOrEmpty(preferredLanguage)) - { - preferredLanguage = NormalizeLanguage(preferredLanguage); - - languages.Add(preferredLanguage); - - if (preferredLanguage.Length == 5) // like en-US - { - // Currenty, TMDB supports 2-letter language codes only - // They are planning to change this in the future, thus we're - // supplying both codes if we're having a 5-letter code. - languages.Add(preferredLanguage.Substring(0, 2)); - } - } - - languages.Add("null"); - - if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase)) - { - languages.Add("en"); - } - - return string.Join(',', languages); - } - - public static string NormalizeLanguage(string language) - { - if (!string.IsNullOrEmpty(language)) - { - // They require this to be uppercase - // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api. - // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab - var parts = language.Split('-'); - - if (parts.Length == 2) - { - language = parts[0] + "-" + parts[1].ToUpperInvariant(); - } - } - - return language; - } - - public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) - { - if (!string.IsNullOrEmpty(imageLanguage) - && !string.IsNullOrEmpty(requestLanguage) - && requestLanguage.Length > 2 - && imageLanguage.Length == 2 - && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) - { - return requestLanguage; - } - - return imageLanguage; - } - - /// - /// Fetches the main result. - /// - /// The id. - /// if set to true [is TMDB identifier]. - /// The language. - /// The cancellation token. - /// Task{CompleteMovieData}. - internal async Task FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken) - { - var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language)); - - // Get images in english and with no language - url += "&include_image_language=" + GetImageLanguagesParam(language); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - if (mainResponse.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var mainResult = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - // If the language preference isn't english, then have the overview fallback to english if it's blank - if (mainResult != null && - string.IsNullOrEmpty(mainResult.Overview) && - !string.IsNullOrEmpty(language) && - !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English..."); - - url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) - { - // Get images in english and with no language - url += "&include_image_language=" + GetImageLanguagesParam(language); - } - - using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false); - - await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var langResult = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - mainResult.Overview = langResult.Overview; - } - - return mainResult; - } - - /// - /// Gets the movie db response. - /// - /// A representing the asynchronous operation. - internal Task GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default) - { - message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent); - return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken); + return metadataResult; } /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs deleted file mode 100644 index 36a4eef8a3..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs +++ /dev/null @@ -1,302 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Movies -{ - public class TmdbSearch - { - private const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; - private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}"; - private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}"; - - private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled); - private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled); - private static readonly Regex _cleanStopWords = new Regex( - @"\b( # Start at word boundary - 19[0-9]{2}|20[0-9]{2}| # 1900-2099 - S[0-9]{2}| # Season - E[0-9]{2}| # Episode - (2160|1080|720|576|480)[ip]?| # Resolution - [xh]?264| # Encoding - (web|dvd|bd|hdtv|hd)rip| # *Rip - web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac - ).* # Match rest of string", - RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); - - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - private readonly ILibraryManager _libraryManager; - - public TmdbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager) - { - _logger = logger; - _json = json; - _libraryManager = libraryManager; - } - - public Task> GetSearchResults(SeriesInfo idInfo, CancellationToken cancellationToken) - { - return GetSearchResults(idInfo, "tv", cancellationToken); - } - - public Task> GetMovieSearchResults(ItemLookupInfo idInfo, CancellationToken cancellationToken) - { - return GetSearchResults(idInfo, "movie", cancellationToken); - } - - public Task> GetSearchResults(BoxSetInfo idInfo, CancellationToken cancellationToken) - { - return GetSearchResults(idInfo, "collection", cancellationToken); - } - - private async Task> GetSearchResults(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken) - { - var name = idInfo.Name; - var year = idInfo.Year; - - if (string.IsNullOrWhiteSpace(name)) - { - return new List(); - } - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - // ParseName is required here. - // Caller provides the filename with extension stripped and NOT the parsed filename - var parsedName = _libraryManager.ParseName(name); - var yearInName = parsedName.Year; - name = parsedName.Name; - year ??= yearInName; - - var language = idInfo.MetadataLanguage.ToLowerInvariant(); - - // Replace sequences of non-word characters with space - // TMDB expects a space separated list of words make sure that is the case - name = _cleanNonWord.Replace(name, " ").Trim(); - - _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year); - var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); - - if (results.Count == 0) - { - // try in english if wasn't before - if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); - } - } - - // TODO: retrying alternatives should be done outside the search - // provider so that the retry logic can be common for all search - // providers - if (results.Count == 0) - { - var name2 = parsedName.Name; - - // Remove things enclosed in []{}() etc - name2 = _cleanEnclosed.Replace(name2, string.Empty); - - // Replace sequences of non-word characters with space - name2 = _cleanNonWord.Replace(name2, " "); - - // Clean based on common stop words / tokens - name2 = _cleanStopWords.Replace(name2, string.Empty); - - // Trim whitespace - name2 = name2.Trim(); - - // Search again if the new name is different - if (!string.Equals(name2, name, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(name2)) - { - _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year); - results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); - - if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - // one more time, in english - results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); - } - } - } - - return results.Where(i => - { - if (year.HasValue && i.ProductionYear.HasValue) - { - // Allow one year tolerance - return Math.Abs(year.Value - i.ProductionYear.Value) <= 1; - } - - return true; - }); - } - - private Task> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken) - { - switch (type) - { - case "tv": - return GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken); - default: - return GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken); - } - } - - private async Task> GetSearchResultsGeneric(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("String can't be null or empty.", nameof(name)); - } - - string url3; - if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase)) - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrlMovieWithYear, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - year); - } - else - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrl, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - type); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var searchResults = await _json.DeserializeFromStreamAsync>(stream).ConfigureAwait(false); - - var results = searchResults.Results ?? new List(); - - return results - .Select(i => - { - var remoteResult = new RemoteSearchResult - { - SearchProviderName = TmdbMovieProvider.Current.Name, - Name = i.Title ?? i.Name ?? i.Original_Title, - ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path - }; - - if (!string.IsNullOrWhiteSpace(i.Release_Date)) - { - // These dates are always in this exact format - if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) - { - remoteResult.PremiereDate = r.ToUniversalTime(); - remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; - } - } - - remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); - - return remoteResult; - }) - .ToList(); - } - - private async Task> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("String can't be null or empty.", nameof(name)); - } - - string url3; - if (year == null) - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrl, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - "tv"); - } - else - { - url3 = string.Format( - CultureInfo.InvariantCulture, - SearchUrlTvWithYear, - WebUtility.UrlEncode(name), - TmdbUtils.ApiKey, - language, - year); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var searchResults = await _json.DeserializeFromStreamAsync>(stream).ConfigureAwait(false); - - var results = searchResults.Results ?? new List(); - - return results - .Select(i => - { - var remoteResult = new RemoteSearchResult - { - SearchProviderName = TmdbMovieProvider.Current.Name, - Name = i.Name ?? i.Original_Name, - ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path - }; - - if (!string.IsNullOrWhiteSpace(i.First_Air_Date)) - { - // These dates are always in this exact format - if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) - { - remoteResult.PremiereDate = r.ToUniversalTime(); - remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; - } - } - - remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); - - return remoteResult; - }) - .ToList(); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs deleted file mode 100644 index b88ecce87f..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Music -{ - public class TmdbMusicVideoProvider : IRemoteMetadataProvider - { - public string Name => TmdbMovieProvider.Current.Name; - - public Task> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken) - { - return TmdbMovieProvider.Current.GetItemMetadata(info, cancellationToken); - } - - public Task> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult((IEnumerable)new List()); - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index f2d2c8120e..99e4176984 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -11,31 +12,25 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.People; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.People { public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IServerConfigurationManager _config; - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + public TmdbPersonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _config = config; - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } - public static string ProviderName => TmdbUtils.ProviderName; - /// - public string Name => ProviderName; + public string Name => TmdbUtils.ProviderName; /// public int Order => 0; @@ -56,80 +51,39 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { var person = (Person)item; - var id = person.GetProviderId(MetadataProvider.Tmdb); + var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (!string.IsNullOrEmpty(id)) + if (personTmdbId > 0) { - await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false); + var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false); + if (personResult?.Images?.Profiles == null) + { + return Enumerable.Empty(); + } - var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id); + var remoteImages = new List(); + var language = item.GetPreferredMetadataLanguage(); - var result = _jsonSerializer.DeserializeFromFile(dataFilePath); + for (var i = 0; i < personResult.Images.Profiles.Count; i++) + { + var image = personResult.Images.Profiles[i]; + remoteImages.Add(new RemoteImageInfo + { + ProviderName = Name, + Type = ImageType.Primary, + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + Url = _tmdbClientManager.GetProfileUrl(image.FilePath) + }); + } - var images = result.Images ?? new PersonImages(); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - return GetImages(images, item.GetPreferredMetadataLanguage(), tmdbImageUrl); + return remoteImages.OrderByLanguageDescending(language); } return new List(); } - private IEnumerable GetImages(PersonImages images, string preferredLanguage, string baseImageUrl) - { - var list = new List(); - - if (images.Profiles != null) - { - list.AddRange(images.Profiles.Select(i => new RemoteImageInfo - { - ProviderName = Name, - Type = ImageType.Primary, - Width = i.Width, - Height = i.Height, - Language = GetLanguage(i), - Url = baseImageUrl + i.File_Path - })); - } - - var language = preferredLanguage; - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(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 string GetLanguage(Profile profile) - { - return profile.Iso_639_1?.ToString(); - } - public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 777ebce492..8c5e8590f0 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -3,198 +3,133 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.People; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.People { public class TmdbPersonProvider : IRemoteMetadataProvider { - private const string DataFileName = "info.json"; - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbPersonProvider( - IFileSystem fileSystem, - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IHttpClientFactory httpClientFactory) + public TmdbPersonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; - Current = this; + _tmdbClientManager = tmdbClientManager; } - internal static TmdbPersonProvider Current { get; private set; } - public string Name => TmdbUtils.ProviderName; public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); + var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - if (!string.IsNullOrEmpty(tmdbId)) + if (personTmdbId <= 0) { - await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false); + var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false); - var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); - var info = _jsonSerializer.DeserializeFromFile(dataFilePath); - - IReadOnlyList images = info.Images?.Profiles ?? Array.Empty(); - - var result = new RemoteSearchResult + if (personResult != null) { - Name = info.Name, + var result = new RemoteSearchResult + { + Name = personResult.Name, + SearchProviderName = Name, + Overview = personResult.Biography + }; - SearchProviderName = Name, + if (personResult.Images?.Profiles != null && personResult.Images.Profiles.Count > 0) + { + result.ImageUrl = _tmdbClientManager.GetProfileUrl(personResult.Images.Profiles[0].FilePath); + } - ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) - }; + result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); + result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); - result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); - result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id); - - return new[] { result }; + return new[] { result }; + } } + // TODO why? Because of the old rate limit? if (searchInfo.IsAutomated) { // Don't hammer moviedb searching by name - return Array.Empty(); + return new List(); } - var url = string.Format( - CultureInfo.InvariantCulture, - TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", - WebUtility.UrlEncode(searchInfo.Name), - TmdbUtils.ApiKey); + var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) + var remoteSearchResults = new List(); + for (var i = 0; i < personSearchResult.Count; i++) { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + var person = personSearchResult[i]; + var remoteSearchResult = new RemoteSearchResult + { + SearchProviderName = Name, + Name = person.Name, + ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath) + }; + + remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); + remoteSearchResults.Add(remoteSearchResult); } - var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - - var result2 = await _jsonSerializer.DeserializeFromStreamAsync>(stream).ConfigureAwait(false) - ?? new TmdbSearchResult(); - - return result2.Results.Select(i => GetSearchResult(i, tmdbImageUrl)); - } - - private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl) - { - var result = new RemoteSearchResult - { - SearchProviderName = Name, - - Name = i.Name, - - ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : baseImageUrl + i.Profile_Path - }; - - result.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); - - return result; + return remoteSearchResults; } public async Task> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken) { - var tmdbId = id.GetProviderId(MetadataProvider.Tmdb); + var personTmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); // We don't already have an Id, need to fetch it - if (string.IsNullOrEmpty(tmdbId)) + if (personTmdbId <= 0) { - tmdbId = await GetTmdbId(id, cancellationToken).ConfigureAwait(false); + var personSearchResults = await _tmdbClientManager.SearchPersonAsync(id.Name, cancellationToken).ConfigureAwait(false); + if (personSearchResults.Count > 0) + { + personTmdbId = personSearchResults[0].Id; + } } var result = new MetadataResult(); - if (!string.IsNullOrEmpty(tmdbId)) + if (personTmdbId > 0) { - try - { - await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } + var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false); - throw; - } - - var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); - - var info = _jsonSerializer.DeserializeFromFile(dataFilePath); - - var item = new Person(); result.HasMetadata = true; - // Take name from incoming info, don't rename the person - // TODO: This should go in PersonMetadataService, not each person provider - item.Name = id.Name; - - // item.HomePageUrl = info.homepage; - - if (!string.IsNullOrWhiteSpace(info.Place_Of_Birth)) + var item = new Person { - item.ProductionLocations = new string[] { info.Place_Of_Birth }; + // Take name from incoming info, don't rename the person + // TODO: This should go in PersonMetadataService, not each person provider + Name = id.Name, + HomePageUrl = person.Homepage, + Overview = person.Biography, + PremiereDate = person.Birthday?.ToUniversalTime(), + EndDate = person.Deathday?.ToUniversalTime() + }; + + if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth)) + { + item.ProductionLocations = new[] { person.PlaceOfBirth }; } - item.Overview = info.Biography; + item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); - if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date)) + if (!string.IsNullOrEmpty(person.ImdbId)) { - item.PremiereDate = date.ToUniversalTime(); - } - - if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) - { - item.EndDate = date.ToUniversalTime(); - } - - item.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); - - if (!string.IsNullOrEmpty(info.Imdb_Id)) - { - item.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id); + item.SetProviderId(MetadataProvider.Imdb, person.ImdbId); } result.HasMetadata = true; @@ -204,65 +139,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return result; } - /// - /// Gets the TMDB id. - /// - /// The information. - /// The cancellation token. - /// Task{System.String}. - private async Task GetTmdbId(PersonLookupInfo info, CancellationToken cancellationToken) - { - var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); - - return results.Select(i => i.GetProviderId(MetadataProvider.Tmdb)).FirstOrDefault(); - } - - internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken) - { - var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id); - - var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath); - - if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return; - } - - var url = string.Format( - CultureInfo.InvariantCulture, - TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", - TmdbUtils.ApiKey, - id); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - await response.Content.CopyToAsync(fs).ConfigureAwait(false); - } - - private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId) - { - var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1); - - return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId); - } - - internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId) - { - return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName); - } - - private static string GetPersonsDataPath(IApplicationPaths appPaths) - { - return Path.Combine(appPaths.CachePath, "tmdb-people"); - } - public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index c56774f8e7..172db129e4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -2,40 +2,37 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { - public class TmdbEpisodeImageProvider : - TmdbEpisodeProviderBase, - IRemoteImageProvider, - IHasOrder + public class TmdbEpisodeImageProvider : IRemoteImageProvider, IHasOrder { - public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) - { - } + private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public string Name => TmdbUtils.ProviderName; + public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) + { + _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; + } // After TheTvDb public int Order => 1; + public string Name => TmdbUtils.ProviderName; + public IEnumerable GetSupportedImages(BaseItem item) { return new List @@ -49,13 +46,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var episode = (Controller.Entities.TV.Episode)item; var series = episode.Series; - var seriesId = series?.GetProviderId(MetadataProvider.Tmdb); + var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - var list = new List(); - - if (string.IsNullOrEmpty(seriesId)) + if (seriesTmdbId <= 0) { - return list; + return Enumerable.Empty(); } var seasonNumber = episode.ParentIndexNumber; @@ -63,76 +58,51 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!seasonNumber.HasValue || !episodeNumber.HasValue) { - return list; + return Enumerable.Empty(); } var language = item.GetPreferredMetadataLanguage(); - var response = await GetEpisodeInfo( - seriesId, - seasonNumber.Value, - episodeNumber.Value, - language, - cancellationToken).ConfigureAwait(false); + var episodeResult = await _tmdbClientManager + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .ConfigureAwait(false); - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo + if (episodeResult?.Images?.Stills == null) { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); + return Enumerable.Empty(); + } - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + var remoteImages = new List(); - return list.OrderByDescending(i => + for (var i = 0; i < episodeResult.Images.Stills.Count; i++) { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + var image = episodeResult.Images.Stills[i]; + remoteImages.Add(new RemoteImageInfo { - return 3; - } + Url = _tmdbClientManager.GetStillUrl(image.FilePath), + CommunityRating = image.VoteAverage, + VoteCount = image.VoteCount, + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }); + } - 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 IEnumerable GetPosters(StillImages images) - { - return images.Stills ?? new List(); + return remoteImages.OrderByLanguageDescending(language); } public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return GetResponse(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } public bool Supports(BaseItem item) { return item is Controller.Entities.TV.Episode; } + } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index a7e3a03fe3..b9d4a895d5 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -15,21 +15,21 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { - public class TmdbEpisodeProvider : - TmdbEpisodeProviderBase, - IRemoteMetadataProvider, - IHasOrder + public class TmdbEpisodeProvider : IRemoteMetadataProvider, IHasOrder { - public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) + private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; + + public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { + _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } // After TheTvDb @@ -39,51 +39,54 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public async Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) { - var list = new List(); - // The search query must either provide an episode number or date if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue) { - return list; + return Enumerable.Empty(); } - var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); + var metadataResult = await GetMetadata(searchInfo, cancellationToken); - if (metadataResult.HasMetadata) + if (!metadataResult.HasMetadata) { - var item = metadataResult.Item; - - list.Add(new RemoteSearchResult - { - IndexNumber = item.IndexNumber, - Name = item.Name, - ParentIndexNumber = item.ParentIndexNumber, - PremiereDate = item.PremiereDate, - ProductionYear = item.ProductionYear, - ProviderIds = item.ProviderIds, - SearchProviderName = Name, - IndexNumberEnd = item.IndexNumberEnd - }); + return Enumerable.Empty(); } + var list = new List(); + + var item = metadataResult.Item; + + list.Add(new RemoteSearchResult + { + IndexNumber = item.IndexNumber, + Name = item.Name, + ParentIndexNumber = item.ParentIndexNumber, + PremiereDate = item.PremiereDate, + ProductionYear = item.ProductionYear, + ProviderIds = item.ProviderIds, + SearchProviderName = Name, + IndexNumberEnd = item.IndexNumberEnd + }); + return list; } public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) { - var result = new MetadataResult(); + var metadataResult = new MetadataResult(); // Allowing this will dramatically increase scan times if (info.IsMissingEpisode) { - return result; + return metadataResult; } - info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId); + info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string tmdbId); - if (string.IsNullOrEmpty(seriesTmdbId)) + var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture); + if (seriesTmdbId <= 0) { - return result; + return metadataResult; } var seasonNumber = info.ParentIndexNumber; @@ -91,125 +94,121 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!seasonNumber.HasValue || !episodeNumber.HasValue) { - return result; + return metadataResult; } - try + var episodeResult = await _tmdbClientManager + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + + if (episodeResult == null) { - var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - result.HasMetadata = true; - result.QueriedById = true; - - if (!string.IsNullOrEmpty(response.Overview)) - { - // if overview is non-empty, we can assume that localized data was returned - result.ResultLanguage = info.MetadataLanguage; - } - - var item = new Episode(); - result.Item = item; - - item.Name = info.Name; - item.IndexNumber = info.IndexNumber; - item.ParentIndexNumber = info.ParentIndexNumber; - item.IndexNumberEnd = info.IndexNumberEnd; - - if (response.External_Ids != null && response.External_Ids.Tvdb_Id > 0) - { - item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture)); - } - - item.PremiereDate = response.Air_Date; - item.ProductionYear = result.Item.PremiereDate.Value.Year; - - item.Name = response.Name; - item.Overview = response.Overview; - - item.CommunityRating = (float)response.Vote_Average; - - if (response.Videos?.Results != null) - { - foreach (var video in response.Videos.Results) - { - if (video.Type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase) - || video.Type.Equals("clip", System.StringComparison.OrdinalIgnoreCase)) - { - if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase)) - { - var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key); - item.AddTrailerUrl(videoUrl); - } - } - } - } - - result.ResetPeople(); - - var credits = response.Credits; - if (credits != null) - { - // Actors, Directors, Writers - all in People - // actors come from cast - if (credits.Cast != null) - { - foreach (var actor in credits.Cast.OrderBy(a => a.Order)) - { - result.AddPerson(new PersonInfo { Name = actor.Name.Trim(), Role = actor.Character, Type = PersonType.Actor, SortOrder = actor.Order }); - } - } - - // guest stars - if (credits.Guest_Stars != null) - { - foreach (var guest in credits.Guest_Stars.OrderBy(a => a.Order)) - { - result.AddPerson(new PersonInfo { Name = guest.Name.Trim(), Role = guest.Character, Type = PersonType.GuestStar, SortOrder = guest.Order }); - } - } - - // and the rest from crew - if (credits.Crew != null) - { - var keepTypes = new[] - { - PersonType.Director, - PersonType.Writer, - PersonType.Producer - }; - - foreach (var person in credits.Crew) - { - // Normalize this - var type = TmdbUtils.MapCrewToPersonType(person); - - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && - !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - result.AddPerson(new PersonInfo { Name = person.Name.Trim(), Role = person.Job, Type = type }); - } - } - } + return metadataResult; } - catch (HttpException ex) + + metadataResult.HasMetadata = true; + metadataResult.QueriedById = true; + + if (!string.IsNullOrEmpty(episodeResult.Overview)) { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - - throw; + // if overview is non-empty, we can assume that localized data was returned + metadataResult.ResultLanguage = info.MetadataLanguage; } - return result; + var item = new Episode + { + Name = info.Name, + IndexNumber = info.IndexNumber, + ParentIndexNumber = info.ParentIndexNumber, + IndexNumberEnd = info.IndexNumberEnd + }; + + if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId)) + { + item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId); + } + + item.PremiereDate = episodeResult.AirDate; + item.ProductionYear = episodeResult.AirDate?.Year; + + item.Name = episodeResult.Name; + item.Overview = episodeResult.Overview; + + item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage); + + if (episodeResult.Videos?.Results != null) + { + foreach (var video in episodeResult.Videos.Results) + { + if (TmdbUtils.IsTrailerType(video)) + { + var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key); + item.AddTrailerUrl(videoUrl); + } + } + } + + var credits = episodeResult.Credits; + + if (credits?.Cast != null) + { + foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) + { + metadataResult.AddPerson(new PersonInfo + { + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order + }); + } + } + + if (credits?.GuestStars != null) + { + foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) + { + metadataResult.AddPerson(new PersonInfo + { + Name = guest.Name.Trim(), + Role = guest.Character, + Type = PersonType.GuestStar, + SortOrder = guest.Order + }); + } + } + + // and the rest from crew + if (credits?.Crew != null) + { + foreach (var person in credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); + + if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + metadataResult.AddPerson(new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }); + } + } + + metadataResult.Item = item; + + return metadataResult; } public Task GetImageResponse(string url, CancellationToken cancellationToken) { - return GetResponse(url, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs deleted file mode 100644 index 34d2424a34..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs +++ /dev/null @@ -1,156 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Plugins.Tmdb.TV -{ - public abstract class TmdbEpisodeProviderBase - { - private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos"; - - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerConfigurationManager _configurationManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - - protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - { - _httpClientFactory = httpClientFactory; - _configurationManager = configurationManager; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _logger = loggerFactory.CreateLogger(); - } - - protected ILogger Logger => _logger; - - protected async Task GetEpisodeInfo( - string seriesTmdbId, - int season, - int episodeNumber, - string preferredMetadataLanguage, - CancellationToken cancellationToken) - { - await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage); - - return _jsonSerializer.DeserializeFromFile(dataFilePath); - } - - internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException(nameof(preferredLanguage)); - } - - var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format( - CultureInfo.InvariantCulture, - "season-{0}-episode-{1}-{2}.json", - seasonNumber.ToString(CultureInfo.InvariantCulture), - episodeNumber.ToString(CultureInfo.InvariantCulture), - preferredLanguage); - - return Path.Combine(path, filename); - } - - internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) - { - var url = string.Format( - CultureInfo.InvariantCulture, - urlPattern, - id, - seasonNumber.ToString(CultureInfo.InvariantCulture), - episodeNumber, - TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", language); - } - - var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; - - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - return await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - } - - protected Task GetResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index dcc7f87002..aeb0e5d34f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -13,29 +13,25 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + public TmdbSeasonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } public int Order => 1; - public string Name => ProviderName; - - public static string ProviderName => TmdbUtils.ProviderName; + public string Name => TmdbUtils.ProviderName; public Task GetImageResponse(string url, CancellationToken cancellationToken) { @@ -45,87 +41,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { var season = (Season)item; - var series = season.Series; + var series = season?.Series; - var seriesId = series?.GetProviderId(MetadataProvider.Tmdb); + var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (string.IsNullOrEmpty(seriesId)) - { - return Enumerable.Empty(); - } - - var seasonNumber = season.IndexNumber; - - if (!seasonNumber.HasValue) + if (seriesTmdbId <= 0 || season?.IndexNumber != null) { return Enumerable.Empty(); } var language = item.GetPreferredMetadataLanguage(); - var results = await FetchImages(season, seriesId, language, cancellationToken).ConfigureAwait(false); + var seasonResult = await _tmdbClientManager + .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .ConfigureAwait(false); - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - var list = results.Select(i => new RemoteImageInfo + if (seasonResult?.Images?.Posters == null) { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - }); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(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 async Task> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken) - { - var seasonNumber = item.IndexNumber.GetValueOrDefault(); - await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false); - - var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language); - - if (!string.IsNullOrEmpty(path)) - { - if (File.Exists(path)) - { - return _jsonSerializer.DeserializeFromFile(path).Images.Posters; - } + return Enumerable.Empty(); } - return null; + var remoteImages = new List(); + for (var i = 0; i < seasonResult.Images.Posters.Count; i++) + { + var image = seasonResult.Images.Posters[i]; + remoteImages.Add(new RemoteImageInfo + { + Url = _tmdbClientManager.GetPosterUrl(image.FilePath), + CommunityRating = image.VoteAverage, + VoteCount = image.VoteCount, + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }); + } + + return remoteImages.OrderByLanguageDescending(language); } public IEnumerable GetSupportedImages(BaseItem item) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index c9b257fcc2..6ca462474a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -3,53 +3,28 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Net; +using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; 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.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; -using Season = MediaBrowser.Controller.Entities.TV.Season; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeasonProvider : IRemoteMetadataProvider { - private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos"; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerConfigurationManager _configurationManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly TmdbClientManager _tmdbClientManager; - internal static TmdbSeasonProvider Current { get; private set; } - - public TmdbSeasonProvider( - IHttpClientFactory httpClientFactory, - IServerConfigurationManager configurationManager, - IFileSystem fileSystem, - IJsonSerializer jsonSerializer, - ILogger logger) + public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; - _configurationManager = configurationManager; - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - _logger = logger; - Current = this; + _tmdbClientManager = tmdbClientManager; } public string Name => TmdbUtils.ProviderName; @@ -62,180 +37,86 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var seasonNumber = info.IndexNumber; - if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue) + if (string.IsNullOrWhiteSpace(seriesTmdbId) || !seasonNumber.HasValue) { - try + return result; + } + + var seasonResult = await _tmdbClientManager + .GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + + if (seasonResult == null) + { + return result; + } + + result.HasMetadata = true; + result.Item = new Season + { + Name = info.Name, + IndexNumber = seasonNumber, + Overview = seasonResult?.Overview + }; + + if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId)) + { + result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId); + } + + // TODO why was this disabled? + var credits = seasonResult.Credits; + if (credits?.Cast != null) + { + var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList(); + for (var i = 0; i < cast.Count; i++) { - var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - result.HasMetadata = true; - result.Item = new Season(); - - // Don't use moviedb season names for now until if/when we have field-level configuration - // result.Item.Name = seasonInfo.name; - - result.Item.Name = info.Name; - - result.Item.IndexNumber = seasonNumber; - - result.Item.Overview = seasonInfo.Overview; - - if (seasonInfo.External_Ids != null && seasonInfo.External_Ids.Tvdb_Id > 0) + result.AddPerson(new PersonInfo { - result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture)); - } - - var credits = seasonInfo.Credits; - if (credits != null) - { - // Actors, Directors, Writers - all in People - // actors come from cast - if (credits.Cast != null) - { - // foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); - } - - // and the rest from crew - if (credits.Crew != null) - { - // foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); - } - } - - result.Item.PremiereDate = seasonInfo.Air_Date; - result.Item.ProductionYear = result.Item.PremiereDate.Value.Year; - } - catch (HttpException ex) - { - _logger.LogError(ex, "No metadata found for {0}", seasonNumber.Value); - - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - - throw; + Name = cast[i].Name.Trim(), + Role = cast[i].Character, + Type = PersonType.Actor, + SortOrder = cast[i].Order + }); } } + if (credits?.Crew != null) + { + foreach (var person in credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); + + if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + result.AddPerson(new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }); + } + } + + result.Item.PremiereDate = seasonResult.AirDate; + result.Item.ProductionYear = seasonResult.AirDate?.Year; + return result; } public Task> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) { - return Task.FromResult>(new List()); + return Task.FromResult(Enumerable.Empty()); } public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - - private async Task GetSeasonInfo( - string seriesTmdbId, - int season, - string preferredMetadataLanguage, - CancellationToken cancellationToken) - { - await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage); - - return _jsonSerializer.DeserializeFromFile(dataFilePath); - } - - internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - var path = GetDataFilePath(tmdbId, seasonNumber, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } - } - - return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException(nameof(preferredLanguage)); - } - - var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format( - CultureInfo.InvariantCulture, - "season-{0}-{1}.json", - seasonNumber.ToString(CultureInfo.InvariantCulture), - preferredLanguage); - - return Path.Combine(path, filename); - } - - internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken) - { - var url = string.Format( - CultureInfo.InvariantCulture, - GetTvInfo3, - id, - seasonNumber.ToString(CultureInfo.InvariantCulture), - TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); - } - - var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; - - cancellationToken.ThrowIfCancellationRequested(); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - return await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 179ceb825d..0b477f75b6 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; @@ -13,28 +13,23 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClientFactory _httpClientFactory; + private readonly TmdbClientManager _tmdbClientManager; - public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + public TmdbSeriesImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { - _jsonSerializer = jsonSerializer; _httpClientFactory = httpClientFactory; + _tmdbClientManager = tmdbClientManager; } - public string Name => ProviderName; - - public static string ProviderName => TmdbUtils.ProviderName; + public string Name => TmdbUtils.ProviderName; // After tvdb and fanart public int Order => 2; @@ -54,107 +49,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) - { - var list = new List(); - - var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false); - - if (results == null) - { - return list; - } - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - var language = item.GetPreferredMetadataLanguage(); - - list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - - list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.File_Path, - CommunityRating = i.Vote_Average, - VoteCount = i.Vote_Count, - Width = i.Width, - Height = i.Height, - ProviderName = Name, - Type = ImageType.Backdrop, - RatingType = RatingType.Score - })); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(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); - } - - /// - /// Gets the posters. - /// - /// The images. - private IEnumerable GetPosters(Images images) - { - return images.Posters ?? new List(); - } - - /// - /// Gets the backdrops. - /// - /// The images. - private IEnumerable GetBackdrops(Images images) - { - var eligibleBackdrops = images.Backdrops ?? new List(); - - return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) - .ThenByDescending(i => i.Vote_Count); - } - - /// - /// Fetches the images. - /// - /// The item. - /// The language. - /// The cancellation token. - /// Task{MovieImages}. - private async Task FetchImages( - BaseItem item, - string language, - CancellationToken cancellationToken) { var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); @@ -163,16 +57,53 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return null; } - await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + var language = item.GetPreferredMetadataLanguage(); - var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + var series = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .ConfigureAwait(false); - if (!string.IsNullOrEmpty(path) && File.Exists(path)) + if (series?.Images == null) { - return _jsonSerializer.DeserializeFromFile(path).Images; + return Array.Empty(); } - return null; + var remoteImages = new List(); + + for (var i = 0; i < series.Images.Posters.Count; i++) + { + var poster = series.Images.Posters[i]; + remoteImages.Add(new RemoteImageInfo + { + Url = _tmdbClientManager.GetPosterUrl(poster.FilePath), + CommunityRating = poster.VoteAverage, + VoteCount = poster.VoteCount, + Width = poster.Width, + Height = poster.Height, + Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language), + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + }); + } + + for (var i = 0; i < series.Images.Backdrops.Count; i++) + { + var backdrop = series.Images.Backdrops[i]; + remoteImages.Add(new RemoteImageInfo + { + Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath), + CommunityRating = backdrop.VoteAverage, + VoteCount = backdrop.VoteCount, + Width = backdrop.Width, + Height = backdrop.Height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + }); + } + + return remoteImages.OrderByLanguageDescending(language); } public Task GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 287ebca8c9..f110785df9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -9,7 +10,6 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -17,93 +17,78 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; -using MediaBrowser.Providers.Plugins.Tmdb.Models.TV; using MediaBrowser.Providers.Plugins.Tmdb.Movies; using Microsoft.Extensions.Logging; +using TMDbLib.Objects.Find; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { public class TmdbSeriesProvider : IRemoteMetadataProvider, IHasOrder { - private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IServerConfigurationManager _configurationManager; - private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; - private readonly ILibraryManager _libraryManager; + private readonly TmdbClientManager _tmdbClientManager; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); public TmdbSeriesProvider( - IJsonSerializer jsonSerializer, - IServerConfigurationManager configurationManager, - ILogger logger, IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager) + TmdbClientManager tmdbClientManager) { - _jsonSerializer = jsonSerializer; - _configurationManager = configurationManager; - _logger = logger; _httpClientFactory = httpClientFactory; - _libraryManager = libraryManager; + _tmdbClientManager = tmdbClientManager; Current = this; } - internal static TmdbSeriesProvider Current { get; private set; } - public string Name => TmdbUtils.ProviderName; // After TheTVDB public int Order => 1; + internal static TmdbSeriesProvider Current { get; private set; } + public async Task> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) { var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(tmdbId)) { - cancellationToken.ThrowIfCancellationRequested(); + var series = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); - await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); - - var obj = _jsonSerializer.DeserializeFromFile(dataFilePath); - - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - - var remoteResult = new RemoteSearchResult + if (series != null) { - Name = obj.Name, - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path - }; + var remoteResult = MapTvShowToRemoteSearchResult(series); - remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture)); - remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id); - - if (obj.External_Ids != null && obj.External_Ids.Tvdb_Id > 0) - { - remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture)); + return new[] { remoteResult }; } - - return new[] { remoteResult }; } var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { - var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); + var findResult = await _tmdbClientManager + .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); - if (searchResult != null) + if (findResult?.TvResults != null) { - return new[] { searchResult }; + var imdbIdResults = new List(); + for (var i = 0; i < findResult.TvResults.Count; i++) + { + var remoteResult = MapSearchTvToRemoteSearchResult(findResult.TvResults[i]); + remoteResult.SetProviderId(MetadataProvider.Imdb, imdbId); + imdbIdResults.Add(remoteResult); + } + + return imdbIdResults; } } @@ -111,15 +96,79 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!string.IsNullOrEmpty(tvdbId)) { - var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); + var findResult = await _tmdbClientManager + .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); - if (searchResult != null) + if (findResult?.TvResults != null) { - return new[] { searchResult }; + var tvIdResults = new List(); + for (var i = 0; i < findResult.TvResults.Count; i++) + { + var remoteResult = MapSearchTvToRemoteSearchResult(findResult.TvResults[i]); + remoteResult.SetProviderId(MetadataProvider.Tvdb, tvdbId); + tvIdResults.Add(remoteResult); + } + + return tvIdResults; } } - return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + + var remoteResults = new List(); + for (var i = 0; i < tvSearchResults.Count; i++) + { + remoteResults.Add(MapSearchTvToRemoteSearchResult(tvSearchResults[i])); + } + + return remoteResults; + } + + private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series) + { + var remoteResult = new RemoteSearchResult + { + Name = series.Name ?? series.OriginalName, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath), + Overview = series.Overview + }; + + remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(_usCulture)); + if (series.ExternalIds != null) + { + if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId)) + { + remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId); + } + + if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId)) + { + remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId); + } + } + + remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); + + return remoteResult; + } + + private RemoteSearchResult MapSearchTvToRemoteSearchResult(SearchTv series) + { + var remoteResult = new RemoteSearchResult + { + Name = series.Name ?? series.OriginalName, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath), + Overview = series.Overview + }; + + remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(_usCulture)); + remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); + + return remoteResult; } public async Task> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) @@ -137,11 +186,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!string.IsNullOrEmpty(imdbId)) { - var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); + var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); if (searchResult != null) { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); + tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); } } } @@ -152,11 +201,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!string.IsNullOrEmpty(tvdbId)) { - var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); + var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); if (searchResult != null) { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); + tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); } } } @@ -164,13 +213,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (string.IsNullOrEmpty(tmdbId)) { result.QueriedById = false; - var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false); + var searchResults = await _tmdbClientManager.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) + if (searchResults.Count > 0) { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); + tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); } } @@ -178,7 +225,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { cancellationToken.ThrowIfCancellationRequested(); - result = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + var tvShow = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); + + result = new MetadataResult + { + Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode), + ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage + }; + + foreach (var person in GetPersons(tvShow)) + { + result.AddPerson(person); + } result.HasMetadata = result.Item != null; } @@ -186,99 +246,62 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return result; } - private async Task> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken) + private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode) { - SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false); + var series = new Series {Name = seriesResult.Name, OriginalTitle = seriesResult.OriginalName}; - if (seriesInfo == null) + series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(_usCulture)); + + series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage); + + series.Overview = seriesResult.Overview; + + if (seriesResult.Networks != null) { - return null; + series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray(); } - tmdbId = seriesInfo.Id.ToString(_usCulture); - - string dataFilePath = GetDataFilePath(tmdbId, language); - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath); - - await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var result = new MetadataResult + if (seriesResult.Genres != null) { - Item = new Series(), - ResultLanguage = seriesInfo.ResultLanguage - }; - - var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings); - - return result; - } - - private void ProcessMainInfo(MetadataResult seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings) - { - var series = seriesResult.Item; - - series.Name = seriesInfo.Name; - series.OriginalTitle = seriesInfo.Original_Name; - series.SetProviderId(MetadataProvider.Tmdb, seriesInfo.Id.ToString(_usCulture)); - - string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture); - - if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating)) - { - series.CommunityRating = rating; + series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray(); } - series.Overview = seriesInfo.Overview; + series.HomePageUrl = seriesResult.Homepage; - if (seriesInfo.Networks != null) - { - series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray(); - } + series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - if (seriesInfo.Genres != null) - { - series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray(); - } - - series.HomePageUrl = seriesInfo.Homepage; - - series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - - if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase)) { series.Status = SeriesStatus.Ended; - series.EndDate = seriesInfo.Last_Air_Date; + series.EndDate = seriesResult.LastAirDate; } else { series.Status = SeriesStatus.Continuing; } - series.PremiereDate = seriesInfo.First_Air_Date; + series.PremiereDate = seriesResult.FirstAirDate; - var ids = seriesInfo.External_Ids; + var ids = seriesResult.ExternalIds; if (ids != null) { - if (!string.IsNullOrWhiteSpace(ids.Imdb_Id)) + if (!string.IsNullOrWhiteSpace(ids.ImdbId)) { - series.SetProviderId(MetadataProvider.Imdb, ids.Imdb_Id); + series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId); } - if (ids.Tvrage_Id > 0) + if (!string.IsNullOrEmpty(ids.TvrageId)) { - series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture)); + series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId); } - if (ids.Tvdb_Id > 0) + if (!string.IsNullOrEmpty(ids.TvdbId)) { - series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture)); + series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId); } } - var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List(); + var contentRatings = seriesResult.ContentRatings.Results ?? new List(); var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); @@ -297,254 +320,72 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.OfficialRating = minimumRelease.Rating; } - if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null) + if (seriesResult.Videos?.Results != null) { - foreach (var video in seriesInfo.Videos.Results) + foreach (var video in seriesResult.Videos.Results) { - if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) - || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase)) - && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)) + if (TmdbUtils.IsTrailerType(video)) { series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}"); } } } - seriesResult.ResetPeople(); - var tmdbImageUrl = settings.images.GetImageUrl("original"); + return series; + } - if (seriesInfo.Credits != null) + private IEnumerable GetPersons(TvShow seriesResult) + { + if (seriesResult.Credits?.Cast != null) { - if (seriesInfo.Credits.Cast != null) + foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers)) { - foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order)) + var personInfo = new PersonInfo { - var personInfo = new PersonInfo - { - Name = actor.Name.Trim(), - Role = actor.Character, - Type = PersonType.Actor, - SortOrder = actor.Order - }; - - if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) - { - personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; - } - - if (actor.Id > 0) - { - personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); - } - - seriesResult.AddPerson(personInfo); - } - } - - if (seriesInfo.Credits.Crew != null) - { - var keepTypes = new[] - { - PersonType.Director, - PersonType.Writer, - PersonType.Producer + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order, + ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath) }; - foreach (var person in seriesInfo.Credits.Crew) + if (actor.Id > 0) { - // Normalize this - var type = TmdbUtils.MapCrewToPersonType(person); - - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) - && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - seriesResult.AddPerson(new PersonInfo - { - Name = person.Name.Trim(), - Role = person.Job, - Type = type - }); + personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); } - } - } - } - internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetSeriesDataPath(appPaths); - - return Path.Combine(dataPath, tmdbId); - } - - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv"); - - return dataPath; - } - - internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult == null) - { - return; - } - - var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task FetchMainResult(string id, string language, CancellationToken cancellationToken) - { - var url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language) - + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language - } - - cancellationToken.ThrowIfCancellationRequested(); - - using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage, cancellationToken).ConfigureAwait(false); - await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var mainResult = await _jsonSerializer.DeserializeFromStreamAsync(mainStream).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(language)) - { - mainResult.ResultLanguage = language; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // If the language preference isn't english, then have the overview fallback to english if it's blank - if (mainResult != null && - string.IsNullOrEmpty(mainResult.Overview) && - !string.IsNullOrEmpty(language) && - !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language); - - url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) - { - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var englishResult = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - - mainResult.Overview = englishResult.Overview; - mainResult.ResultLanguage = "en"; - } - - return mainResult; - } - - internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); - } - - var path = GetDataFilePath(tmdbId, language); - - var fileInfo = new FileInfo(path); - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays <= 2) - { - return Task.CompletedTask; + yield return personInfo; } } - return DownloadSeriesInfo(tmdbId, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) + if (seriesResult.Credits?.Crew != null) { - throw new ArgumentNullException(nameof(tmdbId)); - } - - var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format(CultureInfo.InvariantCulture, "series-{0}.json", preferredLanguage ?? string.Empty); - - return Path.Combine(path, filename); - } - - private async Task FindByExternalId(string id, string externalSource, CancellationToken cancellationToken) - { - var url = string.Format( - CultureInfo.InvariantCulture, - TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}", - id, - TmdbUtils.ApiKey, - externalSource); - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) - { - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); - } - - using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - - var result = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); - - if (result != null && result.Tv_Results != null) - { - var tv = result.Tv_Results.FirstOrDefault(); - - if (tv != null) + var keepTypes = new[] { - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + PersonType.Director, + PersonType.Writer, + PersonType.Producer + }; - var remoteResult = new RemoteSearchResult + foreach (var person in seriesResult.Credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); + + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - Name = tv.Name, - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path) - ? null - : tmdbImageUrl + tv.Poster_Path + continue; + } + + yield return new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type }; - - remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture)); - - return remoteResult; } } - - return null; } public Task GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs new file mode 100644 index 0000000000..537082300d --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using TMDbLib.Client; +using TMDbLib.Objects.Collections; +using TMDbLib.Objects.Find; +using TMDbLib.Objects.General; +using TMDbLib.Objects.Movies; +using TMDbLib.Objects.People; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; + +namespace MediaBrowser.Providers.Plugins.Tmdb +{ + /// + /// Manager class for abstracting the TMDb API client library. + /// + public class TmdbClientManager + { + private const int CacheDurationInHours = 1; + + private readonly IMemoryCache _memoryCache; + private readonly TMDbClient _tmDbClient; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + public TmdbClientManager(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + _tmDbClient = new TMDbClient(TmdbUtils.ApiKey); + // Not really interested in NotFoundException + _tmDbClient.ThrowApiExceptions = false; + } + + /// + /// Gets a movie from the TMDb API based on its TMDb id. + /// + /// The movie's TMDb id. + /// The movie's language. + /// A comma-separated list of image languages. + /// The cancellation token. + /// The TMDb movie or null if not found. + public async Task GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out Movie movie)) + { + return movie; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + // TODO include image language + movie = await _tmDbClient.GetMovieAsync( + tmdbId, + TmdbUtils.NormalizeLanguage(language), + MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos, + cancellationToken).ConfigureAwait(false); + + if (movie != null) + { + _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours)); + } + + return movie; + } + + /// + /// Gets a collection from the TMDb API based on its TMDb id. + /// + /// The collection's TMDb id. + /// The collection's language. + /// A comma-separated list of image languages. + /// The cancellation token. + /// The TMDb collection or null if not found. + public async Task GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out Collection collection)) + { + return collection; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + // TODO include image language + collection = await _tmDbClient.GetCollectionAsync( + tmdbId, + TmdbUtils.NormalizeLanguage(language), + CollectionMethods.Images, + cancellationToken).ConfigureAwait(false); + + if (collection != null) + { + _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours)); + } + + return collection; + } + + /// + /// Gets a tv show from the TMDb API based on its TMDb id. + /// + /// The tv show's TMDb id. + /// The tv show's language. + /// A comma-separated list of image languages. + /// The cancellation token. + /// The TMDb tv show information or null if not found. + public async Task GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out TvShow series)) + { + return series; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + // TODO include image language + series = await _tmDbClient.GetTvShowAsync( + tmdbId, + language: TmdbUtils.NormalizeLanguage(language), + extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (series != null) + { + _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours)); + } + + return series; + } + + /// + /// Gets a tv season from the TMDb API based on the tv show's TMDb id. + /// + /// The tv season's TMDb id. + /// The season number. + /// The tv season's language. + /// A comma-separated list of image languages. + /// The cancellation token. + /// The TMDb tv season information or null if not found. + public async Task GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out TvSeason season)) + { + return season; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + // TODO include image language + season = await _tmDbClient.GetTvSeasonAsync( + tvShowId, + seasonNumber, + language: TmdbUtils.NormalizeLanguage(language), + extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (season != null) + { + _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours)); + } + + return season; + } + + /// + /// Gets a movie from the TMDb API based on the tv show's TMDb id. + /// + /// The tv show's TMDb id. + /// The season number. + /// The episode number. + /// The episode's language. + /// A comma-separated list of image languages. + /// The cancellation token. + /// The TMDb tv episode information or null if not found. + public async Task GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken) + { + var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out TvEpisode episode)) + { + return episode; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + // TODO include image language + episode = await _tmDbClient.GetTvEpisodeAsync( + tvShowId, + seasonNumber, + episodeNumber, + language: TmdbUtils.NormalizeLanguage(language), + extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (episode != null) + { + _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours)); + } + + return episode; + } + + /// + /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id. + /// + /// The person's TMDb id. + /// The cancellation token. + /// The TMDb person information or null if not found. + public async Task GetPersonAsync(int personTmdbId, CancellationToken cancellationToken) + { + var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out Person person)) + { + return person; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + person = await _tmDbClient.GetPersonAsync( + personTmdbId, + PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds, + cancellationToken).ConfigureAwait(false); + + if (person != null) + { + _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours)); + } + + return person; + } + + /// + /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id. + /// + /// The item's external id. + /// The source of the id eg. IMDb. + /// The item's language. + /// The cancellation token. + /// The TMDb item or null if not found. + public async Task FindByExternalIdAsync( + string externalId, + FindExternalSource source, + string language, + CancellationToken cancellationToken) + { + var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out FindContainer result)) + { + return result; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + // TODO language + result = await _tmDbClient.FindAsync( + source, + externalId, + cancellationToken).ConfigureAwait(false); + + if (result != null) + { + _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours)); + } + + return result; + } + + /// + /// Searches for a tv show using the TMDb API based on its name. + /// + /// The name of the tv show. + /// The tv show's language. + /// The cancellation token. + /// The TMDb tv show information. + public async Task> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken) + { + var key = $"searchseries-{name}-{language}"; + if (_memoryCache.TryGetValue(key, out SearchContainer series)) + { + return series.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// + /// Searches for a person based on their name using the TMDb API. + /// + /// The name of the person. + /// The cancellation token. + /// The TMDb person information. + public async Task> SearchPersonAsync(string name, CancellationToken cancellationToken) + { + var key = $"searchperson-{name}"; + if (_memoryCache.TryGetValue(key, out SearchContainer person)) + { + return person.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchPersonAsync(name, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// + /// Searches for a movie based on its name using the TMDb API. + /// + /// The name of the movie. + /// The movie's language. + /// The cancellation token. + /// The TMDb movie information. + public Task> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) + { + return SearchMovieAsync(name, 0, language, cancellationToken); + } + + /// + /// Searches for a movie based on its name using the TMDb API. + /// + /// The name of the movie. + /// The release year of the movie. + /// The movie's language. + /// The cancellation token. + /// The TMDb movie information. + public async Task> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken) + { + var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; + if (_memoryCache.TryGetValue(key, out SearchContainer movies)) + { + return movies.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// + /// Searches for a collection based on its name using the TMDb API. + /// + /// The name of the collection. + /// The collection's language. + /// The cancellation token. + /// The TMDb collection information. + public async Task> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken) + { + var key = $"collectionsearch-{name}-{language}"; + if (_memoryCache.TryGetValue(key, out SearchContainer collections)) + { + return collections.Results; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var searchResults = await _tmDbClient + .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (searchResults.Results.Count > 0) + { + _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); + } + + return searchResults.Results; + } + + /// + /// Gets the absolute URL of the poster. + /// + /// The relative URL of the poster. + /// The absolute URL. + public string GetPosterUrl(string posterPath) + { + if (string.IsNullOrEmpty(posterPath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString(); + } + + /// + /// Gets the absolute URL of the backdrop image. + /// + /// The relative URL of the backdrop image. + /// The absolute URL. + public string GetBackdropUrl(string posterPath) + { + if (string.IsNullOrEmpty(posterPath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString(); + } + + /// + /// Gets the absolute URL of the profile image. + /// + /// The relative URL of the profile image. + /// The absolute URL. + public string GetProfileUrl(string actorProfilePath) + { + if (string.IsNullOrEmpty(actorProfilePath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString(); + } + + /// + /// Gets the absolute URL of the still image. + /// + /// The relative URL of the still image. + /// The absolute URL. + public string GetStillUrl(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + return null; + } + + return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString(); + } + + private Task EnsureClientConfigAsync() + { + return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask; + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 1415d69761..1fb85e1a36 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -1,7 +1,10 @@ +#nullable enable + using System; +using System.Collections.Generic; using System.Net.Mime; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.Plugins.Tmdb.Models.General; +using TMDbLib.Objects.General; namespace MediaBrowser.Providers.Plugins.Tmdb { @@ -30,11 +33,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// public const string ApiKey = "4219e299c89411838049ab0dab19ebd5"; + /// + /// Maximum number of cast members to pull. + /// + public const int MaxCastMembers = 15; + /// /// Value of the Accept header for requests to the provider. /// public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" }; + /// + /// The crew types to keep. + /// + public static readonly string[] WantedCrewTypes = + { + PersonType.Director, + PersonType.Writer, + PersonType.Producer + }; + /// /// Maps the TMDB provided roles for crew members to Jellyfin roles. /// @@ -61,5 +79,75 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return null; } + + public static bool IsTrailerType(Video video) + { + return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase) + && (!video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) + || !video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase)); + } + + public static string GetImageLanguagesParam(string preferredLanguage) + { + var languages = new List(); + + if (!string.IsNullOrEmpty(preferredLanguage)) + { + preferredLanguage = NormalizeLanguage(preferredLanguage); + + languages.Add(preferredLanguage); + + if (preferredLanguage.Length == 5) // like en-US + { + // Currenty, TMDB supports 2-letter language codes only + // They are planning to change this in the future, thus we're + // supplying both codes if we're having a 5-letter code. + languages.Add(preferredLanguage.Substring(0, 2)); + } + } + + languages.Add("null"); + + if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase)) + { + languages.Add("en"); + } + + return string.Join(',', languages); + } + + public static string NormalizeLanguage(string language) + { + if (string.IsNullOrEmpty(language)) + { + return language; + } + + // They require this to be uppercase + // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api. + // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab + var parts = language.Split('-'); + + if (parts.Length == 2) + { + language = parts[0] + "-" + parts[1].ToUpperInvariant(); + } + + return language; + } + + public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) + { + if (!string.IsNullOrEmpty(imageLanguage) + && !string.IsNullOrEmpty(requestLanguage) + && requestLanguage.Length > 2 + && imageLanguage.Length == 2 + && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) + { + return requestLanguage; + } + + return imageLanguage; + } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs deleted file mode 100644 index 613dc17e31..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs +++ /dev/null @@ -1,43 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; - -namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers -{ - public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider - { - private readonly IHttpClientFactory _httpClientFactory; - - public TmdbTrailerProvider(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } - - public string Name => TmdbMovieProvider.Current.Name; - - public int Order => 0; - - public Task> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken) - { - return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken); - } - - public Task> GetMetadata(TrailerInfo info, CancellationToken cancellationToken) - { - return TmdbMovieProvider.Current.GetItemMetadata(info, cancellationToken); - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); - } - } -} From 08e1f15add2bcc328c56191ff18c392263880bb6 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 24 Sep 2020 19:56:11 +0200 Subject: [PATCH 092/153] Cleanup in TmdbMovieProvider --- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 4dd29e773a..a5088dec4e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -4,9 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -16,7 +14,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { @@ -26,30 +23,24 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies public class TmdbMovieProvider : IRemoteMetadataProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly TmdbClientManager _tmdbClientManager; + public TmdbMovieProvider( + ILibraryManager libraryManager, + TmdbClientManager tmdbClientManager, + IHttpClientFactory httpClientFactory) + { + _libraryManager = libraryManager; + _tmdbClientManager = tmdbClientManager; + _httpClientFactory = httpClientFactory; + } + public string Name => TmdbUtils.ProviderName; /// public int Order => 1; - internal static TmdbMovieProvider Current { get; private set; } - - public TmdbMovieProvider( - ILogger logger, - ILibraryManager libraryManager, - TmdbClientManager tmdbClientManager, - IHttpClientFactory httpClientFactory) - { - _logger = logger; - _libraryManager = libraryManager; - _tmdbClientManager = tmdbClientManager; - _httpClientFactory = httpClientFactory; - Current = this; - } - public async Task> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) { var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); From 34d2716ce81087cc6a7315f90210e9816d0948c2 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 24 Sep 2020 20:00:44 +0200 Subject: [PATCH 093/153] Remove TmdbSettingsResult which came back in the rebase --- .../Plugins/Tmdb/Movies/TmdbSettingsResult.cs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs deleted file mode 100644 index c7ba974386..0000000000 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Providers.Plugins.Tmdb.Movies -{ - internal class TmdbSettingsResult - { - public TmdbImageSettings images { get; set; } - } -} From f8c4f7c63e3adb98dc1ad30aa01c3dd3d5c1e76c Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 24 Sep 2020 20:18:45 +0200 Subject: [PATCH 094/153] Fix inverted boolean statement --- .../Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index aeb0e5d34f..0feaa5732a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -45,7 +45,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (seriesTmdbId <= 0 || season?.IndexNumber != null) + if (seriesTmdbId <= 0 || season?.IndexNumber == null) { return Enumerable.Empty(); } From 3df2a95cac1e34f3392abbf239c9c7eb137e88cd Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 24 Sep 2020 20:31:57 +0200 Subject: [PATCH 095/153] Cleanup in TmdbUtils --- .../Plugins/Tmdb/TmdbUtils.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 1fb85e1a36..b754a07953 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Net.Mime; using MediaBrowser.Model.Entities; using TMDbLib.Objects.General; @@ -18,11 +17,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// public const string BaseTmdbUrl = "https://www.themoviedb.org/"; - /// - /// URL of the TMDB API instance to use. - /// - public const string BaseTmdbApiUrl = "https://api.themoviedb.org/"; - /// /// Name of the provider. /// @@ -38,11 +32,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// public const int MaxCastMembers = 15; - /// - /// Value of the Accept header for requests to the provider. - /// - public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" }; - /// /// The crew types to keep. /// @@ -77,9 +66,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return PersonType.Writer; } - return null; + return string.Empty; } + /// + /// Determines whether a video is a trailer. + /// + /// The TMDb video. + /// A boolean indicating whether the video is a trailer. public static bool IsTrailerType(Video video) { return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase) @@ -87,6 +81,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb || !video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase)); } + /// + /// Normalizes a language string for use with TMDb's include image language parameter. + /// + /// The preferred language as either a 2 letter code with or without country code. + /// The comma separated language string. public static string GetImageLanguagesParam(string preferredLanguage) { var languages = new List(); @@ -116,6 +115,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return string.Join(',', languages); } + /// + /// Normalizes a language string for use with TMDb's language parameter. + /// + /// The language code. + /// The normalized language code. public static string NormalizeLanguage(string language) { if (string.IsNullOrEmpty(language)) @@ -136,6 +140,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return language; } + /// + /// Adjusts the image's language code preferring the 5 letter language code eg. en-US. + /// + /// The image's actual language code. + /// The requested language code. + /// The language code. public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) { if (!string.IsNullOrEmpty(imageLanguage) From f4d49eff9bc209743270b104cb20a3ebd4f95d4e Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 10 Sep 2020 11:05:46 +0200 Subject: [PATCH 096/153] Add keywords as tags --- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index a5088dec4e..b736dede1b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -190,6 +190,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies movie.AddGenre(genre); } + if (movieResult.Keywords.Keywords != null) + { + for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++) + { + movie.AddTag(movieResult.Keywords.Keywords[i].Name); + } + } + if (movieResult.Credits?.Cast != null) { // TODO configurable From 9adbf4e9d8e52ed42619b4413d1eaccaa752b37d Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 10 Sep 2020 11:05:46 +0200 Subject: [PATCH 097/153] Add keywords as tags to series + cleanup --- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 2 +- .../Tmdb/People/TmdbPersonImageProvider.cs | 2 -- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 4 --- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 5 ---- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 1 - .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 26 ++++++++----------- 6 files changed, 12 insertions(+), 28 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index b736dede1b..3984e49537 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -190,7 +190,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies movie.AddGenre(genre); } - if (movieResult.Keywords.Keywords != null) + if (movieResult.Keywords?.Keywords != null) { for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index 99e4176984..6e50662232 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -8,13 +8,11 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Tmdb.People { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 8c5e8590f0..856d985e4d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -7,14 +7,10 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.People { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index b9d4a895d5..55f1ba7dcc 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -8,16 +8,11 @@ using System.Net.Http; 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.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 0b477f75b6..122e1d47d4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index f110785df9..7944dfe27c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -1,28 +1,18 @@ #pragma warning disable CS1591 using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; 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.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Plugins.Tmdb.Movies; -using Microsoft.Extensions.Logging; using TMDbLib.Objects.Find; using TMDbLib.Objects.Search; using TMDbLib.Objects.TvShows; @@ -34,8 +24,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public TmdbSeriesProvider( IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) @@ -136,7 +124,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV Overview = series.Overview }; - remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); if (series.ExternalIds != null) { if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId)) @@ -165,7 +153,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV Overview = series.Overview }; - remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); return remoteResult; @@ -250,7 +238,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { var series = new Series {Name = seriesResult.Name, OriginalTitle = seriesResult.OriginalName}; - series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(_usCulture)); + series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture)); series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage); @@ -266,6 +254,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray(); } + if (seriesResult.Keywords?.Results != null) + { + for (var i = 0; i < seriesResult.Keywords.Results.Count; i++) + { + series.AddTag(seriesResult.Keywords.Results[i].Name); + } + } + series.HomePageUrl = seriesResult.Homepage; series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); From c0f4c90d57ef32f00bdbdd4e5fa767322cd07905 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sun, 27 Sep 2020 20:13:38 +0200 Subject: [PATCH 098/153] Apply suggestions from code review Co-authored-by: Cody Robibero --- .../Plugins/Tmdb/People/TmdbPersonImageProvider.cs | 2 +- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 2 +- .../Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs | 1 - .../Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index 6e50662232..3f57c4bc4c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return remoteImages.OrderByLanguageDescending(language); } - return new List(); + return Enumerable.Empty(); } public Task GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 856d985e4d..78ea2a382c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People if (searchInfo.IsAutomated) { // Don't hammer moviedb searching by name - return new List(); + return Enumerable.Empty(); } var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 172db129e4..fe9e483c67 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -103,6 +103,5 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { return item is Controller.Entities.TV.Episode; } - } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 122e1d47d4..14ebf78be7 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (series?.Images == null) { - return Array.Empty(); + return Enumerable.Empty(); } var remoteImages = new List(); From e0f21afa6285e6c4153ebc39d3524f8f7d19096c Mon Sep 17 00:00:00 2001 From: cvium Date: Mon, 28 Sep 2020 23:56:55 +0200 Subject: [PATCH 099/153] Fix build --- MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs | 1 + .../Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 78ea2a382c..4384c203e5 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 14ebf78be7..2fdec0196b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; From d7cabb2e4ed601a09579b5e280d02115e476c902 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 19:36:28 +0200 Subject: [PATCH 100/153] Add image languages --- .../Plugins/Tmdb/TmdbClientManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 537082300d..343c81ed88 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -55,10 +55,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - // TODO include image language movie = await _tmDbClient.GetMovieAsync( tmdbId, TmdbUtils.NormalizeLanguage(language), + imageLanguages, MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos, cancellationToken).ConfigureAwait(false); @@ -121,10 +121,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - // TODO include image language series = await _tmDbClient.GetTvShowAsync( tmdbId, language: TmdbUtils.NormalizeLanguage(language), + includeImageLanguage: imageLanguages, extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -155,11 +155,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - // TODO include image language season = await _tmDbClient.GetTvSeasonAsync( tvShowId, seasonNumber, language: TmdbUtils.NormalizeLanguage(language), + includeImageLanguage: imageLanguages, extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -191,12 +191,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - // TODO include image language episode = await _tmDbClient.GetTvEpisodeAsync( tvShowId, seasonNumber, episodeNumber, language: TmdbUtils.NormalizeLanguage(language), + includeImageLanguage: imageLanguages, extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -259,10 +259,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - // TODO language result = await _tmDbClient.FindAsync( source, externalId, + TmdbUtils.NormalizeLanguage(language), cancellationToken).ConfigureAwait(false); if (result != null) From a0862c933d0479cf6672b8ba3b50c40f9c763663 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 19:45:16 +0200 Subject: [PATCH 101/153] Add image languages to collections --- MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 343c81ed88..2dc5cd55da 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -88,10 +88,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); - // TODO include image language collection = await _tmDbClient.GetCollectionAsync( tmdbId, TmdbUtils.NormalizeLanguage(language), + imageLanguages, CollectionMethods.Images, cancellationToken).ConfigureAwait(false); From 24dfa097fa08ab688129e94c458824ac1fc9439c Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 19:46:13 +0200 Subject: [PATCH 102/153] Update TMDbLib version --- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 11e30940fd..794490cc52 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -21,7 +21,7 @@ - + From a839b015f948fb8dee665d37f6ecb3796675da8a Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 21:24:04 +0200 Subject: [PATCH 103/153] Disable invalid auth provider --- Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs index e38cd07f0e..5f32479e1d 100644 --- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Users public string Name => "InvalidOrMissingAuthenticationProvider"; /// - public bool IsEnabled => true; + public bool IsEnabled => false; /// public Task Authenticate(string username, string password) From 7cec65b97390dd9fc5fe75a81eb69d61d48329c5 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sat, 3 Oct 2020 14:35:57 -0500 Subject: [PATCH 104/153] Update documentation --- Jellyfin.Api/Controllers/ImageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 6f925dec58..05efe23553 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Determines the output format of the image - original,gif,jpg,png. + /// Optional. The of the returned image. /// Optional. Add a played indicator. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. From d7dc15971ec2fd3598dbc0d60faed1d3aa57d40a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 4 Oct 2020 16:26:43 +0200 Subject: [PATCH 105/153] Add roundtrip test --- .../Json/JsonGuidConverterTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs index aef5b3a7d7..d9e66d6774 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs @@ -18,5 +18,15 @@ namespace Jellyfin.Common.Tests.Extensions value = JsonSerializer.Deserialize(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", options); Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); } + + [Fact] + public static void Roundtrip_Valid_Success() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonGuidConverter()); + Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); + string value = JsonSerializer.Serialize(guid, options); + Assert.Equal(guid, JsonSerializer.Deserialize(value, options)); + } } } From f0556c8dedf05683488aee901b7f3d57bebcdc62 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 4 Oct 2020 16:27:34 +0200 Subject: [PATCH 106/153] Improve GroupInfo class * Fixed docs * Remove extra dictionary lookups * change property to constant --- MediaBrowser.Controller/SyncPlay/GroupInfo.cs | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs index e742df5179..a1cada25cc 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -14,12 +14,12 @@ namespace MediaBrowser.Controller.SyncPlay public class GroupInfo { /// - /// Gets the default ping value used for sessions. + /// The default ping value used for sessions. /// - public long DefaultPing { get; } = 500; + public const long DefaultPing = 500; /// - /// Gets or sets the group identifier. + /// Gets the group identifier. /// /// The group identifier. public Guid GroupId { get; } = Guid.NewGuid(); @@ -58,7 +58,8 @@ namespace MediaBrowser.Controller.SyncPlay /// /// Checks if a session is in this group. /// - /// true if the session is in this group; false otherwise. + /// The session id to check. + /// true if the session is in this group; false otherwise. public bool ContainsSession(string sessionId) { return Participants.ContainsKey(sessionId); @@ -70,16 +71,14 @@ namespace MediaBrowser.Controller.SyncPlay /// The session. public void AddSession(SessionInfo session) { - if (ContainsSession(session.Id)) - { - return; - } - - var member = new GroupMember(); - member.Session = session; - member.Ping = DefaultPing; - member.IsBuffering = false; - Participants[session.Id] = member; + Participants.TryAdd( + session.Id, + new GroupMember + { + Session = session, + Ping = DefaultPing, + IsBuffering = false + }); } /// @@ -88,12 +87,7 @@ namespace MediaBrowser.Controller.SyncPlay /// The session. public void RemoveSession(SessionInfo session) { - if (!ContainsSession(session.Id)) - { - return; - } - - Participants.Remove(session.Id, out _); + Participants.Remove(session.Id); } /// @@ -103,18 +97,16 @@ namespace MediaBrowser.Controller.SyncPlay /// The ping. public void UpdatePing(SessionInfo session, long ping) { - if (!ContainsSession(session.Id)) + if (Participants.TryGetValue(session.Id, out GroupMember value)) { - return; + value.Ping = ping; } - - Participants[session.Id].Ping = ping; } /// /// Gets the highest ping in the group. /// - /// The highest ping in the group. + /// The highest ping in the group. public long GetHighestPing() { long max = long.MinValue; @@ -133,18 +125,16 @@ namespace MediaBrowser.Controller.SyncPlay /// The state. public void SetBuffering(SessionInfo session, bool isBuffering) { - if (!ContainsSession(session.Id)) + if (Participants.TryGetValue(session.Id, out GroupMember value)) { - return; + value.IsBuffering = isBuffering; } - - Participants[session.Id].IsBuffering = isBuffering; } /// /// Gets the group buffering state. /// - /// true if there is a session buffering in the group; false otherwise. + /// true if there is a session buffering in the group; false otherwise. public bool IsBuffering() { foreach (var session in Participants.Values) @@ -161,7 +151,7 @@ namespace MediaBrowser.Controller.SyncPlay /// /// Checks if the group is empty. /// - /// true if the group is empty; false otherwise. + /// true if the group is empty; false otherwise. public bool IsEmpty() { return Participants.Count == 0; From e96d41d5ef3b438437853e155578bf2dece4ad51 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 4 Oct 2020 16:45:00 +0200 Subject: [PATCH 107/153] Fix build --- .../SyncPlay/SyncPlayController.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index 80b977731c..5384795122 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -301,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay if (_group.IsPaused) { // Pick a suitable time that accounts for latency - var delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; + var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); // Unpause group and set starting point in future // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) @@ -452,8 +451,7 @@ namespace Emby.Server.Implementations.SyncPlay else { // Client, that was buffering, resumed playback but did not update others in time - delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; + delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); _group.LastActivity = currentTime.AddMilliseconds( delay); @@ -497,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) { // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? _group.DefaultPing); + _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing); } /// From 5a7dda337f4cdda0d0c61adef3d2b13772e708d0 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 11:50:00 -0400 Subject: [PATCH 108/153] Add active session tracking Adds a flag for a maximum number of user sessions, as well as an authentication check to ensure that the user is not above this level. --- .../Session/SessionManager.cs | 13 +++++++++++++ Jellyfin.Data/Entities/User.cs | 5 +++++ .../Users/UserManager.cs | 2 ++ MediaBrowser.Model/Users/UserPolicy.cs | 6 ++++++ 4 files changed, 26 insertions(+) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e42d478533..5903d395a2 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1484,6 +1484,19 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } + var sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; + int maxActiveSessions = user.MaxActiveSessions; + _logger.LogDebug("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); + if (maxActiveSessions >= 0 && sessionsCount >= maxActiveSessions) + { + throw new SecurityException( + "User {User} is at their maximum number of sessions ({Sessions}/{Max}).", + user.Username, + sessionsCount, + maxActiveSessions + ) + } + var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); var session = LogSessionActivity( diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index f7ab57a1b1..daa4de0b57 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -188,6 +188,11 @@ namespace Jellyfin.Data.Entities /// public int? LoginAttemptsBeforeLockout { get; set; } + /// + /// Gets or sets the maximum number of active sessions the user can have at once. + /// + public int? MaxActiveSessions { get; set; } + /// /// Gets or sets the subtitle mode. /// diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8f04baa089..43698efb75 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -379,6 +379,7 @@ namespace Jellyfin.Server.Implementations.Users PasswordResetProviderId = user.PasswordResetProviderId, InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1, + MaxActiveSessions = user.MaxActiveSessions ?? -1, IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), IsHidden = user.HasPermission(PermissionKind.IsHidden), IsDisabled = user.HasPermission(PermissionKind.IsDisabled), @@ -701,6 +702,7 @@ namespace Jellyfin.Server.Implementations.Users user.PasswordResetProviderId = policy.PasswordResetProviderId; user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; user.LoginAttemptsBeforeLockout = maxLoginAttempts; + user.MaxActiveSessions = policy.MaxActiveSessions; user.SyncPlayAccess = policy.SyncPlayAccess; user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index a1f01f7e8e..53dcb6bbd8 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -92,6 +92,10 @@ namespace MediaBrowser.Model.Users public int LoginAttemptsBeforeLockout { get; set; } + public int ActiveSessionCount { get; set; } + + public int MaxActiveSessions { get; set; } + public bool EnablePublicSharing { get; set; } public Guid[] BlockedMediaFolders { get; set; } @@ -144,6 +148,8 @@ namespace MediaBrowser.Model.Users LoginAttemptsBeforeLockout = -1; + MaxActiveSessions = -1; + EnableAllChannels = true; EnabledChannels = Array.Empty(); From b9d79d7635aa39052244f356e699bc125c88bc34 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:14:32 -0400 Subject: [PATCH 109/153] Add DB migration for new MaxActiveSessions field --- ...201004171403_MaxActiveSessions.Designer.cs | 462 ++++++++++++++++++ .../20201004171403_MaxActiveSessions.cs | 24 + .../Migrations/JellyfinDbModelSnapshot.cs | 5 +- 3 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs new file mode 100644 index 0000000000..65a25c16de --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs @@ -0,0 +1,462 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20201004171403_MaxActiveSessions")] + partial class MaxActiveSessions + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("DashboardTheme") + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_Permissions_Guid"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Guid"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EasyPassword") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("DisplayPreferences") + .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("Permission_Permissions_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Guid"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs new file mode 100644 index 0000000000..4ce6a7bd89 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class MaxActiveSessions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxActiveSessions", + schema: "jellyfin", + table: "Users", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxActiveSessions", + schema: "jellyfin", + table: "Users"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index ccfcf96b1f..3c9e1aee4b 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "3.1.7"); + .HasAnnotation("ProductVersion", "3.1.8"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -344,6 +344,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("LoginAttemptsBeforeLockout") .HasColumnType("INTEGER"); + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + b.Property("MaxParentalAgeRating") .HasColumnType("INTEGER"); From 975fca51583de23bafb6720104a2a6857ab29b18 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:19:56 -0400 Subject: [PATCH 110/153] Fix syntax error --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 5903d395a2..ac59fe3860 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1494,7 +1494,7 @@ namespace Emby.Server.Implementations.Session user.Username, sessionsCount, maxActiveSessions - ) + ); } var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); From 2577595bac615fc74c8d4b21bd0ce925a1b21b7b Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:23:20 -0400 Subject: [PATCH 111/153] Remove obsolete getter --- MediaBrowser.Model/Users/UserPolicy.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 53dcb6bbd8..9d3a1ca4db 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -92,8 +92,6 @@ namespace MediaBrowser.Model.Users public int LoginAttemptsBeforeLockout { get; set; } - public int ActiveSessionCount { get; set; } - public int MaxActiveSessions { get; set; } public bool EnablePublicSharing { get; set; } From a9e5f6e770edec9c768bf9365fbb2dac26f0b7c1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:25:07 -0400 Subject: [PATCH 112/153] Remove nullable from MaxActiveSessions --- Jellyfin.Data/Entities/User.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index daa4de0b57..6d46819143 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -191,7 +191,7 @@ namespace Jellyfin.Data.Entities /// /// Gets or sets the maximum number of active sessions the user can have at once. /// - public int? MaxActiveSessions { get; set; } + public int MaxActiveSessions { get; set; } /// /// Gets or sets the subtitle mode. From b130af1b0c869fa6a35be4a53510e0f0fa4ad0d3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:29:18 -0400 Subject: [PATCH 113/153] Remove variables from SecurityException --- Emby.Server.Implementations/Session/SessionManager.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index ac59fe3860..46fa29919a 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1489,12 +1489,7 @@ namespace Emby.Server.Implementations.Session _logger.LogDebug("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); if (maxActiveSessions >= 0 && sessionsCount >= maxActiveSessions) { - throw new SecurityException( - "User {User} is at their maximum number of sessions ({Sessions}/{Max}).", - user.Username, - sessionsCount, - maxActiveSessions - ); + throw new SecurityException("User is at their maximum number of sessions."); } var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); From 6e1d1eed23442976ac0d9b2a2af44b7a6804ea09 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:30:21 -0400 Subject: [PATCH 114/153] Make log entry informational --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 46fa29919a..cd40bda556 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1486,7 +1486,7 @@ namespace Emby.Server.Implementations.Session var sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; int maxActiveSessions = user.MaxActiveSessions; - _logger.LogDebug("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); + _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); if (maxActiveSessions >= 0 && sessionsCount >= maxActiveSessions) { throw new SecurityException("User is at their maximum number of sessions."); From cd328a0be3a8a3e20c51eb999f1b858d5263bab1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:34:53 -0400 Subject: [PATCH 115/153] Remove default set for MaxActiveSessions --- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 43698efb75..d8ec2a3cdf 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -379,7 +379,7 @@ namespace Jellyfin.Server.Implementations.Users PasswordResetProviderId = user.PasswordResetProviderId, InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1, - MaxActiveSessions = user.MaxActiveSessions ?? -1, + MaxActiveSessions = user.MaxActiveSessions, IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), IsHidden = user.HasPermission(PermissionKind.IsHidden), IsDisabled = user.HasPermission(PermissionKind.IsDisabled), From 725acb528aab34df5cd5c6b6e552e5f5e54b7fea Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 13:43:20 -0400 Subject: [PATCH 116/153] Add pragma warning disabled to migrations --- .../Migrations/20201004171403_MaxActiveSessions.Designer.cs | 4 +++- .../Migrations/20201004171403_MaxActiveSessions.cs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs index 65a25c16de..9eac077bda 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs @@ -1,4 +1,6 @@ -// +#pragma warning disable CS1591 + +// using System; using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs index 4ce6a7bd89..f6ed3c66cd 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using Microsoft.EntityFrameworkCore.Migrations; namespace Jellyfin.Server.Implementations.Migrations { From d0ec6872f3418a3bc6bacff6ba068526f45fc167 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 14:06:20 -0400 Subject: [PATCH 117/153] Increase count check to 1 There's another way to disable a user anyways. --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index cd40bda556..90363e5603 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1487,7 +1487,7 @@ namespace Emby.Server.Implementations.Session var sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; int maxActiveSessions = user.MaxActiveSessions; _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); - if (maxActiveSessions >= 0 && sessionsCount >= maxActiveSessions) + if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions) { throw new SecurityException("User is at their maximum number of sessions."); } From 8dfa2015d0e804227fe9db09caf21315da7495e4 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 14:14:42 -0400 Subject: [PATCH 118/153] Make the count an int for cleanliness --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 90363e5603..04becff49c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1484,7 +1484,7 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } - var sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; + int sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; int maxActiveSessions = user.MaxActiveSessions; _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions) From 5aa519fd399d8b09a360f6448acc992225f8c219 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 15:46:57 -0400 Subject: [PATCH 119/153] Implement better count method --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 04becff49c..d283ee8d8b 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1484,7 +1484,7 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } - int sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; + int sessionsCount = Sessions.Count(i => string.Equals(i.UserId, user.Id, StringComparison.OrdinalIgnoreCase)); int maxActiveSessions = user.MaxActiveSessions; _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions) From df177b47524c0f7c5df21f32ebeaba5576d3f8a4 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 15:52:39 -0400 Subject: [PATCH 120/153] Revert "Implement better count method" This reverts commit 5aa519fd399d8b09a360f6448acc992225f8c219. --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index d283ee8d8b..04becff49c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1484,7 +1484,7 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } - int sessionsCount = Sessions.Count(i => string.Equals(i.UserId, user.Id, StringComparison.OrdinalIgnoreCase)); + int sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; int maxActiveSessions = user.MaxActiveSessions; _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions) From 67e89dd5255a8ae38e0f0f489595d290e718b7a1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 16:57:48 -0400 Subject: [PATCH 121/153] Use nicer count comparitor --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 04becff49c..fa4caeb18c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1484,7 +1484,7 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } - int sessionsCount = Sessions.Where(i => string.Equals(i.UserId, user.Id)).ToList().Count; + int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id)); int maxActiveSessions = user.MaxActiveSessions; _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions) From fc28ffea53861ea7e8c35d71dcf863e4900f4d8f Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 19:03:39 -0400 Subject: [PATCH 122/153] Improve handling of apiclient generator 1. Replace the separate stable/unstable scripts with a single script and handle the difference with argument parsing. 2. Unify the calls and pass the Azure arguments in on the CLI. --- .ci/azure-pipelines-api-client.yml | 4 ++-- .../templates/typescript/axios/generate.sh | 19 +++++++++++++++++++ .../templates/typescript/axios/stable.sh | 9 --------- .../templates/typescript/axios/unstable.sh | 9 --------- 4 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 apiclient/templates/typescript/axios/generate.sh delete mode 100644 apiclient/templates/typescript/axios/stable.sh delete mode 100644 apiclient/templates/typescript/axios/unstable.sh diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index 7f428aec15..d120593ea8 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -34,7 +34,7 @@ jobs: displayName: 'Build unstable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: - script: 'bash ./apiclient/templates/typescript/axios/unstable.sh' + script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)" - task: Npm@1 displayName: 'Publish unstable typescript axios client' @@ -50,7 +50,7 @@ jobs: displayName: 'Build stable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: - script: 'bash ./apiclient/templates/typescript/axios/stable.sh' + script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" - task: Npm@1 displayName: 'Publish stable typescript axios client' diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh new file mode 100644 index 0000000000..c514b10d5b --- /dev/null +++ b/apiclient/templates/typescript/axios/generate.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +artifactsDirectory="${1}" +buildNumber="${2}" +if [[ -n ${buildNumber} ]]; then + # Unstable build + additionalProperties=",snapshotVersion=\"-SNAPSHOT.${buildNumber}\",npmRepository=\"https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/\"" +else + # Stable build + additionalProperties="" +fi + +java -jar openapi-generator-cli.jar generate \ + --input-spec ${artifactsDirectory}/openapispec/openapi.json \ + --generator-name typescript-axios \ + --output ./apiclient/generated/typescript/axios \ + --template-dir ./apiclient/templates/typescript/axios \ + --ignore-file-override ./apiclient/.openapi-generator-ignore \ + --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios"${additionalProperties} diff --git a/apiclient/templates/typescript/axios/stable.sh b/apiclient/templates/typescript/axios/stable.sh deleted file mode 100644 index 118ef219fe..0000000000 --- a/apiclient/templates/typescript/axios/stable.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -java -jar openapi-generator-cli.jar generate \ - --input-spec $(System.ArtifactsDirectory)/openapispec/openapi.json \ - --generator-name typescript-axios \ - --output ./apiclient/generated/typescript/axios \ - --template-dir ./apiclient/templates/typescript/axios \ - --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios" diff --git a/apiclient/templates/typescript/axios/unstable.sh b/apiclient/templates/typescript/axios/unstable.sh deleted file mode 100644 index be9f9be438..0000000000 --- a/apiclient/templates/typescript/axios/unstable.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -java -jar openapi-generator-cli.jar generate \ - --input-spec $(System.ArtifactsDirectory)/openapispec/openapi.json \ - --generator-name typescript-axios \ - --output ./apiclient/generated/typescript/axios \ - --template-dir ./apiclient/templates/typescript/axios \ - --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/" From 88bc2fa3ec1dac87981bb9ab559b3e2da11cf580 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 19:50:54 -0400 Subject: [PATCH 123/153] Just make everything under libdir/jf a wildcard I don't know if this will actually work but maybe it will. --- fedora/jellyfin.spec | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index bfb2b3be29..93fb9fb411 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -79,15 +79,7 @@ EOF %files server %attr(755,root,root) %{_bindir}/jellyfin -%{_libdir}/jellyfin/*.json -%{_libdir}/jellyfin/*.dll -%{_libdir}/jellyfin/*.so -%{_libdir}/jellyfin/*.a -%{_libdir}/jellyfin/createdump -%{_libdir}/jellyfin/*.xml -%{_libdir}/jellyfin/wwwroot/api-docs/* -%{_libdir}/jellyfin/wwwroot/api-docs/redoc/* -%{_libdir}/jellyfin/wwwroot/api-docs/swagger/* +%{_libdir}/jellyfin/* # Needs 755 else only root can run it since binary build by dotnet is 722 %attr(755,root,root) %{_libdir}/jellyfin/jellyfin %{_libdir}/jellyfin/SOS_README.md From 1bfe9713afe6abfc7845e3ab61f4a25f3c0df520 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 20:24:35 -0400 Subject: [PATCH 124/153] Make MaxSessions default 0 --- MediaBrowser.Model/Users/UserPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 9d3a1ca4db..363b2633fd 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -146,7 +146,7 @@ namespace MediaBrowser.Model.Users LoginAttemptsBeforeLockout = -1; - MaxActiveSessions = -1; + MaxActiveSessions = 0; EnableAllChannels = true; EnabledChannels = Array.Empty(); From 4d7e7d6331243bd339464bd6010569c6c308088b Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 3 Oct 2020 21:14:25 -0400 Subject: [PATCH 125/153] Rewrite activity log backend to use a query class. --- .../Controllers/ActivityLogController.cs | 20 +++++----- Jellyfin.Data/Queries/ActivityLogQuery.cs | 30 +++++++++++++++ .../Activity/ActivityManager.cs | 37 ++++++++----------- .../Activity/IActivityManager.cs | 9 +---- 4 files changed, 58 insertions(+), 38 deletions(-) create mode 100644 Jellyfin.Data/Queries/ActivityLogQuery.cs diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index a07cea9c0c..b429cebecb 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; +using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -39,19 +39,19 @@ namespace Jellyfin.Api.Controllers /// A containing the log entries. [HttpGet("Entries")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetLogEntries( + public async Task>> GetLogEntries( [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] DateTime? minDate, [FromQuery] bool? hasUserId) { - var filterFunc = new Func, IQueryable>( - entries => entries.Where(entry => entry.DateCreated >= minDate - && (!hasUserId.HasValue || (hasUserId.Value - ? entry.UserId != Guid.Empty - : entry.UserId == Guid.Empty)))); - - return _activityManager.GetPagedResult(filterFunc, startIndex, limit); + return await _activityManager.GetPagedResultAsync(new ActivityLogQuery + { + StartIndex = startIndex, + Limit = limit, + MinDate = minDate, + HasUserId = hasUserId + }).ConfigureAwait(false); } } } diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs new file mode 100644 index 0000000000..92919d3a51 --- /dev/null +++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs @@ -0,0 +1,30 @@ +using System; + +namespace Jellyfin.Data.Queries +{ + /// + /// A class representing a query to the activity logs. + /// + public class ActivityLogQuery + { + /// + /// Gets or sets the index to start at. + /// + public int? StartIndex { get; set; } + + /// + /// Gets or sets the maximum number of items to include. + /// + public int? Limit { get; set; } + + /// + /// Gets or sets a value indicating whether to take entries with a user id. + /// + public bool? HasUserId { get; set; } + + /// + /// Gets or sets the minimum date to query for. + /// + public DateTime? MinDate { get; set; } + } +} diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index abdd290d45..695e2fbd84 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -3,8 +3,10 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; +using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Activity { @@ -39,41 +41,34 @@ namespace Jellyfin.Server.Implementations.Activity } /// - public QueryResult GetPagedResult( - Func, IQueryable> func, - int? startIndex, - int? limit) + public async Task> GetPagedResultAsync(ActivityLogQuery query) { - using var dbContext = _provider.CreateContext(); + await using var dbContext = _provider.CreateContext(); - var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated)); + IQueryable entries = dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated); - if (startIndex.HasValue) + if (query.MinDate.HasValue) { - query = query.Skip(startIndex.Value); + entries = entries.Where(entry => entry.DateCreated >= query.MinDate); } - if (limit.HasValue) + if (query.HasUserId.HasValue) { - query = query.Take(limit.Value); + entries = entries.Where(entry => entry.UserId != Guid.Empty == query.HasUserId.Value ); } - // This converts the objects from the new database model to the old for compatibility with the existing API. - var list = query.Select(ConvertToOldModel).ToList(); - return new QueryResult { - Items = list, - TotalRecordCount = func(dbContext.ActivityLogs).Count() + Items = await entries.Skip(query.StartIndex ?? 0) + .Take(query.Limit ?? 100) + .Select(ConvertToOldModel) + .AsQueryable() + .ToListAsync() + .ConfigureAwait(false), + TotalRecordCount = await entries.CountAsync().ConfigureAwait(false) }; } - /// - public QueryResult GetPagedResult(int? startIndex, int? limit) - { - return GetPagedResult(logs => logs, startIndex, limit); - } - private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) { return new ActivityLogEntry diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index d5344494e8..3e4ea208ed 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,10 +1,10 @@ #pragma warning disable CS1591 using System; -using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Querying; namespace MediaBrowser.Model.Activity @@ -15,11 +15,6 @@ namespace MediaBrowser.Model.Activity Task CreateAsync(ActivityLog entry); - QueryResult GetPagedResult(int? startIndex, int? limit); - - QueryResult GetPagedResult( - Func, IQueryable> func, - int? startIndex, - int? limit); + Task> GetPagedResultAsync(ActivityLogQuery query); } } From f2763a71e06c087c940f267313e24fb15dc13d37 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 4 Oct 2020 20:31:24 -0400 Subject: [PATCH 126/153] Rename migrations to match others --- ...ner.cs => 20201004171403_AddMaxActiveSessions.Designer.cs} | 4 ++-- ...tiveSessions.cs => 20201004171403_AddMaxActiveSessions.cs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename Jellyfin.Server.Implementations/Migrations/{20201004171403_MaxActiveSessions.Designer.cs => 20201004171403_AddMaxActiveSessions.Designer.cs} (99%) rename Jellyfin.Server.Implementations/Migrations/{20201004171403_MaxActiveSessions.cs => 20201004171403_AddMaxActiveSessions.cs} (92%) diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs similarity index 99% rename from Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs rename to Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs index 9eac077bda..e5c326a326 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { [DbContext(typeof(JellyfinDb))] - [Migration("20201004171403_MaxActiveSessions")] - partial class MaxActiveSessions + [Migration("20201004171403_AddMaxActiveSessions")] + partial class AddMaxActiveSessions { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs similarity index 92% rename from Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs rename to Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs index f6ed3c66cd..8fded7d0a1 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_MaxActiveSessions.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Jellyfin.Server.Implementations.Migrations { - public partial class MaxActiveSessions : Migration + public partial class AddMaxActiveSessions : Migration { protected override void Up(MigrationBuilder migrationBuilder) { From e77040a4fba93c0ca65650ce02998e9a35f28c40 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Mon, 5 Oct 2020 00:15:25 -0400 Subject: [PATCH 127/153] Fix Transcode Cleanup Schedule Sets the default time to midnight, and confirms to the same task scheduling commands that other tasks use. --- .../ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 6914081673..dc6efcaf5f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -41,7 +41,13 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. - public IEnumerable GetDefaultTriggers() => new List(); + public IEnumerable GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, TimeOfDayTicks = TimeSpan.FromHours(0).Ticks} + }; + } /// /// Returns the task to be executed. From 375f849b3e84a6f92e1928f7adcdb6a8a4115d25 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Mon, 5 Oct 2020 02:14:43 -0400 Subject: [PATCH 128/153] Simply trigger Co-authored-by: Claus Vium --- .../ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index dc6efcaf5f..2a40f8b1e8 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -45,7 +45,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { return new[] { - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, TimeOfDayTicks = TimeSpan.FromHours(0).Ticks} + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = 0 + } }; } From 959d906c859a4a7c611907d500498a53ba92dc15 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Mon, 5 Oct 2020 02:25:09 -0400 Subject: [PATCH 129/153] Just enable instead --- .../Tasks/DeleteTranscodeFileTask.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 2a40f8b1e8..26ef193542 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -41,17 +41,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. - public IEnumerable GetDefaultTriggers() - { - return new[] - { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = 0 - } - }; - } + public IEnumerable GetDefaultTriggers() => new List(); /// /// Returns the task to be executed. @@ -158,7 +148,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public bool IsHidden => false; /// - public bool IsEnabled => false; + public bool IsEnabled => true; /// public bool IsLogged => true; From df75c1cd9c8fdcec8302a848c05d3ee36b0f7a58 Mon Sep 17 00:00:00 2001 From: cvium Date: Mon, 5 Oct 2020 08:28:37 +0200 Subject: [PATCH 130/153] SecurityException should return 403 --- Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index fb1ee3b2bb..f6c76e4d9e 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -125,8 +125,8 @@ namespace Jellyfin.Server.Middleware switch (ex) { case ArgumentException _: return StatusCodes.Status400BadRequest; - case AuthenticationException _: - case SecurityException _: return StatusCodes.Status401Unauthorized; + case AuthenticationException _: return StatusCodes.Status401Unauthorized; + case SecurityException _: return StatusCodes.Status403Forbidden; case DirectoryNotFoundException _: case FileNotFoundException _: case ResourceNotFoundException _: return StatusCodes.Status404NotFound; From 72cd6ab0712a0a532e0dda2b26afe793ad34111b Mon Sep 17 00:00:00 2001 From: cvium Date: Fri, 18 Sep 2020 15:24:56 +0200 Subject: [PATCH 131/153] Remove dummy season and missing episode provider in a futile attempt to remove cruft --- .../Configuration/LibraryOptions.cs | 2 - .../Plugins/TheTvdb/TvdbClientManager.cs | 26 -- .../TV/DummySeasonProvider.cs | 229 ---------- .../TV/MissingEpisodeProvider.cs | 404 ------------------ .../TV/SeriesMetadataService.cs | 41 +- 5 files changed, 1 insertion(+), 701 deletions(-) delete mode 100644 MediaBrowser.Providers/TV/DummySeasonProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MissingEpisodeProvider.cs diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 890469d361..54ef49ea62 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -25,8 +25,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableInternetProviders { get; set; } - public bool ImportMissingEpisodes { get; set; } - public bool EnableAutomaticSeriesGrouping { get; set; } public bool EnableEmbeddedTitles { get; set; } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index f22d484abc..5e9a4a2252 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -80,32 +80,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); } - public async Task> GetAllEpisodesAsync(int tvdbId, string language, - CancellationToken cancellationToken) - { - // Traverse all episode pages and join them together - var episodes = new List(); - var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken) - .ConfigureAwait(false); - episodes.AddRange(episodePage.Data); - if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue) - { - return episodes; - } - - int next = episodePage.Links.Next.Value; - int last = episodePage.Links.Last.Value; - - for (var page = next; page <= last; ++page) - { - episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken) - .ConfigureAwait(false); - episodes.AddRange(episodePage.Data); - } - - return episodes; - } - public Task> GetSeriesByImdbIdAsync( string imdbId, string language, diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs deleted file mode 100644 index 905cbefd32..0000000000 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ /dev/null @@ -1,229 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV -{ - public class DummySeasonProvider - { - private readonly ILogger _logger; - private readonly ILocalizationManager _localization; - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _fileSystem; - - public DummySeasonProvider( - ILogger logger, - ILocalizationManager localization, - ILibraryManager libraryManager, - IFileSystem fileSystem) - { - _logger = logger; - _localization = localization; - _libraryManager = libraryManager; - _fileSystem = fileSystem; - } - - public async Task Run(Series series, CancellationToken cancellationToken) - { - var seasonsRemoved = RemoveObsoleteSeasons(series); - - var hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false); - - if (hasNewSeasons) - { - // var directoryService = new DirectoryService(_fileSystem); - - // await series.RefreshMetadata(new MetadataRefreshOptions(directoryService), cancellationToken).ConfigureAwait(false); - - // await series.ValidateChildren(new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(directoryService)) - // .ConfigureAwait(false); - } - - return seasonsRemoved || hasNewSeasons; - } - - private async Task AddDummySeasonFolders(Series series, CancellationToken cancellationToken) - { - var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode) - .Cast() - .Where(i => !i.IsInSeasonFolder) - .ToList(); - - var hasChanges = false; - - List seasons = null; - - // Loop through the unique season numbers - foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1) - .Where(i => i >= 0) - .Distinct() - .ToList()) - { - if (seasons == null) - { - seasons = series.Children.OfType().ToList(); - } - - var existingSeason = seasons - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (existingSeason == null) - { - await AddSeason(series, seasonNumber, false, cancellationToken).ConfigureAwait(false); - hasChanges = true; - seasons = null; - } - else if (existingSeason.IsVirtualItem) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - seasons = null; - } - } - - // Unknown season - create a dummy season to put these under - if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue)) - { - if (seasons == null) - { - seasons = series.Children.OfType().ToList(); - } - - var existingSeason = seasons - .FirstOrDefault(i => !i.IndexNumber.HasValue); - - if (existingSeason == null) - { - await AddSeason(series, null, false, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - seasons = null; - } - else if (existingSeason.IsVirtualItem) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - seasons = null; - } - } - - return hasChanges; - } - - /// - /// Adds the season. - /// - public async Task AddSeason( - Series series, - int? seasonNumber, - bool isVirtualItem, - CancellationToken cancellationToken) - { - string seasonName; - if (seasonNumber == null) - { - seasonName = _localization.GetLocalizedString("NameSeasonUnknown"); - } - else if (seasonNumber == 0) - { - seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName; - } - else - { - seasonName = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameSeasonNumber"), - seasonNumber.Value); - } - - _logger.LogInformation("Creating Season {0} entry for {1}", seasonName, series.Name); - - var season = new Season - { - Name = seasonName, - IndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId( - series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, - typeof(Season)), - IsVirtualItem = isVirtualItem, - SeriesId = series.Id, - SeriesName = series.Name - }; - - season.SetParent(series); - - series.AddChild(season, cancellationToken); - - await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - - return season; - } - - private bool RemoveObsoleteSeasons(Series series) - { - var existingSeasons = series.Children.OfType().ToList(); - - var physicalSeasons = existingSeasons - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); - - var virtualSeasons = existingSeasons - .Where(i => i.LocationType == LocationType.Virtual) - .ToList(); - - var seasonsToRemove = virtualSeasons - .Where(i => - { - if (i.IndexNumber.HasValue) - { - var seasonNumber = i.IndexNumber.Value; - - // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && (p.IndexNumber.Value == seasonNumber))) - { - return true; - } - } - - // If there are no episodes with this season number, delete it - if (!i.GetEpisodes().Any()) - { - return true; - } - - return false; - }) - .ToList(); - - var hasChanges = false; - - foreach (var seasonToRemove in seasonsToRemove) - { - _logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber); - - _libraryManager.DeleteItem( - seasonToRemove, - new DeleteOptions - { - DeleteFileLocation = true - }, - false); - - hasChanges = true; - } - - return hasChanges; - } - } -} diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs deleted file mode 100644 index c833b12271..0000000000 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ /dev/null @@ -1,404 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Providers.Plugins.TheTvdb; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.TV -{ - public class MissingEpisodeProvider - { - private const double UnairedEpisodeThresholdDays = 2; - - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; - private readonly TvdbClientManager _tvdbClientManager; - - public MissingEpisodeProvider( - ILogger logger, - IServerConfigurationManager config, - ILibraryManager libraryManager, - ILocalizationManager localization, - IFileSystem fileSystem, - TvdbClientManager tvdbClientManager) - { - _logger = logger; - _config = config; - _libraryManager = libraryManager; - _localization = localization; - _fileSystem = fileSystem; - _tvdbClientManager = tvdbClientManager; - } - - public async Task Run(Series series, bool addNewItems, CancellationToken cancellationToken) - { - var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb); - if (string.IsNullOrEmpty(tvdbIdString)) - { - return false; - } - - var episodes = await _tvdbClientManager.GetAllEpisodesAsync( - int.Parse(tvdbIdString, CultureInfo.InvariantCulture), - series.GetPreferredMetadataLanguage(), - cancellationToken).ConfigureAwait(false); - - var episodeLookup = episodes - .Select(i => - { - if (!DateTime.TryParse(i.FirstAired, out var firstAired)) - { - firstAired = default; - } - - var seasonNumber = i.AiredSeason.GetValueOrDefault(-1); - var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1); - return (seasonNumber, episodeNumber, firstAired); - }) - .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1) - .OrderBy(i => i.seasonNumber) - .ThenBy(i => i.episodeNumber) - .ToList(); - - var allRecursiveChildren = series.GetRecursiveChildren(); - - var hasBadData = HasInvalidContent(allRecursiveChildren); - - // Be conservative here to avoid creating missing episodes for ones they already have - var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes; - - var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup); - - if (anySeasonsRemoved) - { - // refresh this - allRecursiveChildren = series.GetRecursiveChildren(); - } - - var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes); - - if (anyEpisodesRemoved) - { - // refresh this - allRecursiveChildren = series.GetRecursiveChildren(); - } - - var hasNewEpisodes = false; - - if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name)) - { - hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken) - .ConfigureAwait(false); - } - - if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved) - { - return true; - } - - return false; - } - - /// - /// Returns true if a series has any seasons or episodes without season or episode numbers - /// If this data is missing no virtual items will be added in order to prevent possible duplicates. - /// - private bool HasInvalidContent(IList allItems) - { - return allItems.OfType().Any(i => !i.IndexNumber.HasValue) || - allItems.OfType().Any(i => - { - if (!i.ParentIndexNumber.HasValue) - { - return true; - } - - // You could have episodes under season 0 with no number - return false; - }); - } - - private async Task AddMissingEpisodes( - Series series, - IEnumerable allItems, - bool addMissingEpisodes, - IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup, - CancellationToken cancellationToken) - { - var existingEpisodes = allItems.OfType().ToList(); - - var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count()); - - var hasChanges = false; - - foreach (var tuple in episodeLookup) - { - if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0) - { - // Ignore episode/season zeros - continue; - } - - var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple); - - if (existingEpisode != null) - { - continue; - } - - var airDate = tuple.firstAired; - - var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays); - - if ((airDate < now && addMissingEpisodes) || airDate > now) - { - // tvdb has a lot of nearly blank episodes - _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber); - await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false); - - hasChanges = true; - } - } - - return hasChanges; - } - - /// - /// Removes the virtual entry after a corresponding physical version has been added. - /// - private bool RemoveObsoleteOrMissingEpisodes( - IEnumerable allRecursiveChildren, - IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup, - bool allowMissingEpisodes) - { - var existingEpisodes = allRecursiveChildren.OfType(); - - var physicalEpisodes = new List(); - var virtualEpisodes = new List(); - foreach (var episode in existingEpisodes) - { - if (episode.LocationType == LocationType.Virtual) - { - virtualEpisodes.Add(episode); - } - else - { - physicalEpisodes.Add(episode); - } - } - - var episodesToRemove = virtualEpisodes - .Where(i => - { - if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue) - { - return true; - } - - var seasonNumber = i.ParentIndexNumber.Value; - var episodeNumber = i.IndexNumber.Value; - - // If there's a physical episode with the same season and episode number, delete it - if (physicalEpisodes.Any(p => - p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber && - p.ContainsEpisodeNumber(episodeNumber))) - { - return true; - } - - // If the episode no longer exists in the remote lookup, delete it - if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber)) - { - return true; - } - - // If it's missing, but not unaired, remove it - return !allowMissingEpisodes && i.IsMissingEpisode && - (!i.PremiereDate.HasValue || - i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) < - DateTime.Now.Date); - }); - - var hasChanges = false; - - foreach (var episodeToRemove in episodesToRemove) - { - _libraryManager.DeleteItem( - episodeToRemove, - new DeleteOptions - { - DeleteFileLocation = true - }, - false); - - hasChanges = true; - } - - return hasChanges; - } - - /// - /// Removes the obsolete or missing seasons. - /// - /// All recursive children. - /// The episode lookup. - /// . - private bool RemoveObsoleteOrMissingSeasons( - IList allRecursiveChildren, - IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup) - { - var existingSeasons = allRecursiveChildren.OfType().ToList(); - - var physicalSeasons = new List(); - var virtualSeasons = new List(); - foreach (var season in existingSeasons) - { - if (season.LocationType == LocationType.Virtual) - { - virtualSeasons.Add(season); - } - else - { - physicalSeasons.Add(season); - } - } - - var allEpisodes = allRecursiveChildren.OfType().ToList(); - - var seasonsToRemove = virtualSeasons - .Where(i => - { - if (i.IndexNumber.HasValue) - { - var seasonNumber = i.IndexNumber.Value; - - // If there's a physical season with the same number, delete it - if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal))) - { - return true; - } - - // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it - return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder); - } - - // Season does not have a number - // Remove if there are no episodes directly in series without a season number - return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder); - }); - - var hasChanges = false; - - foreach (var seasonToRemove in seasonsToRemove) - { - _libraryManager.DeleteItem( - seasonToRemove, - new DeleteOptions - { - DeleteFileLocation = true - }, - false); - - hasChanges = true; - } - - return hasChanges; - } - - /// - /// Adds the episode. - /// - /// The series. - /// The season number. - /// The episode number. - /// The cancellation token. - /// Task. - private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken) - { - var season = series.Children.OfType() - .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (season == null) - { - var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem); - season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false); - } - - var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture); - - var episode = new Episode - { - Name = name, - IndexNumber = episodeNumber, - ParentIndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId( - series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name, - typeof(Episode)), - IsVirtualItem = true, - SeasonId = season?.Id ?? Guid.Empty, - SeriesId = series.Id - }; - - season.AddChild(episode, cancellationToken); - - await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - } - - /// - /// Gets the existing episode. - /// - /// The existing episodes. - /// - /// - /// Episode. - private Episode GetExistingEpisode( - IEnumerable existingEpisodes, - IReadOnlyDictionary seasonCounts, - (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple) - { - var seasonNumber = episodeTuple.seasonNumber; - var episodeNumber = episodeTuple.episodeNumber; - - while (true) - { - var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber); - if (episode != null) - { - return episode; - } - - seasonNumber--; - - if (seasonCounts.ContainsKey(seasonNumber)) - { - episodeNumber += seasonCounts[seasonNumber]; - } - else - { - break; - } - } - - return null; - } - - private Episode GetExistingEpisode(IEnumerable existingEpisodes, int season, int episode) - => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode)); - } -} diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index a2c0e62c19..c8fc568a22 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,65 +1,26 @@ #pragma warning disable CS1591 -using System; -using System.Threading; -using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; -using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { public class SeriesMetadataService : MetadataService { - private readonly ILocalizationManager _localization; - private readonly TvdbClientManager _tvdbClientManager; - public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - ILibraryManager libraryManager, - ILocalizationManager localization, - TvdbClientManager tvdbClientManager) + ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - _localization = localization; - _tvdbClientManager = tvdbClientManager; - } - - /// - protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) - { - await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); - - var seasonProvider = new DummySeasonProvider(Logger, _localization, LibraryManager, FileSystem); - await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false); - - // TODO why does it not register this itself omg - var provider = new MissingEpisodeProvider( - Logger, - ServerConfigurationManager, - LibraryManager, - _localization, - FileSystem, - _tvdbClientManager); - - try - { - await provider.Run(item, true, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path); - } } /// From dba4df197753a2c61f80a0c01a3a57683a378a25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 12:00:58 +0000 Subject: [PATCH 132/153] Bump Moq from 4.14.5 to 4.14.6 Bumps [Moq](https://github.com/moq/moq4) from 4.14.5 to 4.14.6. - [Release notes](https://github.com/moq/moq4/releases) - [Changelog](https://github.com/moq/moq4/blob/master/CHANGELOG.md) - [Commits](https://github.com/moq/moq4/compare/v4.14.5...v4.14.6) Signed-off-by: dependabot[bot] --- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index e3a7a54286..8a559b7b62 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index d1679c2796..75b67f460f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -17,7 +17,7 @@ - + From 7e7eb3a2f90a96b1fa3a907de4e36049d111e347 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 5 Oct 2020 14:30:35 +0100 Subject: [PATCH 133/153] Add missing properties to typescript-axios gen --- apiclient/templates/typescript/axios/generate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh index c514b10d5b..111b71b82e 100644 --- a/apiclient/templates/typescript/axios/generate.sh +++ b/apiclient/templates/typescript/axios/generate.sh @@ -16,4 +16,4 @@ java -jar openapi-generator-cli.jar generate \ --output ./apiclient/generated/typescript/axios \ --template-dir ./apiclient/templates/typescript/axios \ --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios"${additionalProperties} + --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"${additionalProperties} From 49c363751a5bb54ee52e7764f223cd6263200c7c Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 5 Oct 2020 13:33:22 -0400 Subject: [PATCH 134/153] Make MaxActiveSessions not nullable Fixes a bad assumption with the previous migration. --- .../Migrations/20201004171403_AddMaxActiveSessions.cs | 2 +- .../Migrations/JellyfinDbModelSnapshot.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs index 8fded7d0a1..e6bf4192b3 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Migrations name: "MaxActiveSessions", schema: "jellyfin", table: "Users", - nullable: true); + nullable: false); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 3c9e1aee4b..16d62f4826 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -344,7 +344,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("LoginAttemptsBeforeLockout") .HasColumnType("INTEGER"); - b.Property("MaxActiveSessions") + b.Property("MaxActiveSessions") .HasColumnType("INTEGER"); b.Property("MaxParentalAgeRating") From 8bf560c41e6b1c47400a62e35485e08defe7b008 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 5 Oct 2020 13:00:07 -0600 Subject: [PATCH 135/153] fix registry name and link --- .ci/azure-pipelines-api-client.yml | 2 +- apiclient/templates/typescript/axios/generate.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index d120593ea8..1321014445 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -42,7 +42,7 @@ jobs: inputs: command: publish publishRegistry: useFeed - publishFeed: unstable + publishFeed: 'unstable@Local' workingDir: ./apiclient/generated/typescript/axios # Stable diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh index 111b71b82e..360fe9f33b 100644 --- a/apiclient/templates/typescript/axios/generate.sh +++ b/apiclient/templates/typescript/axios/generate.sh @@ -4,7 +4,7 @@ artifactsDirectory="${1}" buildNumber="${2}" if [[ -n ${buildNumber} ]]; then # Unstable build - additionalProperties=",snapshotVersion=\"-SNAPSHOT.${buildNumber}\",npmRepository=\"https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/\"" + additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable%40Local/npm/registry/" else # Stable build additionalProperties="" From b3249e849c68c3474e18f999fc085d41bff75adc Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 5 Oct 2020 16:54:51 -0400 Subject: [PATCH 136/153] Add default value of 0 --- .../Migrations/20201004171403_AddMaxActiveSessions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs index e6bf4192b3..10acb4defb 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs @@ -13,7 +13,8 @@ namespace Jellyfin.Server.Implementations.Migrations name: "MaxActiveSessions", schema: "jellyfin", table: "Users", - nullable: false); + nullable: false, + defaultValue: 0); } protected override void Down(MigrationBuilder migrationBuilder) From d4a492ef93f6b663fd4a4f7710613f06863f401f Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 5 Oct 2020 22:51:52 -0400 Subject: [PATCH 137/153] Fix activity log query. --- .../Activity/ActivityManager.cs | 9 ++++++--- .../Jellyfin.Server.Implementations.csproj | 1 + .../Users/DisplayPreferencesManager.cs | 1 + Jellyfin.Server.Implementations/Users/UserManager.cs | 11 ++++++----- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 695e2fbd84..5926abfe0d 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -45,7 +45,9 @@ namespace Jellyfin.Server.Implementations.Activity { await using var dbContext = _provider.CreateContext(); - IQueryable entries = dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated); + IQueryable entries = dbContext.ActivityLogs + .AsQueryable() + .OrderByDescending(entry => entry.DateCreated); if (query.MinDate.HasValue) { @@ -59,10 +61,11 @@ namespace Jellyfin.Server.Implementations.Activity return new QueryResult { - Items = await entries.Skip(query.StartIndex ?? 0) + Items = await entries + .Skip(query.StartIndex ?? 0) .Take(query.Limit ?? 100) + .AsAsyncEnumerable() .Select(ConvertToOldModel) - .AsQueryable() .ToListAsync() .ConfigureAwait(false), TotalRecordCount = await entries.CountAsync().ConfigureAwait(false) diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 4e79dd8d6c..17ba092588 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -24,6 +24,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 46f1c618f2..76f9433854 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -61,6 +61,7 @@ namespace Jellyfin.Server.Implementations.Users public IList ListItemDisplayPreferences(Guid userId, string client) { return _dbContext.ItemDisplayPreferences + .AsQueryable() .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client)) .ToList(); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8f04baa089..8f6a0496ab 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -108,6 +108,7 @@ namespace Jellyfin.Server.Implementations.Users { using var dbContext = _dbProvider.CreateContext(); return dbContext.Users + .AsQueryable() .Select(user => user.Id) .ToList(); } @@ -200,8 +201,8 @@ namespace Jellyfin.Server.Implementations.Users internal async Task CreateUserInternalAsync(string name, JellyfinDb dbContext) { // TODO: Remove after user item data is migrated. - var max = await dbContext.Users.AnyAsync().ConfigureAwait(false) - ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) + var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false) + ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) : 0; return new User( @@ -221,7 +222,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); } - using var dbContext = _dbProvider.CreateContext(); + await using var dbContext = _dbProvider.CreateContext(); var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); @@ -587,9 +588,9 @@ namespace Jellyfin.Server.Implementations.Users public async Task InitializeAsync() { // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - using var dbContext = _dbProvider.CreateContext(); + await using var dbContext = _dbProvider.CreateContext(); - if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) + if (await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)) { return; } From 3d69bcd19090a3e467594375db3ee25f39e3def9 Mon Sep 17 00:00:00 2001 From: cvium Date: Tue, 6 Oct 2020 08:14:56 +0200 Subject: [PATCH 138/153] Fix playbackstart not triggering in the new eventmanager --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b1ab20da22..607b322f2e 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -666,7 +666,7 @@ namespace Emby.Server.Implementations.Session } } - var eventArgs = new PlaybackProgressEventArgs + var eventArgs = new PlaybackStartEventArgs { Item = libraryItem, Users = users, From 6def4b06a8916c831ec2bd38bb066aeac7f75594 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 6 Oct 2020 14:54:17 +0200 Subject: [PATCH 139/153] Minor improvements to tmdb code --- .../Tmdb/TV/TmdbEpisodeImageProvider.cs | 14 ++++---- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 32 ++++++++--------- .../Tmdb/TV/TmdbSeasonImageProvider.cs | 13 +++---- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 19 ++++++----- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 34 +++++++++++-------- 5 files changed, 60 insertions(+), 52 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index fe9e483c67..3b7a0b254e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -67,17 +67,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) .ConfigureAwait(false); - if (episodeResult?.Images?.Stills == null) + var stills = episodeResult?.Images?.Stills; + if (stills == null) { return Enumerable.Empty(); } - var remoteImages = new List(); - - for (var i = 0; i < episodeResult.Images.Stills.Count; i++) + var remoteImages = new RemoteImageInfo[stills.Count]; + for (var i = 0; i < stills.Count; i++) { - var image = episodeResult.Images.Stills[i]; - remoteImages.Add(new RemoteImageInfo + var image = stills[i]; + remoteImages[i] = new RemoteImageInfo { Url = _tmdbClientManager.GetStillUrl(image.FilePath), CommunityRating = image.VoteAverage, @@ -88,7 +88,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score - }); + }; } return remoteImages.OrderByLanguageDescending(language); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 55f1ba7dcc..93998a1102 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -40,30 +40,29 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return Enumerable.Empty(); } - var metadataResult = await GetMetadata(searchInfo, cancellationToken); + var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); if (!metadataResult.HasMetadata) { return Enumerable.Empty(); } - var list = new List(); - var item = metadataResult.Item; - list.Add(new RemoteSearchResult + return new[] { - IndexNumber = item.IndexNumber, - Name = item.Name, - ParentIndexNumber = item.ParentIndexNumber, - PremiereDate = item.PremiereDate, - ProductionYear = item.ProductionYear, - ProviderIds = item.ProviderIds, - SearchProviderName = Name, - IndexNumberEnd = item.IndexNumberEnd - }); - - return list; + new RemoteSearchResult + { + IndexNumber = item.IndexNumber, + Name = item.Name, + ParentIndexNumber = item.ParentIndexNumber, + PremiereDate = item.PremiereDate, + ProductionYear = item.ProductionYear, + ProviderIds = item.ProviderIds, + SearchProviderName = Name, + IndexNumberEnd = item.IndexNumberEnd + } + }; } public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) @@ -137,8 +136,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { if (TmdbUtils.IsTrailerType(video)) { - var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key); - item.AddTrailerUrl(videoUrl); + item.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index 0feaa5732a..f4ed480aef 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -56,16 +56,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) .ConfigureAwait(false); - if (seasonResult?.Images?.Posters == null) + var posters = seasonResult?.Images?.Posters; + if (posters == null) { return Enumerable.Empty(); } - var remoteImages = new List(); - for (var i = 0; i < seasonResult.Images.Posters.Count; i++) + var remoteImages = new RemoteImageInfo[posters.Count]; + for (var i = 0; i < posters.Count; i++) { - var image = seasonResult.Images.Posters[i]; - remoteImages.Add(new RemoteImageInfo + var image = posters[i]; + remoteImages[i] = new RemoteImageInfo { Url = _tmdbClientManager.GetPosterUrl(image.FilePath), CommunityRating = image.VoteAverage, @@ -76,7 +77,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score - }); + }; } return remoteImages.OrderByLanguageDescending(language); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 2fdec0196b..d0c6b8b886 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -68,12 +68,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return Enumerable.Empty(); } - var remoteImages = new List(); + var posters = series.Images.Posters; + var backdrops = series.Images.Backdrops; - for (var i = 0; i < series.Images.Posters.Count; i++) + var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count]; + + for (var i = 0; i < posters.Count; i++) { - var poster = series.Images.Posters[i]; - remoteImages.Add(new RemoteImageInfo + var poster = posters[i]; + remoteImages[i] = new RemoteImageInfo { Url = _tmdbClientManager.GetPosterUrl(poster.FilePath), CommunityRating = poster.VoteAverage, @@ -84,13 +87,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score - }); + }; } - for (var i = 0; i < series.Images.Backdrops.Count; i++) + for (var i = 0; i < backdrops.Count; i++) { var backdrop = series.Images.Backdrops[i]; - remoteImages.Add(new RemoteImageInfo + remoteImages[posters.Count + i] = new RemoteImageInfo { Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath), CommunityRating = backdrop.VoteAverage, @@ -100,7 +103,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV ProviderName = Name, Type = ImageType.Backdrop, RatingType = RatingType.Score - }); + }; } return remoteImages.OrderByLanguageDescending(language); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 7944dfe27c..942c85b90d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -66,14 +66,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken) .ConfigureAwait(false); - if (findResult?.TvResults != null) + var tvResults = findResult?.TvResults; + if (tvResults != null) { - var imdbIdResults = new List(); - for (var i = 0; i < findResult.TvResults.Count; i++) + var imdbIdResults = new RemoteSearchResult[tvResults.Count]; + for (var i = 0; i < tvResults.Count; i++) { - var remoteResult = MapSearchTvToRemoteSearchResult(findResult.TvResults[i]); + var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]); remoteResult.SetProviderId(MetadataProvider.Imdb, imdbId); - imdbIdResults.Add(remoteResult); + imdbIdResults[i] = remoteResult; } return imdbIdResults; @@ -88,14 +89,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken) .ConfigureAwait(false); - if (findResult?.TvResults != null) + var tvResults = findResult?.TvResults; + if (tvResults != null) { - var tvIdResults = new List(); - for (var i = 0; i < findResult.TvResults.Count; i++) + var tvIdResults = new RemoteSearchResult[tvResults.Count]; + for (var i = 0; i < tvResults.Count; i++) { - var remoteResult = MapSearchTvToRemoteSearchResult(findResult.TvResults[i]); + var remoteResult = MapSearchTvToRemoteSearchResult(tvResults[i]); remoteResult.SetProviderId(MetadataProvider.Tvdb, tvdbId); - tvIdResults.Add(remoteResult); + tvIdResults[i] = remoteResult; } return tvIdResults; @@ -105,10 +107,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken) .ConfigureAwait(false); - var remoteResults = new List(); + var remoteResults = new RemoteSearchResult[tvSearchResults.Count]; for (var i = 0; i < tvSearchResults.Count; i++) { - remoteResults.Add(MapSearchTvToRemoteSearchResult(tvSearchResults[i])); + remoteResults[i] = MapSearchTvToRemoteSearchResult(tvSearchResults[i]); } return remoteResults; @@ -236,7 +238,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode) { - var series = new Series {Name = seriesResult.Name, OriginalTitle = seriesResult.OriginalName}; + var series = new Series + { + Name = seriesResult.Name, + OriginalTitle = seriesResult.OriginalName + }; series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture)); @@ -322,7 +328,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { if (TmdbUtils.IsTrailerType(video)) { - series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}"); + series.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key); } } } From 508b7c0a4e24d52fa8c10a29f6fb5c1cf77a65d2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 7 Oct 2020 17:16:03 -0600 Subject: [PATCH 140/153] Add missing general commands --- MediaBrowser.Model/Session/GeneralCommandType.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs index 5a9042d5f9..dce33210bb 100644 --- a/MediaBrowser.Model/Session/GeneralCommandType.cs +++ b/MediaBrowser.Model/Session/GeneralCommandType.cs @@ -43,6 +43,9 @@ namespace MediaBrowser.Model.Session Guide = 32, ToggleStats = 33, PlayMediaSource = 34, - PlayTrailers = 35 + PlayTrailers = 35, + SetShuffleQueue = 36, + Playstate = 37, + PlayNext = 38 } } From e9f17f3cab621929eacb6680a48246a4033d727d Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 7 Oct 2020 17:26:02 -0600 Subject: [PATCH 141/153] Add missing general commands for Kodi --- MediaBrowser.Model/Session/GeneralCommandType.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs index dce33210bb..62fa436b5e 100644 --- a/MediaBrowser.Model/Session/GeneralCommandType.cs +++ b/MediaBrowser.Model/Session/GeneralCommandType.cs @@ -46,6 +46,8 @@ namespace MediaBrowser.Model.Session PlayTrailers = 35, SetShuffleQueue = 36, Playstate = 37, - PlayNext = 38 + PlayNext = 38, + ToggleOsdMenu = 39, + Play = 40 } } From fe49ae7cb558fe86f23cb11f6f564db59828509f Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 8 Oct 2020 07:21:55 -0600 Subject: [PATCH 142/153] fix casing --- MediaBrowser.Model/Session/GeneralCommandType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs index 62fa436b5e..c58fa9a6b9 100644 --- a/MediaBrowser.Model/Session/GeneralCommandType.cs +++ b/MediaBrowser.Model/Session/GeneralCommandType.cs @@ -45,7 +45,7 @@ namespace MediaBrowser.Model.Session PlayMediaSource = 34, PlayTrailers = 35, SetShuffleQueue = 36, - Playstate = 37, + PlayState = 37, PlayNext = 38, ToggleOsdMenu = 39, Play = 40 From 2a6ea45ee1106a46c9947995d3b4e314ce68c495 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 08:43:59 -0600 Subject: [PATCH 143/153] Run npm install task --- .ci/azure-pipelines-api-client.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index 1321014445..8b03b233fb 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -28,6 +28,12 @@ jobs: inputs: script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" + - task: Npm@1 + displayName: 'Install npm dependencies' + inputs: + command: install + workingDir: ./apiclient/generated/typescript/axios + # Generate npm api client # Unstable - task: CmdLine@2 From 60c0cc1d1cd1b6ebd58b47021652782ceddf232e Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 08:51:22 -0600 Subject: [PATCH 144/153] Fix feed name and url --- .ci/azure-pipelines-api-client.yml | 2 +- apiclient/templates/typescript/axios/generate.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index 8b03b233fb..35a494cd6b 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -48,7 +48,7 @@ jobs: inputs: command: publish publishRegistry: useFeed - publishFeed: 'unstable@Local' + publishFeed: 'jellyfin/unstable' workingDir: ./apiclient/generated/typescript/axios # Stable diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh index 360fe9f33b..8c4d742825 100644 --- a/apiclient/templates/typescript/axios/generate.sh +++ b/apiclient/templates/typescript/axios/generate.sh @@ -4,7 +4,7 @@ artifactsDirectory="${1}" buildNumber="${2}" if [[ -n ${buildNumber} ]]; then # Unstable build - additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable%40Local/npm/registry/" + additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/" else # Stable build additionalProperties="" From 5109d558d927a3aa5ebc12ab4c961f0213946197 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 09:52:05 -0600 Subject: [PATCH 145/153] fix npm install order --- .ci/azure-pipelines-api-client.yml | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index 35a494cd6b..fc89b90d4e 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -28,13 +28,7 @@ jobs: inputs: script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" - - task: Npm@1 - displayName: 'Install npm dependencies' - inputs: - command: install - workingDir: ./apiclient/generated/typescript/axios - -# Generate npm api client +## Generate npm api client # Unstable - task: CmdLine@2 displayName: 'Build unstable typescript axios client' @@ -42,6 +36,22 @@ jobs: inputs: script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)" +# Stable + - task: CmdLine@2 + displayName: 'Build stable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" + +## Run npm install + - task: Npm@1 + displayName: 'Install npm dependencies' + inputs: + command: install + workingDir: ./apiclient/generated/typescript/axios + +## Publish npm packages +# Unstable - task: Npm@1 displayName: 'Publish unstable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') @@ -52,12 +62,6 @@ jobs: workingDir: ./apiclient/generated/typescript/axios # Stable - - task: CmdLine@2 - displayName: 'Build stable typescript axios client' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - inputs: - script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" - - task: Npm@1 displayName: 'Publish stable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') From 17b20aa0c03b17cf41494983115127abc6f7ede1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 17:03:45 -0600 Subject: [PATCH 146/153] Fix comma delimited array model binder --- Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 13469194a0..208566dc8e 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -18,11 +18,6 @@ namespace Jellyfin.Api.ModelBinders var elementType = bindingContext.ModelType.GetElementType(); var converter = TypeDescriptor.GetConverter(elementType); - if (valueProviderResult == ValueProviderResult.None) - { - return Task.CompletedTask; - } - if (valueProviderResult.Length > 1) { var result = Array.CreateInstance(elementType, valueProviderResult.Length); From 6a3a193dbbe856a22662f495af04aca729495d1f Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 17:10:06 -0600 Subject: [PATCH 147/153] Fix and clean tests --- .../CommaDelimitedArrayModelBinderTests.cs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs index c801b4a523..89c7d62f79 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -1,17 +1,13 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Globalization; -using System.Text; using System.Threading.Tasks; using Jellyfin.Api.ModelBinders; -using MediaBrowser.Controller.Entities; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Primitives; using Moq; using Xunit; -using Xunit.Sdk; namespace Jellyfin.Api.Tests.ModelBinders { @@ -21,14 +17,14 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery() { var queryParamName = "test"; - var queryParamValues = new string[] { "lol", "xd" }; + var queryParamValues = new[] { "lol", "xd" }; var queryParamString = "lol,xd"; var queryParamType = typeof(string[]); var modelBinder = new CommaDelimitedArrayModelBinder(); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); @@ -46,14 +42,14 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery() { var queryParamName = "test"; - var queryParamValues = new int[] { 42, 0 }; + var queryParamValues = new[] { 42, 0 }; var queryParamString = "42,0"; var queryParamType = typeof(int[]); var modelBinder = new CommaDelimitedArrayModelBinder(); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); @@ -71,14 +67,14 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery() { var queryParamName = "test"; - var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamValues = new[] { TestType.How, TestType.Much }; var queryParamString = "How,Much"; var queryParamType = typeof(TestType[]); var modelBinder = new CommaDelimitedArrayModelBinder(); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); @@ -96,14 +92,14 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas() { var queryParamName = "test"; - var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamValues = new[] { TestType.How, TestType.Much }; var queryParamString = "How,,Much"; var queryParamType = typeof(TestType[]); var modelBinder = new CommaDelimitedArrayModelBinder(); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); @@ -121,7 +117,7 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() { var queryParamName = "test"; - var queryParamValues = new TestType[] { TestType.How, TestType.Much }; + var queryParamValues = new[] { TestType.How, TestType.Much }; var queryParamString1 = "How"; var queryParamString2 = "Much"; var queryParamType = typeof(TestType[]); @@ -130,9 +126,9 @@ namespace Jellyfin.Api.Tests.ModelBinders var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() + new QueryCollection(new Dictionary { - { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) }, + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); @@ -158,7 +154,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() + new QueryCollection(new Dictionary { { queryParamName, new StringValues(value: null) }, }), @@ -171,7 +167,8 @@ namespace Jellyfin.Api.Tests.ModelBinders await modelBinder.BindModelAsync(bindingContextMock.Object); - Assert.False(bindingContextMock.Object.Result.IsModelSet); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues); } [Fact] @@ -184,7 +181,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var modelBinder = new CommaDelimitedArrayModelBinder(); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() { { queryParamName, new StringValues(queryParamString) } }), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider); @@ -201,7 +198,6 @@ namespace Jellyfin.Api.Tests.ModelBinders public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2() { var queryParamName = "test"; - var queryParamValues = new TestType[] { TestType.How, TestType.Much }; var queryParamString1 = "How"; var queryParamString2 = "😱"; var queryParamType = typeof(TestType[]); @@ -210,9 +206,9 @@ namespace Jellyfin.Api.Tests.ModelBinders var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary() + new QueryCollection(new Dictionary { - { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) }, + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, }), CultureInfo.InvariantCulture); var bindingContextMock = new Mock(); From 67722a2034d9ab4b3d6df82f11c2c12a5bfedf38 Mon Sep 17 00:00:00 2001 From: shakesac Date: Fri, 9 Oct 2020 23:05:24 +0000 Subject: [PATCH 148/153] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index c1fb65743d..a7c17ecb63 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -26,7 +26,7 @@ "HeaderLiveTV": "TV em Direto", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", - "HomeVideos": "Videos caseiros", + "HomeVideos": "Vídeos Caseiros", "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", From 549f2284ad87e78bcf43cc9baec2ce7977769dc4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 17:41:17 -0600 Subject: [PATCH 149/153] Convert filters string to enum. --- Jellyfin.Api/Controllers/ArtistsController.cs | 12 ++++++------ Jellyfin.Api/Controllers/ChannelsController.cs | 12 ++++++------ Jellyfin.Api/Controllers/GenresController.cs | 6 +++--- Jellyfin.Api/Controllers/ItemsController.cs | 4 ++-- Jellyfin.Api/Controllers/MusicGenresController.cs | 6 +++--- Jellyfin.Api/Controllers/PersonsController.cs | 4 ++-- Jellyfin.Api/Controllers/StudiosController.cs | 6 +++--- Jellyfin.Api/Controllers/TrailersController.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 12 ------------ 9 files changed, 26 insertions(+), 38 deletions(-) diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index d38214116c..784ecaeb32 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -54,7 +54,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. /// Optional filter by MediaType. Allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? genres, @@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers }).Where(i => i != null).Select(i => i!.Id).ToArray(); } - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { @@ -263,7 +263,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. /// Optional filter by MediaType. Allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. @@ -298,7 +298,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? genres, @@ -397,7 +397,7 @@ namespace Jellyfin.Api.Controllers }).Where(i => i != null).Select(i => i!.Id).ToArray(); } - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 33a969f859..fda00b8d46 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -105,7 +105,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. Sort Order - Ascending,Descending. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Channel items returned. @@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? sortOrder, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] string? sortBy, [FromQuery] string? fields) { @@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers .AddItemFields(fields) }; - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { @@ -183,7 +183,7 @@ namespace Jellyfin.Api.Controllers /// Optional. User Id. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. Specify one or more channel id's, comma delimited. /// Latest channel items returned. @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] string? fields, [FromQuery] string? channelIds) { @@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers .AddItemFields(fields) }; - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index de6aa86c94..f4b69b312e 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. /// Optional filter by MediaType. Allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. @@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? genres, @@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers .ToArray(); } - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 652c4689d0..1f73bd8582 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? imageTypes, @@ -365,7 +365,7 @@ namespace Jellyfin.Api.Controllers query.CollapseBoxSetItems = false; } - foreach (var filter in RequestHelpers.GetFilters(filters!)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 570ae8fdc7..d216b7f728 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. /// Optional filter by MediaType. Allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? genres, @@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers .ToArray(); } - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 8bd610dad9..bbef1ff9a1 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? genres, @@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers .ToArray(); } - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index cdd5f958e9..3a8ac1b243 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. /// Optional filter by MediaType. Allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? genres, @@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers .ToArray(); } - foreach (var filter in RequestHelpers.GetFilters(filters)) + foreach (var filter in filters) { switch (filter) { diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 5157b08ae5..08d9f78fc5 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -146,7 +146,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? parentId, [FromQuery] string? fields, [FromQuery] string? excludeItemTypes, - [FromQuery] string? filters, + [FromQuery] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] string? mediaTypes, [FromQuery] string? imageTypes, diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 8dcf08af56..fb5c283f59 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -56,18 +56,6 @@ namespace Jellyfin.Api.Helpers return result; } - /// - /// Get parsed filters. - /// - /// The filters. - /// Item filters. - public static IEnumerable GetFilters(string? filters) - { - return string.IsNullOrEmpty(filters) - ? Array.Empty() - : filters.Split(',').Select(v => Enum.Parse(v, true)); - } - /// /// Splits a string at a separating character into an array of substrings. /// From ed4b470397c41ba1c86b2f4161874ef741e275c5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 17:46:39 -0600 Subject: [PATCH 150/153] Convert exclude location type string to enum. --- Jellyfin.Api/Controllers/ItemsController.cs | 9 +++------ Jellyfin.Api/Controllers/TrailersController.cs | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 652c4689d0..fd7187cd4f 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isHd, [FromQuery] bool? is4K, [FromQuery] string? locationTypes, - [FromQuery] string? excludeLocationTypes, + [FromQuery] LocationType[] excludeLocationTypes, [FromQuery] bool? isMissing, [FromQuery] bool? isUnaired, [FromQuery] double? minCommunityRating, @@ -406,12 +406,9 @@ namespace Jellyfin.Api.Controllers } // ExcludeLocationTypes - if (!string.IsNullOrEmpty(excludeLocationTypes)) + if (excludeLocationTypes.Any(t => t == LocationType.Virtual)) { - if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual)) - { - query.IsVirtualItem = false; - } + query.IsVirtualItem = false; } if (!string.IsNullOrEmpty(locationTypes)) diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 5157b08ae5..bf2dfcc395 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,6 +1,7 @@ using System; using Jellyfin.Api.Constants; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isHd, [FromQuery] bool? is4K, [FromQuery] string? locationTypes, - [FromQuery] string? excludeLocationTypes, + [FromQuery] LocationType[] excludeLocationTypes, [FromQuery] bool? isMissing, [FromQuery] bool? isUnaired, [FromQuery] double? minCommunityRating, From 1bbd8df92b1a5ef1661693810f089d9884e416d3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 9 Oct 2020 18:02:53 -0600 Subject: [PATCH 151/153] Remove references to legacy scripts --- deployment/build.windows.amd64 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index afe4905769..a9daa6a239 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -35,10 +35,6 @@ unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmp cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir} rm -rf ${addin_build_dir} -# Prepare scripts -cp ${SOURCE_DIR}/windows/legacy/install-jellyfin.ps1 ${output_dir}/install-jellyfin.ps1 -cp ${SOURCE_DIR}/windows/legacy/install.bat ${output_dir}/install.bat - # Create zip package pushd dist zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version} From 6f9beef7323cf133af481d003c7db7a7c6abc6dd Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Mon, 12 Oct 2020 19:22:33 +0200 Subject: [PATCH 152/153] Fix SA1012 and SA1013 warnings --- .../Data/SqliteItemRepository.cs | 2 +- .../LiveTv/RefreshChannelsScheduledTask.cs | 2 +- .../Tasks/DeleteCacheFileTask.cs | 2 +- .../ScheduledTasks/Tasks/DeleteLogFileTask.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 18 +++++++++--------- .../MediaInfo/SubtitleScheduledTask.cs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d09f84e174..86a9908bef 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5357,7 +5357,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type itemCountColumns = new Dictionary() { - { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"} + { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" } }; } diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index f1b61f7c7d..582b649235 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 5adcefc1fe..692d1667d2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 54e18eaea0..184d155d44 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { return new[] { - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c5529ad5bd..906afa7f6f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -70,15 +70,15 @@ namespace MediaBrowser.Controller.MediaEncoding var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { - {"qsv", hwEncoder + "_qsv"}, - {hwEncoder + "_qsv", hwEncoder + "_qsv"}, - {"nvenc", hwEncoder + "_nvenc"}, - {"amf", hwEncoder + "_amf"}, - {"omx", hwEncoder + "_omx"}, - {hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m"}, - {"mediacodec", hwEncoder + "_mediacodec"}, - {"vaapi", hwEncoder + "_vaapi"}, - {"videotoolbox", hwEncoder + "_videotoolbox"} + { "qsv", hwEncoder + "_qsv" }, + { hwEncoder + "_qsv", hwEncoder + "_qsv" }, + { "nvenc", hwEncoder + "_nvenc" }, + { "amf", hwEncoder + "_amf" }, + { "omx", hwEncoder + "_omx" }, + { hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m" }, + { "mediacodec", hwEncoder + "_mediacodec" }, + { "vaapi", hwEncoder + "_vaapi" }, + { "videotoolbox", hwEncoder + "_videotoolbox" } }; if (!string.IsNullOrEmpty(hwType) diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index d231bfa2fc..9804ec3bb4 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -214,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } } From ec91d3893d6f1aee7f75dabae3d2311f7592440b Mon Sep 17 00:00:00 2001 From: Jan-Pieter Baert Date: Mon, 12 Oct 2020 20:05:11 +0200 Subject: [PATCH 153/153] Fix SA 1116 warnings --- .../Data/BaseSqliteRepository.cs | 3 +- .../Data/SqliteItemRepository.cs | 33 ++++++++++++------- .../Data/SqliteUserDataRepository.cs | 9 +++-- .../Library/Resolvers/Audio/AudioResolver.cs | 6 ++-- .../Library/Resolvers/Books/BookResolver.cs | 3 +- .../Security/AuthenticationRepository.cs | 18 ++++++---- .../Entities/BaseItemExtensions.cs | 3 +- MediaBrowser.Controller/Entities/Folder.cs | 9 +++-- .../Entities/UserViewBuilder.cs | 6 ++-- .../Library/ILibraryManager.cs | 3 +- .../MediaEncoding/EncodingHelper.cs | 3 +- .../MediaEncoding/EncodingJobInfo.cs | 6 ++-- .../MediaEncoding/IMediaEncoder.cs | 3 +- .../Resolvers/IItemResolver.cs | 3 +- .../Dlna/ContentFeatureBuilder.cs | 12 ++++--- MediaBrowser.Model/Dlna/DeviceProfile.cs | 3 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 15 ++++++--- .../Plugins/TheTvdb/TvdbClientManager.cs | 3 +- .../Plugins/TheTvdb/TvdbEpisodeProvider.cs | 3 +- 19 files changed, 96 insertions(+), 48 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 0fb050a7a5..8c756a7f4c 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data protected bool TableExists(ManagedConnection connection, string name) { - return connection.RunInTransaction(db => + return connection.RunInTransaction( + db => { using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 86a9908bef..1c2aeda709 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data { connection.RunQueries(queries); - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var existingColumnNames = GetColumnNames(db, "AncestorIds"); AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); @@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) { @@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { SaveItemsInTranscation(db, tuples); }, TransactionMode); @@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { // First delete chapters db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); @@ -2921,7 +2925,8 @@ namespace Emby.Server.Implementations.Data var result = new QueryResult(); using (var connection = GetConnection(true)) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var statements = PrepareAll(db, statementTexts); @@ -3324,7 +3329,8 @@ namespace Emby.Server.Implementations.Data var result = new QueryResult(); using (var connection = GetConnection(true)) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var statements = PrepareAll(db, statementTexts); @@ -4899,7 +4905,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { connection.ExecuteAll(sql); }, TransactionMode); @@ -4950,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var idBlob = id.ToByteArray(); @@ -5744,7 +5752,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var itemIdBlob = itemId.ToByteArray(); @@ -5898,7 +5907,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var itemIdBlob = id.ToByteArray(); @@ -6232,7 +6242,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var itemIdBlob = id.ToByteArray(); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4a78aac8e6..2c4e8e0fcc 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data var users = userDatasTableExists ? null : userManager.Users; - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { db.ExecuteAll(string.Join(";", new[] { @@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { SaveUserData(db, internalUserId, key, userData); }, TransactionMode); @@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { foreach (var userItemData in userDataList) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 03059e6d35..70be524116 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// The priority. public override ResolverPriority Priority => ResolverPriority.Fourth; - public MultiItemResolverResult ResolveMultiple(Folder parent, + public MultiItemResolverResult ResolveMultiple( + Folder parent, List files, string collectionType, IDirectoryService directoryService) @@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return result; } - private MultiItemResolverResult ResolveMultipleInternal(Folder parent, + private MultiItemResolverResult ResolveMultipleInternal( + Folder parent, List files, string collectionType, IDirectoryService directoryService) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 86a5d8b7d8..59af7ce8ac 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var fileExtension = Path.GetExtension(f.FullName) ?? string.Empty; - return _validExtensions.Contains(fileExtension, + return _validExtensions.Contains( + fileExtension, StringComparer .OrdinalIgnoreCase); }).ToList(); diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 29393ae07d..4bc12f44a4 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Security { if (tableNewlyCreated && TableExists(connection, "AccessTokens")) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var existingColumnNames = GetColumnNames(db, "AccessTokens"); @@ -88,7 +89,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)")) { @@ -119,7 +121,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id")) { @@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id")) { @@ -346,7 +350,8 @@ namespace Emby.Server.Implementations.Security { using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => + return connection.RunInTransaction( + db => { using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId")) { @@ -377,7 +382,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))")) { diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 8a69971d0f..c65477d39a 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -45,7 +45,8 @@ namespace MediaBrowser.Controller.Entities { if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase)) { - item.SetImage(new ItemImageInfo + item.SetImage( + new ItemImageInfo { Path = file, Type = imageType diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 901ea875bc..35ddaffadc 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -255,7 +255,8 @@ namespace MediaBrowser.Controller.Entities var id = child.Id; if (dictionary.ContainsKey(id)) { - Logger.LogError("Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", + Logger.LogError( + "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", Path ?? Name, child.Path ?? child.Name); } @@ -984,7 +985,8 @@ namespace MediaBrowser.Controller.Entities return items; } - private static bool CollapseBoxSetItems(InternalItemsQuery query, + private static bool CollapseBoxSetItems( + InternalItemsQuery query, BaseItem queryParent, User user, IServerConfigurationManager configurationManager) @@ -1593,7 +1595,8 @@ namespace MediaBrowser.Controller.Entities /// The date played. /// if set to true [reset position]. /// Task. - public override void MarkPlayed(User user, + public override void MarkPlayed( + User user, DateTime? datePlayed, bool resetPosition) { diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 068a767698..7bb311900e 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -343,7 +343,8 @@ namespace MediaBrowser.Controller.Entities { var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); - var result = _tvSeriesManager.GetNextUp(new NextUpQuery + var result = _tvSeriesManager.GetNextUp( + new NextUpQuery { Limit = query.Limit, StartIndex = query.StartIndex, @@ -443,7 +444,8 @@ namespace MediaBrowser.Controller.Entities return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager); } - public static QueryResult PostFilterAndSort(IEnumerable items, + public static QueryResult PostFilterAndSort( + IEnumerable items, BaseItem queryParent, int? totalRecordLimit, InternalItemsQuery query, diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 332730bcca..32703c2fd1 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -566,7 +566,8 @@ namespace MediaBrowser.Controller.Library int GetCount(InternalItemsQuery query); - void AddExternalSubtitleStreams(List streams, + void AddExternalSubtitleStreams( + List streams, string videoPath, string[] files); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 906afa7f6f..f913c32e43 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1822,7 +1822,8 @@ namespace MediaBrowser.Controller.MediaEncoding return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); } - public List GetScalingFilters(EncodingJobInfo state, + public List GetScalingFilters( + EncodingJobInfo state, int? videoWidth, int? videoHeight, Video3DFormat? threedFormat, diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index c7ec878d2d..6cd0c70d2f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -342,7 +342,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); - var newSize = DrawingUtils.Resize(size, + var newSize = DrawingUtils.Resize( + size, BaseRequest.Width ?? 0, BaseRequest.Height ?? 0, BaseRequest.MaxWidth ?? 0, @@ -368,7 +369,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); - var newSize = DrawingUtils.Resize(size, + var newSize = DrawingUtils.Resize( + size, BaseRequest.Width ?? 0, BaseRequest.Height ?? 0, BaseRequest.MaxWidth ?? 0, diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 17d6dc5d27..f6bc1f4de9 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -68,7 +68,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Extracts the video images on interval. /// - Task ExtractVideoImagesOnInterval(string[] inputFiles, + Task ExtractVideoImagesOnInterval( + string[] inputFiles, string container, MediaStream videoStream, MediaProtocol protocol, diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index eb7fb793a4..75286eadc0 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -29,7 +29,8 @@ namespace MediaBrowser.Controller.Resolvers public interface IMultiItemResolver { - MultiItemResolverResult ResolveMultiple(Folder parent, + MultiItemResolverResult ResolveMultiple( + Folder parent, List files, string collectionType, IDirectoryService directoryService); diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 93e60753ae..8b73ecbd4d 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -38,7 +38,8 @@ namespace MediaBrowser.Model.Dlna ";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); - ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container, + ResponseProfile mediaProfile = _profile.GetImageMediaProfile( + container, width, height); @@ -160,7 +161,8 @@ namespace MediaBrowser.Model.Dlna string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); - ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container, + ResponseProfile mediaProfile = _profile.GetVideoMediaProfile( + container, audioCodec, videoCodec, width, @@ -221,7 +223,8 @@ namespace MediaBrowser.Model.Dlna private static string GetImageOrgPnValue(string container, int? width, int? height) { MediaFormatProfile? format = new MediaFormatProfileResolver() - .ResolveImageFormat(container, + .ResolveImageFormat( + container, width, height); @@ -231,7 +234,8 @@ namespace MediaBrowser.Model.Dlna private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) { MediaFormatProfile? format = new MediaFormatProfileResolver() - .ResolveAudioFormat(container, + .ResolveAudioFormat( + container, audioBitrate, audioSampleRate, audioChannels); diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 7e921b1fdf..44412f3e41 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -277,7 +277,8 @@ namespace MediaBrowser.Model.Dlna return null; } - public ResponseProfile GetVideoMediaProfile(string container, + public ResponseProfile GetVideoMediaProfile( + string container, string audioCodec, string videoCodec, int? width, diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index fc0aad0727..4959a9b922 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -455,7 +455,8 @@ namespace MediaBrowser.Model.Dlna if (directPlayProfile == null) { - _logger.LogInformation("Profile: {0}, No audio direct play profiles found for {1} with codec {2}", + _logger.LogInformation( + "Profile: {0}, No audio direct play profiles found for {1} with codec {2}", options.Profile.Name ?? "Unknown Profile", item.Path ?? "Unknown path", audioStream.Codec ?? "Unknown codec"); @@ -679,7 +680,8 @@ namespace MediaBrowser.Model.Dlna bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1); bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1); - _logger.LogInformation("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", + _logger.LogInformation( + "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", options.Profile.Name ?? "Unknown Profile", item.Path ?? "Unknown path", isEligibleForDirectPlay, @@ -973,7 +975,8 @@ namespace MediaBrowser.Model.Dlna if (directPlay == null) { - _logger.LogInformation("Profile: {0}, No video direct play profiles found for {1} with codec {2}", + _logger.LogInformation( + "Profile: {0}, No video direct play profiles found for {1} with codec {2}", profile.Name ?? "Unknown Profile", mediaSource.Path ?? "Unknown path", videoStream.Codec ?? "Unknown codec"); @@ -1137,7 +1140,8 @@ namespace MediaBrowser.Model.Dlna private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource) { - _logger.LogInformation("Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}", + _logger.LogInformation( + "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}", type, profile.Name ?? "Unknown Profile", condition.Property, @@ -1342,7 +1346,8 @@ namespace MediaBrowser.Model.Dlna if (itemBitrate > requestedMaxBitrate) { - _logger.LogInformation("Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", + _logger.LogInformation( + "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", playMethod, itemBitrate, requestedMaxBitrate); return false; } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index 5e9a4a2252..ce0dab701d 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -150,7 +150,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb string language, CancellationToken cancellationToken) { - searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb), + searchInfo.SeriesProviderIds.TryGetValue( + nameof(MetadataProvider.Tvdb), out var seriesTvdbId); var episodeQuery = new EpisodeQuery(); diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 5fa8a3e1ca..fd72ea4a81 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -106,7 +106,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb .ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + _logger.LogError( + "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); return result; }