From 2363ad544979adf32207fa927f106fadb784f1fb Mon Sep 17 00:00:00 2001 From: redSpoutnik <15638041+redSpoutnik@users.noreply.github.com> Date: Fri, 8 May 2020 21:53:38 +0200 Subject: [PATCH 001/258] Add Post subtitle in API --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 55 +++++++++++++++ .../Subtitles/ISubtitleManager.cs | 6 ++ .../Subtitles/SubtitleManager.cs | 70 ++++++++++++------- 3 files changed, 104 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index f2968c6b5c..ed2533f6d4 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -37,6 +37,31 @@ namespace MediaBrowser.Api.Subtitles public int Index { get; set; } } + [Route("/Videos/{Id}/Subtitles", "POST", Summary = "Upload an external subtitle file")] + [Authenticated(Roles = "admin")] + public class PostSubtitle : IReturnVoid + { + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Language { get; set; } + + [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Format { get; set; } + + [ApiMember(Name = "IsForced", Description = "IsForced", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string IsForced { get; set; } + + [ApiMember(Name = "Data", Description = "Data", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Data { get; set; } + + } + [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")] [Authenticated] public class SearchRemoteSubtitles : IReturn @@ -270,6 +295,36 @@ namespace MediaBrowser.Api.Subtitles return _subtitleManager.DeleteSubtitles(item, request.Index); } + public void Post(PostSubtitle request) + { + var video = (Video)_libraryManager.GetItemById(request.Id); + + var bytes = Convert.FromBase64String(request.Data); + var memoryStream = new MemoryStream(bytes) + { + Position = 0 + }; + + Task.Run(async () => + { + try + { + await _subtitleManager.UploadSubtitle(video, new SubtitleResponse { + Format = request.Format, + Language = request.Language, + IsForced = Convert.ToBoolean(request.IsForced), + Stream = memoryStream + }).ConfigureAwait(false); + + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error uploading subtitle"); + } + }); + } + public async Task Get(GetRemoteSubtitles request) { var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 39538aacd7..556e2f66c8 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -50,6 +51,11 @@ namespace MediaBrowser.Controller.Subtitles /// Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken); + /// + /// Upload new subtitle. + /// + Task UploadSubtitle(Video video, SubtitleResponse response); + /// /// Gets the remote subtitles. /// diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 127d29c04e..2b55618a8e 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -147,37 +147,11 @@ namespace MediaBrowser.Providers.Subtitles var parts = subtitleId.Split(new[] { '_' }, 2); var provider = GetProvider(parts.First()); - var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; - try { var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false); - using (var stream = response.Stream) - using (var memoryStream = new MemoryStream()) - { - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; - - var savePaths = new List(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); - - if (response.IsForced) - { - saveFileName += ".forced"; - } - - saveFileName += "." + response.Format.ToLowerInvariant(); - - if (saveInMediaFolder) - { - savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName)); - } - - savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); - - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); - } + await TrySaveSubtitle(video, libraryOptions, response); } catch (RateLimitExceededException) { @@ -196,6 +170,48 @@ namespace MediaBrowser.Providers.Subtitles } } + /// + public async Task UploadSubtitle(Video video, SubtitleResponse response) + { + var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); + + await TrySaveSubtitle(video, libraryOptions, response); + } + + private async Task TrySaveSubtitle( + Video video, + LibraryOptions libraryOptions, + SubtitleResponse response) + { + var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; + + using (var stream = response.Stream) + using (var memoryStream = new MemoryStream()) + { + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + + var savePaths = new List(); + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + + if (response.IsForced) + { + saveFileName += ".forced"; + } + + saveFileName += "." + response.Format.ToLowerInvariant(); + + if (saveInMediaFolder) + { + savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName)); + } + + savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + + await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); + } + } + private async Task TrySaveToFiles(Stream stream, List savePaths) { Exception exceptionToThrow = null; From c888b34a6219be0c06331e568d66a8fdf17bceaa Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 24 Jul 2020 21:19:56 +0800 Subject: [PATCH 002/258] add a method to use custom fallback fonts for subtitle rendering --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 53 +++++++++++++++++++ .../Configuration/EncodingOptions.cs | 5 ++ 2 files changed, 58 insertions(+) diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index a70da8e56c..198e459482 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -122,8 +123,15 @@ namespace MediaBrowser.Api.Subtitles public int SegmentLength { get; set; } } + [Route("/FallbackFont/Font", "GET", Summary = "Gets the fallback font file")] + [Authenticated] + public class GetFallbackFont + { + } + public class SubtitleService : BaseApiService { + private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; private readonly ISubtitleEncoder _subtitleEncoder; @@ -145,6 +153,7 @@ namespace MediaBrowser.Api.Subtitles IAuthorizationContext authContext) : base(logger, serverConfigurationManager, httpResultFactory) { + _serverConfigurationManager = serverConfigurationManager; _libraryManager = libraryManager; _subtitleManager = subtitleManager; _subtitleEncoder = subtitleEncoder; @@ -298,5 +307,49 @@ namespace MediaBrowser.Api.Subtitles } }); } + + public async Task Get(GetFallbackFont request) + { + var fallbackFontPath = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager).FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { + var directoryService = new DirectoryService(_fileSystem); + + try + { + // 10 Megabytes + var maxSize = 10485760; + var fontFile = directoryService.GetFile(fallbackFontPath); + var fileSize = fontFile?.Length; + + if (fileSize != null && fileSize > 0) + { + Logger.LogInformation("Fallback font size is {0} Bytes", fileSize); + + if (fileSize <= maxSize) + { + return await ResultFactory.GetStaticFileResult(Request, fontFile.FullName); + } + + Logger.LogInformation("The selected font is too large. Maximum allowed size is 10 Megabytes"); + } + else + { + Logger.LogInformation("The selected font is null or empty"); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error reading fallback font file"); + } + } + else + { + Logger.LogInformation("The path of fallback font has not been set"); + } + + return string.Empty; + } } } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f0..aa6ed5bb9f 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Configuration public string TranscodingTempPath { get; set; } + public string FallbackFontPath { get; set; } + + public bool EnableFallbackFont { get; set; } + public double DownMixAudioBoost { get; set; } public bool EnableThrottling { get; set; } @@ -49,6 +53,7 @@ namespace MediaBrowser.Model.Configuration public EncodingOptions() { + EnableFallbackFont = false; DownMixAudioBoost = 2; EnableThrottling = false; ThrottleDelaySeconds = 180; From a78c8e38544a974f07c6efce1af4e7fbef83b2be Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 25 Jul 2020 01:50:18 +0800 Subject: [PATCH 003/258] change loglevels --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 198e459482..b06ffd1851 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -325,18 +325,18 @@ namespace MediaBrowser.Api.Subtitles if (fileSize != null && fileSize > 0) { - Logger.LogInformation("Fallback font size is {0} Bytes", fileSize); + Logger.LogDebug("Fallback font size is {0} Bytes", fileSize); if (fileSize <= maxSize) { return await ResultFactory.GetStaticFileResult(Request, fontFile.FullName); } - Logger.LogInformation("The selected font is too large. Maximum allowed size is 10 Megabytes"); + Logger.LogWarning("The selected font is too large. Maximum allowed size is 10 Megabytes"); } else { - Logger.LogInformation("The selected font is null or empty"); + Logger.LogWarning("The selected font is null or empty"); } } catch (Exception ex) @@ -346,7 +346,7 @@ namespace MediaBrowser.Api.Subtitles } else { - Logger.LogInformation("The path of fallback font has not been set"); + Logger.LogWarning("The path of fallback font has not been set"); } return string.Empty; From 1883ecd81716acb108424924711aed842dd6338a Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 24 Jul 2020 22:43:32 +0200 Subject: [PATCH 004/258] Fix left /usr/bin/jellyfin symlink on removal and typo After removal of the symlink target file "/usr/lib/jellyfin/bin/jellyfin", file existence check on the symlink "[[ -f /usr/bin/jellyfin ]]" returns false. As a result the symlink is left in place on package purge. The correct check would be "[[ -L /usr/bin/jellyfin ]]", but since it could be a file in cases, e.g. manual fix on file systems with no symlink support or for any other reason, it is easiest to use "rm -f" to assure that it is removed in both cases and not return false even if it does not exist at all. Additionally this fixes a typo on upstart script check. Signed-off-by: MichaIng --- debian/postrm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/postrm b/debian/postrm index 1d00a984ec..3d56a5f1e8 100644 --- a/debian/postrm +++ b/debian/postrm @@ -25,7 +25,7 @@ case "$1" in purge) echo PURGE | debconf-communicate $NAME > /dev/null 2>&1 || true - if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.connf" ]]; then + if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then update-rc.d jellyfin remove >/dev/null 2>&1 || true fi @@ -54,7 +54,7 @@ case "$1" in rm -rf $PROGRAMDATA fi # Remove binary symlink - [[ -f /usr/bin/jellyfin ]] && rm /usr/bin/jellyfin + rm -f /usr/bin/jellyfin # Remove sudoers config [[ -f /etc/sudoers.d/jellyfin-sudoers ]] && rm /etc/sudoers.d/jellyfin-sudoers # Remove anything at the default locations; catches situations where the user moved the defaults From 45a97056ea9fe7bc247f205931f5b0e0f2a1ab75 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Wed, 29 Jul 2020 02:44:11 +0800 Subject: [PATCH 005/258] allows to provide multiple fonts --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 81 +++++++++++++++++-- MediaBrowser.Model/Subtitles/FontFile.cs | 34 ++++++++ 2 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 MediaBrowser.Model/Subtitles/FontFile.cs diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index b06ffd1851..b5f5213fe3 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Services; +using MediaBrowser.Model.Subtitles; using Microsoft.Extensions.Logging; using MimeTypes = MediaBrowser.Model.Net.MimeTypes; @@ -123,10 +124,18 @@ namespace MediaBrowser.Api.Subtitles public int SegmentLength { get; set; } } + [Route("/FallbackFont/FontList", "GET", Summary = "Gets the fallback font list")] + [Authenticated] + public class GetFallbackFontList + { + } + [Route("/FallbackFont/Font", "GET", Summary = "Gets the fallback font file")] [Authenticated] public class GetFallbackFont { + [ApiMember(Name = "Name", Description = "The font file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Name { get; set; } } public class SubtitleService : BaseApiService @@ -308,19 +317,74 @@ namespace MediaBrowser.Api.Subtitles }); } - public async Task Get(GetFallbackFont request) + public object Get(GetFallbackFontList request) { - var fallbackFontPath = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager).FallbackFontPath; + IEnumerable fontFiles = Enumerable.Empty(); + + var encodingOptions = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager); + var fallbackFontPath = encodingOptions.FallbackFontPath; if (!string.IsNullOrEmpty(fallbackFontPath)) { - var directoryService = new DirectoryService(_fileSystem); + try + { + fontFiles = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); + + var result = fontFiles.Select(i => new FontFile + { + Name = i.Name, + Size = i.Length, + DateCreated = _fileSystem.GetCreationTimeUtc(i), + DateModified = _fileSystem.GetLastWriteTimeUtc(i) + }).OrderBy(i => i.Size) + .ThenBy(i => i.Name) + .ThenByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated) + .ToArray(); + + // max total size 20M + var maxSize = 20971520; + var sizeCounter = 0L; + for (int i = 0; i < result.Length; i++) + { + sizeCounter += result[i].Size; + if (sizeCounter >= maxSize) + { + Logger.LogWarning("Some fonts will not be sent due to size limitations"); + Array.Resize(ref result, i); + break; + } + } + + return ToOptimizedResult(result); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error getting fallback font list"); + } + } + else + { + Logger.LogWarning("The path of fallback font folder has not been set"); + encodingOptions.EnableFallbackFont = false; + } + return ResultFactory.GetResult(Request, "[]", "application/json"); + } + + public async Task Get(GetFallbackFont request) + { + var encodingOptions = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager); + var fallbackFontPath = encodingOptions.FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { try { - // 10 Megabytes + // max single font size 10M var maxSize = 10485760; - var fontFile = directoryService.GetFile(fallbackFontPath); + var fontFile = _fileSystem.GetFiles(fallbackFontPath) + .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase)); var fileSize = fontFile?.Length; if (fileSize != null && fileSize > 0) @@ -341,15 +405,16 @@ namespace MediaBrowser.Api.Subtitles } catch (Exception ex) { - Logger.LogError(ex, "Error reading fallback font file"); + Logger.LogError(ex, "Error reading fallback font"); } } else { - Logger.LogWarning("The path of fallback font has not been set"); + Logger.LogWarning("The path of fallback font folder has not been set"); + encodingOptions.EnableFallbackFont = false; } - return string.Empty; + return ResultFactory.GetResult(Request, string.Empty, "text/plain"); } } } diff --git a/MediaBrowser.Model/Subtitles/FontFile.cs b/MediaBrowser.Model/Subtitles/FontFile.cs new file mode 100644 index 0000000000..13963a9cb5 --- /dev/null +++ b/MediaBrowser.Model/Subtitles/FontFile.cs @@ -0,0 +1,34 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Model.Subtitles +{ + public class FontFile + { + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + + /// + /// Gets or sets the size. + /// + /// The size. + public long Size { get; set; } + + /// + /// Gets or sets the date created. + /// + /// The date created. + public DateTime DateCreated { get; set; } + + /// + /// Gets or sets the date modified. + /// + /// The date modified. + public DateTime DateModified { get; set; } + } +} From 893bc7400bc2dadf2a789ba439d9a469c13b05df Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 30 Jul 2020 16:30:49 +0800 Subject: [PATCH 006/258] update as per suggestions --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index b5f5213fe3..e275d3a285 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -124,13 +125,13 @@ namespace MediaBrowser.Api.Subtitles public int SegmentLength { get; set; } } - [Route("/FallbackFont/FontList", "GET", Summary = "Gets the fallback font list")] + [Route("/FallbackFont/Fonts", "GET", Summary = "Gets the fallback font list")] [Authenticated] public class GetFallbackFontList { } - [Route("/FallbackFont/Font", "GET", Summary = "Gets the fallback font file")] + [Route("/FallbackFont/Fonts/{Name}", "GET", Summary = "Gets the fallback font file")] [Authenticated] public class GetFallbackFont { @@ -369,7 +370,7 @@ namespace MediaBrowser.Api.Subtitles encodingOptions.EnableFallbackFont = false; } - return ResultFactory.GetResult(Request, "[]", "application/json"); + return ResultFactory.GetResult(Request, "[]", MediaTypeNames.Application.Json); } public async Task Get(GetFallbackFont request) @@ -414,7 +415,7 @@ namespace MediaBrowser.Api.Subtitles encodingOptions.EnableFallbackFont = false; } - return ResultFactory.GetResult(Request, string.Empty, "text/plain"); + return ResultFactory.GetResult(Request, string.Empty, MediaTypeNames.Text.Plain); } } } From 8165c3bb21a1ac4869dafa951f8a3ca3d21dda1e Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 30 Jul 2020 17:36:38 +0800 Subject: [PATCH 007/258] total font size limit 20M --- MediaBrowser.Api/Subtitles/SubtitleService.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index e275d3a285..bd4da04f90 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -382,8 +382,6 @@ namespace MediaBrowser.Api.Subtitles { try { - // max single font size 10M - var maxSize = 10485760; var fontFile = _fileSystem.GetFiles(fallbackFontPath) .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase)); var fileSize = fontFile?.Length; @@ -391,13 +389,7 @@ namespace MediaBrowser.Api.Subtitles if (fileSize != null && fileSize > 0) { Logger.LogDebug("Fallback font size is {0} Bytes", fileSize); - - if (fileSize <= maxSize) - { - return await ResultFactory.GetStaticFileResult(Request, fontFile.FullName); - } - - Logger.LogWarning("The selected font is too large. Maximum allowed size is 10 Megabytes"); + return await ResultFactory.GetStaticFileResult(Request, fontFile.FullName); } else { From 988e76d4c7df48370a6cb1de59c4909d140f96e2 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Wed, 19 Aug 2020 17:30:22 +0800 Subject: [PATCH 008/258] update endpoints as AspNet controllers --- .../Controllers/SubtitleController.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 988acccc3a..387c9b0953 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -10,6 +10,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -20,6 +22,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Subtitles; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -33,6 +36,7 @@ namespace Jellyfin.Api.Controllers [Route("")] public class SubtitleController : BaseJellyfinApiController { + private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; private readonly ISubtitleEncoder _subtitleEncoder; @@ -45,6 +49,7 @@ namespace Jellyfin.Api.Controllers /// /// Initializes a new instance of the class. /// + /// Instance of interface. /// Instance of interface. /// Instance of interface. /// Instance of interface. @@ -54,6 +59,7 @@ namespace Jellyfin.Api.Controllers /// Instance of interface. /// Instance of interface. public SubtitleController( + IServerConfigurationManager serverConfigurationManager, ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder, @@ -63,6 +69,7 @@ namespace Jellyfin.Api.Controllers IAuthorizationContext authContext, ILogger logger) { + _serverConfigurationManager = serverConfigurationManager; _libraryManager = libraryManager; _subtitleManager = subtitleManager; _subtitleEncoder = subtitleEncoder; @@ -346,5 +353,116 @@ namespace Jellyfin.Api.Controllers copyTimestamps, CancellationToken.None); } + + /// + /// Gets a list of available fallback font files. + /// + /// Information retrieved. + /// An array of with the available font files. + [HttpGet("/FallbackFont/Fonts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetFallbackFontList() + { + IEnumerable fontFiles = Enumerable.Empty(); + + var encodingOptions = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager); + var fallbackFontPath = encodingOptions.FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { + try + { + fontFiles = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); + + var result = fontFiles.Select(i => new FontFile + { + Name = i.Name, + Size = i.Length, + DateCreated = _fileSystem.GetCreationTimeUtc(i), + DateModified = _fileSystem.GetLastWriteTimeUtc(i) + }).OrderBy(i => i.Size) + .ThenBy(i => i.Name) + .ThenByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated) + .ToArray(); + + // max total size 20M + var maxSize = 20971520; + var sizeCounter = 0L; + for (int i = 0; i < result.Length; i++) + { + sizeCounter += result[i].Size; + if (sizeCounter >= maxSize) + { + _logger.LogWarning("Some fonts will not be sent due to size limitations"); + Array.Resize(ref result, i); + break; + } + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting fallback font list"); + } + } + else + { + _logger.LogWarning("The path of fallback font folder has not been set"); + encodingOptions.EnableFallbackFont = false; + } + + return File(Encoding.UTF8.GetBytes("[]"), MediaTypeNames.Application.Json); + } + + /// + /// Gets a fallback font file. + /// + /// The name of the fallback font file to get. + /// Fallback font file retrieved. + /// The fallback font file. + [HttpGet("/FallbackFont/Fonts/{name}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetFallbackFont([FromRoute, Required] string? name) + { + var encodingOptions = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager); + var fallbackFontPath = encodingOptions.FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { + try + { + var fontFile = _fileSystem.GetFiles(fallbackFontPath) + .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); + var fileSize = fontFile?.Length; + + if (fontFile != null && fileSize != null && fileSize > 0) + { + _logger.LogDebug("Fallback font size is {0} Bytes", fileSize); + + FileStream stream = new FileStream(fontFile.FullName, FileMode.Open, FileAccess.Read); + return File(stream, MimeTypes.GetMimeType(fontFile.FullName)); + } + else + { + _logger.LogWarning("The selected font is null or empty"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading fallback font"); + } + } + else + { + _logger.LogWarning("The path of fallback font folder has not been set"); + encodingOptions.EnableFallbackFont = false; + } + + return File(Encoding.UTF8.GetBytes(string.Empty), MediaTypeNames.Text.Plain); + } } } From 53861db45139022907c24a43bea43225f34c6a6d Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 20 Aug 2020 00:04:55 +0800 Subject: [PATCH 009/258] Apply suggestions from code review Co-authored-by: Cody Robibero --- .../Controllers/SubtitleController.cs | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 387c9b0953..0795f8f649 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -359,53 +359,52 @@ namespace Jellyfin.Api.Controllers /// /// Information retrieved. /// An array of with the available font files. - [HttpGet("/FallbackFont/Fonts")] + [HttpGet("FallbackFont/Fonts")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetFallbackFontList() + /// + /// Gets a list of available fallback font files. + /// + /// Information retrieved. + /// An array of with the available font files. + [HttpGet("FallbackFont/Fonts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetFallbackFontList() { - IEnumerable fontFiles = Enumerable.Empty(); - - var encodingOptions = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager); + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var fallbackFontPath = encodingOptions.FallbackFontPath; if (!string.IsNullOrEmpty(fallbackFontPath)) { - try - { - fontFiles = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); + var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); - var result = fontFiles.Select(i => new FontFile + var fontFiles = files + .Select(i => new FontFile { Name = i.Name, Size = i.Length, DateCreated = _fileSystem.GetCreationTimeUtc(i), DateModified = _fileSystem.GetLastWriteTimeUtc(i) - }).OrderBy(i => i.Size) - .ThenBy(i => i.Name) - .ThenByDescending(i => i.DateModified) - .ThenByDescending(i => i.DateCreated) - .ToArray(); - - // max total size 20M - var maxSize = 20971520; - var sizeCounter = 0L; - for (int i = 0; i < result.Length; i++) + }) + .OrderBy(i => i.Size) + .ThenBy(i => i.Name) + .ThenByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated); + + // max total size 20M + const int MaxSize = 20971520; + var sizeCounter = 0L; + foreach (var fontFile in fontFiles) + { + sizeCounter += fontFile.Size; + if (sizeCounter >= MaxSize) { - sizeCounter += result[i].Size; - if (sizeCounter >= maxSize) - { - _logger.LogWarning("Some fonts will not be sent due to size limitations"); - Array.Resize(ref result, i); - break; - } + _logger.LogWarning("Some fonts will not be sent due to size limitations"); + yield break; } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting fallback font list"); + yield return fontFile; } } else @@ -413,8 +412,6 @@ namespace Jellyfin.Api.Controllers _logger.LogWarning("The path of fallback font folder has not been set"); encodingOptions.EnableFallbackFont = false; } - - return File(Encoding.UTF8.GetBytes("[]"), MediaTypeNames.Application.Json); } /// @@ -423,12 +420,12 @@ namespace Jellyfin.Api.Controllers /// The name of the fallback font file to get. /// Fallback font file retrieved. /// The fallback font file. - [HttpGet("/FallbackFont/Fonts/{name}")] + [HttpGet("FallbackFont/Fonts/{name}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetFallbackFont([FromRoute, Required] string? name) + public ActionResult GetFallbackFont([FromRoute, Required] string name) { - var encodingOptions = EncodingConfigurationExtensions.GetEncodingOptions(_serverConfigurationManager); + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var fallbackFontPath = encodingOptions.FallbackFontPath; if (!string.IsNullOrEmpty(fallbackFontPath)) @@ -441,7 +438,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { - _logger.LogDebug("Fallback font size is {0} Bytes", fileSize); + _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); FileStream stream = new FileStream(fontFile.FullName, FileMode.Open, FileAccess.Read); return File(stream, MimeTypes.GetMimeType(fontFile.FullName)); From 16658c92fe1baeb23909001525008208cfc82f75 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 20 Aug 2020 00:08:44 +0800 Subject: [PATCH 010/258] fix SubtitlesOctopus --- .../Controllers/SubtitleController.cs | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 0795f8f649..6c9d1beecd 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -354,14 +354,6 @@ namespace Jellyfin.Api.Controllers CancellationToken.None); } - /// - /// Gets a list of available fallback font files. - /// - /// Information retrieved. - /// An array of with the available font files. - [HttpGet("FallbackFont/Fonts")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] /// /// Gets a list of available fallback font files. /// @@ -378,7 +370,6 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(fallbackFontPath)) { var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); - var fontFiles = files .Select(i => new FontFile { @@ -391,7 +382,6 @@ namespace Jellyfin.Api.Controllers .ThenBy(i => i.Name) .ThenByDescending(i => i.DateModified) .ThenByDescending(i => i.DateCreated); - // max total size 20M const int MaxSize = 20971520; var sizeCounter = 0L; @@ -403,7 +393,6 @@ namespace Jellyfin.Api.Controllers _logger.LogWarning("Some fonts will not be sent due to size limitations"); yield break; } - yield return fontFile; } } @@ -430,27 +419,20 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(fallbackFontPath)) { - try - { - var fontFile = _fileSystem.GetFiles(fallbackFontPath) - .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - var fileSize = fontFile?.Length; + var fontFile = _fileSystem.GetFiles(fallbackFontPath) + .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); + var fileSize = fontFile?.Length; - if (fontFile != null && fileSize != null && fileSize > 0) - { - _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); + if (fontFile != null && fileSize != null && fileSize > 0) + { + _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); - FileStream stream = new FileStream(fontFile.FullName, FileMode.Open, FileAccess.Read); - return File(stream, MimeTypes.GetMimeType(fontFile.FullName)); - } - else - { - _logger.LogWarning("The selected font is null or empty"); - } + FileStream stream = new FileStream(fontFile.FullName, FileMode.Open, FileAccess.Read); + return File(stream, MimeTypes.GetMimeType(fontFile.FullName)); } - catch (Exception ex) + else { - _logger.LogError(ex, "Error reading fallback font"); + _logger.LogWarning("The selected font is null or empty"); } } else @@ -459,7 +441,8 @@ namespace Jellyfin.Api.Controllers encodingOptions.EnableFallbackFont = false; } - return File(Encoding.UTF8.GetBytes(string.Empty), MediaTypeNames.Text.Plain); + // returning HTTP 204 will break the SubtitlesOctopus + return Ok(); } } } From dd3b48f2039fe299a55db437190a9e0d5c78145c Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Thu, 20 Aug 2020 15:17:42 +0800 Subject: [PATCH 011/258] fix lint --- Jellyfin.Api/Controllers/SubtitleController.cs | 3 ++- MediaBrowser.Model/Subtitles/FontFile.cs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 6c9d1beecd..912de0fd60 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -393,6 +393,7 @@ namespace Jellyfin.Api.Controllers _logger.LogWarning("Some fonts will not be sent due to size limitations"); yield break; } + yield return fontFile; } } @@ -425,7 +426,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { - _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); + _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); FileStream stream = new FileStream(fontFile.FullName, FileMode.Open, FileAccess.Read); return File(stream, MimeTypes.GetMimeType(fontFile.FullName)); diff --git a/MediaBrowser.Model/Subtitles/FontFile.cs b/MediaBrowser.Model/Subtitles/FontFile.cs index 13963a9cb5..115c492957 100644 --- a/MediaBrowser.Model/Subtitles/FontFile.cs +++ b/MediaBrowser.Model/Subtitles/FontFile.cs @@ -1,17 +1,17 @@ -#nullable disable -#pragma warning disable CS1591 - using System; namespace MediaBrowser.Model.Subtitles { + /// + /// Class FontFile. + /// public class FontFile { /// /// Gets or sets the name. /// /// The name. - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the size. From 05e78ee78c56364971956507f6239ded61f0af87 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 20 Aug 2020 22:53:36 +0800 Subject: [PATCH 012/258] Apply suggestions from code review Co-authored-by: Cody Robibero --- Jellyfin.Api/Controllers/SubtitleController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 912de0fd60..8550a86e0c 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -427,9 +427,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); - - FileStream stream = new FileStream(fontFile.FullName, FileMode.Open, FileAccess.Read); - return File(stream, MimeTypes.GetMimeType(fontFile.FullName)); + return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } else { From a3020f2917176baef19653a485cc0aaaeb8d00f2 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 21 Aug 2020 19:53:55 +0200 Subject: [PATCH 013/258] 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 | 58 ++++++++++--------- .../Drawing/IImageEncoder.cs | 3 +- .../Drawing/IImageProcessor.cs | 3 +- 7 files changed, 54 insertions(+), 35 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,47 +82,53 @@ 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; - } + return bitmap; + } - // 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)); + // 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); - // 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); - } + // 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 014/258] 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 015/258] 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 016/258] 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 8a15ad160b30769305b5cf03e16b3cec742156de Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 5 Sep 2020 18:58:16 -0400 Subject: [PATCH 017/258] Fix plugin events not being called and clean up InstallationManager.cs --- .../Updates/InstallationManager.cs | 66 ++++++------------- .../Updates/IInstallationManager.cs | 23 ------- 2 files changed, 19 insertions(+), 70 deletions(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index f121a34938..ac3caf83e6 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -10,12 +10,15 @@ using System.Runtime.Serialization; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; @@ -34,6 +37,7 @@ namespace Emby.Server.Implementations.Updates /// private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; + private readonly IEventManager _eventManager; private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _jsonSerializer; private readonly IServerConfigurationManager _config; @@ -63,23 +67,20 @@ namespace Emby.Server.Implementations.Updates ILogger logger, IApplicationHost appHost, IApplicationPaths appPaths, + IEventManager eventManager, IHttpClientFactory httpClientFactory, IJsonSerializer jsonSerializer, IServerConfigurationManager config, IFileSystem fileSystem, IZipClient zipClient) { - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _completedInstallationsInternal = new ConcurrentBag(); _logger = logger; _applicationHost = appHost; _appPaths = appPaths; + _eventManager = eventManager; _httpClientFactory = httpClientFactory; _jsonSerializer = jsonSerializer; _config = config; @@ -87,27 +88,6 @@ namespace Emby.Server.Implementations.Updates _zipClient = zipClient; } - /// - public event EventHandler PackageInstalling; - - /// - public event EventHandler PackageInstallationCompleted; - - /// - public event EventHandler PackageInstallationFailed; - - /// - public event EventHandler PackageInstallationCancelled; - - /// - public event EventHandler PluginUninstalled; - - /// - public event EventHandler PluginUpdated; - - /// - public event EventHandler PluginInstalled; - /// public IEnumerable CompletedInstallations => _completedInstallationsInternal; @@ -256,11 +236,11 @@ namespace Emby.Server.Implementations.Updates var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token; - PackageInstalling?.Invoke(this, package); + await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false); try { - await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); + var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); lock (_currentInstallationsLock) { @@ -268,8 +248,11 @@ namespace Emby.Server.Implementations.Updates } _completedInstallationsInternal.Add(package); + await _eventManager.PublishAsync(isUpdate + ? (GenericEventArgs)new PluginUpdatedEventArgs(package) + : new PluginInstalledEventArgs(package)).ConfigureAwait(false); - PackageInstallationCompleted?.Invoke(this, package); + _applicationHost.NotifyPendingRestart(); } catch (OperationCanceledException) { @@ -280,7 +263,7 @@ namespace Emby.Server.Implementations.Updates _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version); - PackageInstallationCancelled?.Invoke(this, package); + await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false); throw; } @@ -293,11 +276,11 @@ namespace Emby.Server.Implementations.Updates _currentInstallations.Remove(tuple); } - PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs + await _eventManager.PublishAsync(new InstallationFailedEventArgs { InstallationInfo = package, Exception = ex - }); + }).ConfigureAwait(false); throw; } @@ -314,7 +297,7 @@ namespace Emby.Server.Implementations.Updates /// The package. /// The cancellation token. /// . - private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) + private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) { // Set last update time if we were installed before IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid) @@ -324,20 +307,9 @@ namespace Emby.Server.Implementations.Updates await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); // Do plugin-specific processing - if (plugin == null) - { - _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version); - - PluginInstalled?.Invoke(this, package); - } - else - { - _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version); + _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version); - PluginUpdated?.Invoke(this, package); - } - - _applicationHost.NotifyPendingRestart(); + return plugin != null; } private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken) @@ -438,7 +410,7 @@ namespace Emby.Server.Implementations.Updates _config.SaveConfiguration(); } - PluginUninstalled?.Invoke(this, plugin); + _eventManager.Publish(new PluginUninstalledEventArgs(plugin)); _applicationHost.NotifyPendingRestart(); } diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index 4b4030bc27..1b38abb356 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -11,29 +11,6 @@ namespace MediaBrowser.Common.Updates { public interface IInstallationManager : IDisposable { - event EventHandler PackageInstalling; - - event EventHandler PackageInstallationCompleted; - - event EventHandler PackageInstallationFailed; - - event EventHandler PackageInstallationCancelled; - - /// - /// Occurs when a plugin is uninstalled. - /// - event EventHandler PluginUninstalled; - - /// - /// Occurs when a plugin is updated. - /// - event EventHandler PluginUpdated; - - /// - /// Occurs when a plugin is installed. - /// - event EventHandler PluginInstalled; - /// /// Gets the completed installations. /// From 320e3b98ab50e83932156b1a8ce7fc25685f7252 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 23:32:21 -0600 Subject: [PATCH 018/258] 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 019/258] 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 e3fb7f426c969c0316b53fe3348367c949f71c66 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 11 Sep 2020 16:08:49 -0600 Subject: [PATCH 020/258] Migrate subtitle upload to Jellyfin.Api --- .../Controllers/SubtitleController.cs | 28 ++ .../Models/SubtitleDtos/UploadSubtitleDto.cs | 34 ++ MediaBrowser.Api/Subtitles/SubtitleService.cs | 355 ------------------ 3 files changed, 62 insertions(+), 355 deletions(-) create mode 100644 Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs delete mode 100644 MediaBrowser.Api/Subtitles/SubtitleService.cs diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 78c9d43981..944da237bf 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.SubtitleDtos; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -317,6 +318,33 @@ namespace Jellyfin.Api.Controllers return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); } + /// + /// Upload an external subtitle file. + /// + /// The item the subtitle belongs to. + /// The request body. + /// Subtitle uploaded. + /// A . + [HttpPost("Videos/{itemId}/Subtitles")] + public async Task UploadSubtitle( + [FromRoute, Required] Guid itemId, + [FromBody, Required] UploadSubtitleDto body) + { + var video = (Video)_libraryManager.GetItemById(itemId); + var data = Convert.FromBase64String(body.Data); + await using var memoryStream = new MemoryStream(data); + await _subtitleManager.UploadSubtitle( + video, + new SubtitleResponse + { + Format = body.Format, + Language = body.Language, + IsForced = body.IsForced, + Stream = memoryStream + }).ConfigureAwait(false); + return NoContent(); + } + /// /// Encodes a subtitle in the specified format. /// diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs new file mode 100644 index 0000000000..30473255e8 --- /dev/null +++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.SubtitleDtos +{ + /// + /// Upload subtitles dto. + /// + public class UploadSubtitleDto + { + /// + /// Gets or sets the subtitle language. + /// + [Required] + public string Language { get; set; } = string.Empty; + + /// + /// Gets or sets the subtitle format. + /// + [Required] + public string Format { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the subtitle is forced. + /// + [Required] + public bool IsForced { get; set; } + + /// + /// Gets or sets the subtitle data. + /// + [Required] + public string Data { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs deleted file mode 100644 index ed2533f6d4..0000000000 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace MediaBrowser.Api.Subtitles -{ - [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")] - [Authenticated(Roles = "Admin")] - public class DeleteSubtitle - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } - - [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")] - public int Index { get; set; } - } - - [Route("/Videos/{Id}/Subtitles", "POST", Summary = "Upload an external subtitle file")] - [Authenticated(Roles = "admin")] - public class PostSubtitle : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Language { get; set; } - - [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Format { get; set; } - - [ApiMember(Name = "IsForced", Description = "IsForced", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string IsForced { get; set; } - - [ApiMember(Name = "Data", Description = "Data", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Data { get; set; } - - } - - [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")] - [Authenticated] - public class SearchRemoteSubtitles : IReturn - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - - [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Language { get; set; } - - public bool? IsPerfectMatch { get; set; } - } - - [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")] - [Authenticated] - public class DownloadRemoteSubtitles : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } - - [ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SubtitleId { get; set; } - } - - [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")] - [Authenticated] - public class GetRemoteSubtitles : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")] - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")] - public class GetSubtitle - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - - [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string MediaSourceId { get; set; } - - [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] - public int Index { get; set; } - - [ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Format { get; set; } - - [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public long StartPositionTicks { get; set; } - - [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public long? EndPositionTicks { get; set; } - - [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool CopyTimestamps { get; set; } - public bool AddVttTimeMap { get; set; } - } - - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")] - [Authenticated] - public class GetSubtitlePlaylist - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - - [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string MediaSourceId { get; set; } - - [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] - public int Index { get; set; } - - [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")] - public int SegmentLength { get; set; } - } - - public class SubtitleService : BaseApiService - { - private readonly ILibraryManager _libraryManager; - private readonly ISubtitleManager _subtitleManager; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - private readonly IAuthorizationContext _authContext; - - public SubtitleService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - ISubtitleManager subtitleManager, - ISubtitleEncoder subtitleEncoder, - IMediaSourceManager mediaSourceManager, - IProviderManager providerManager, - IFileSystem fileSystem, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _libraryManager = libraryManager; - _subtitleManager = subtitleManager; - _subtitleEncoder = subtitleEncoder; - _mediaSourceManager = mediaSourceManager; - _providerManager = providerManager; - _fileSystem = fileSystem; - _authContext = authContext; - } - - public async Task Get(GetSubtitlePlaylist request) - { - var item = (Video)_libraryManager.GetItemById(new Guid(request.Id)); - - var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); - - var builder = new StringBuilder(); - - var runtime = mediaSource.RunTimeTicks ?? -1; - - if (runtime <= 0) - { - throw new ArgumentException("HLS Subtitles are not supported for this media."); - } - - var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks; - if (segmentLengthTicks <= 0) - { - throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); - } - - builder.AppendLine("#EXTM3U"); - builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture)); - builder.AppendLine("#EXT-X-VERSION:3"); - builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); - builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); - - long positionTicks = 0; - - var accessToken = _authContext.GetAuthorizationInfo(Request).Token; - - while (positionTicks < runtime) - { - var remaining = runtime - positionTicks; - var lengthTicks = Math.Min(remaining, segmentLengthTicks); - - builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ","); - - var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); - - var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", - positionTicks.ToString(CultureInfo.InvariantCulture), - endPositionTicks.ToString(CultureInfo.InvariantCulture), - accessToken); - - builder.AppendLine(url); - - positionTicks += segmentLengthTicks; - } - - builder.AppendLine("#EXT-X-ENDLIST"); - - return ResultFactory.GetResult(Request, builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); - } - - public async Task Get(GetSubtitle request) - { - if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase)) - { - request.Format = "json"; - } - if (string.IsNullOrEmpty(request.Format)) - { - var item = (Video)_libraryManager.GetItemById(request.Id); - - var idString = request.Id.ToString("N", CultureInfo.InvariantCulture); - var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null) - .First(i => string.Equals(i.Id, request.MediaSourceId ?? idString)); - - var subtitleStream = mediaSource.MediaStreams - .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index); - - return await ResultFactory.GetStaticFileResult(Request, subtitleStream.Path).ConfigureAwait(false); - } - - if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap) - { - using var stream = await GetSubtitles(request).ConfigureAwait(false); - using var reader = new StreamReader(stream); - - var text = reader.ReadToEnd(); - - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); - - return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format)); - } - - return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format)); - } - - private Task GetSubtitles(GetSubtitle request) - { - var item = _libraryManager.GetItemById(request.Id); - - return _subtitleEncoder.GetSubtitles(item, - request.MediaSourceId, - request.Index, - request.Format, - request.StartPositionTicks, - request.EndPositionTicks ?? 0, - request.CopyTimestamps, - CancellationToken.None); - } - - public async Task Get(SearchRemoteSubtitles request) - { - var video = (Video)_libraryManager.GetItemById(request.Id); - - return await _subtitleManager.SearchSubtitles(video, request.Language, request.IsPerfectMatch, CancellationToken.None).ConfigureAwait(false); - } - - public Task Delete(DeleteSubtitle request) - { - var item = _libraryManager.GetItemById(request.Id); - return _subtitleManager.DeleteSubtitles(item, request.Index); - } - - public void Post(PostSubtitle request) - { - var video = (Video)_libraryManager.GetItemById(request.Id); - - var bytes = Convert.FromBase64String(request.Data); - var memoryStream = new MemoryStream(bytes) - { - Position = 0 - }; - - Task.Run(async () => - { - try - { - await _subtitleManager.UploadSubtitle(video, new SubtitleResponse { - Format = request.Format, - Language = request.Language, - IsForced = Convert.ToBoolean(request.IsForced), - Stream = memoryStream - }).ConfigureAwait(false); - - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error uploading subtitle"); - } - }); - } - - public async Task Get(GetRemoteSubtitles request) - { - var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false); - - return ResultFactory.GetResult(Request, result.Stream, MimeTypes.GetMimeType("file." + result.Format)); - } - - public void Post(DownloadRemoteSubtitles request) - { - var video = (Video)_libraryManager.GetItemById(request.Id); - - Task.Run(async () => - { - try - { - await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None) - .ConfigureAwait(false); - - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error downloading subtitles"); - } - }); - } - } -} From 97e948bbf6405c65d711aca6cd9d9febae95e8ff Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 11 Sep 2020 16:11:19 -0600 Subject: [PATCH 021/258] Fix added warnings --- MediaBrowser.Controller/Subtitles/ISubtitleManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 8537a7ef47..feb26bc101 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -56,6 +56,9 @@ namespace MediaBrowser.Controller.Subtitles /// /// Upload new subtitle. /// + /// The video the subtitle belongs to. + /// The subtitle response. + /// A representing the asynchronous operation. Task UploadSubtitle(Video video, SubtitleResponse response); /// From dfbe4413ff89aced08dae1222a82468e6d322b43 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 11 Sep 2020 16:13:15 -0600 Subject: [PATCH 022/258] Fix added warnings --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 30f390dcf5..f4a58775db 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -177,8 +177,7 @@ namespace MediaBrowser.Providers.Subtitles public async Task UploadSubtitle(Video video, SubtitleResponse response) { var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); - - await TrySaveSubtitle(video, libraryOptions, response); + await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false); } private async Task TrySaveSubtitle( From c41ed13b3dd875498b5784f11250ae7f421d94e2 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sun, 13 Sep 2020 14:36:10 +0100 Subject: [PATCH 023/258] 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 db07510017dc589da38698fd5e5f9afc55092334 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Thu, 17 Sep 2020 19:16:23 +0800 Subject: [PATCH 024/258] add tonemap for AMD AMF --- .../MediaEncoding/EncodingHelper.cs | 230 ++++++++++-------- 1 file changed, 128 insertions(+), 102 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2c30ca4588..790b26f699 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -452,11 +452,13 @@ namespace MediaBrowser.Controller.MediaEncoding var arg = new StringBuilder(); var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; + var isSwDecoder = string.IsNullOrEmpty(videoDecoder); + var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; - var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); @@ -517,11 +519,12 @@ namespace MediaBrowser.Controller.MediaEncoding } if (state.IsVideoRequest - && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder) + || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder)) { var isColorDepth10 = IsColorDepth10(state); - if (isNvencHevcDecoder && isColorDepth10 + if (isColorDepth10 && _mediaEncoder.SupportsHwaccel("opencl") && encodingOptions.EnableTonemapping && !string.IsNullOrEmpty(state.VideoStream.VideoRange) @@ -1023,19 +1026,19 @@ namespace MediaBrowser.Controller.MediaEncoding && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; } - if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)) { - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; var videoStream = state.VideoStream; var isColorDepth10 = IsColorDepth10(state); - if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1 - && isColorDepth10 + if (isColorDepth10 && _mediaEncoder.SupportsHwaccel("opencl") && encodingOptions.EnableTonemapping && !string.IsNullOrEmpty(videoStream.VideoRange) @@ -1652,47 +1655,7 @@ namespace MediaBrowser.Controller.MediaEncoding var outputSizeParam = ReadOnlySpan.Empty; var request = state.BaseRequest; - outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); - - // All possible beginning of video filters - // Don't break the order - string[] beginOfOutputSizeParam = new[] - { - // for tonemap_opencl - "hwupload,tonemap_opencl", - - // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux) - "hwupload=extra_hw_frames", - - // vpp_qsv - "vpp", - - // hwdownload,format=p010le (hardware decode + software encode for vaapi) - "hwdownload", - - // format=nv12|vaapi,hwupload,scale_vaapi - "format", - - // bwdif,scale=expr - "bwdif", - - // yadif,scale=expr - "yadif", - - // scale=expr - "scale" - }; - - var index = -1; - foreach (var param in beginOfOutputSizeParam) - { - index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = outputSizeParam.Slice(index); - break; - } - } + outputSizeParam = GetOutputSizeParamInternal(state, options, outputVideoCodec); var videoSizeParam = string.Empty; var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; @@ -2083,10 +2046,19 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam); } + public string GetOutputSizeParam( + EncodingJobInfo state, + EncodingOptions options, + string outputVideoCodec) + { + string filters = GetOutputSizeParamInternal(state, options, outputVideoCodec); + return string.IsNullOrEmpty(filters) ? string.Empty : " -vf \"" + filters + "\""; + } + /// /// If we're going to put a fixed size on the command line, this will calculate it. /// - public string GetOutputSizeParam( + public string GetOutputSizeParamInternal( EncodingJobInfo state, EncodingOptions options, string outputVideoCodec) @@ -2102,6 +2074,8 @@ namespace MediaBrowser.Controller.MediaEncoding var inputHeight = videoStream?.Height; var threeDFormat = state.MediaSource.Video3DFormat; + var isSwDecoder = string.IsNullOrEmpty(videoDecoder); + var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1; @@ -2117,47 +2091,78 @@ namespace MediaBrowser.Controller.MediaEncoding // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30; - // Currently only with the use of NVENC decoder can we get a decent performance. - // Currently only the HEVC/H265 format is supported. - // NVIDIA Pascal and Turing or higher are recommended. - if (isNvdecHevcDecoder && isColorDepth10 - && _mediaEncoder.SupportsHwaccel("opencl") - && options.EnableTonemapping - && !string.IsNullOrEmpty(videoStream.VideoRange) - && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) - { - var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; + var isScalingInAdvance = false; + var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); + var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); - if (options.TonemappingParam != 0) + if ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder) + || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder)) + { + // Currently only with the use of NVENC decoder can we get a decent performance. + // Currently only the HEVC/H265 format is supported with NVDEC decoder. + // NVIDIA Pascal and Turing or higher are recommended. + // AMD Polaris and Vega or higher are recommended. + if (isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && options.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) { - parameters += ":param={4}"; - } + var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; - if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) - { - parameters += ":range={5}"; - } + if (options.TonemappingParam != 0) + { + parameters += ":param={4}"; + } - // Upload the HDR10 or HLG data to the OpenCL device, - // use tonemap_opencl filter for tone mapping, - // and then download the SDR data to memory. - filters.Add("hwupload"); - filters.Add( - string.Format( - CultureInfo.InvariantCulture, - parameters, - options.TonemappingAlgorithm, - options.TonemappingDesat, - options.TonemappingThreshold, - options.TonemappingPeak, - options.TonemappingParam, - options.TonemappingRange)); - filters.Add("hwdownload"); - - if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true) - || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) - { - filters.Add("format=nv12"); + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + parameters += ":range={5}"; + } + + if (isSwDecoder || isD3d11vaDecoder) + { + isScalingInAdvance = true; + // Add scaling filter before tonemapping filter for performance. + filters.AddRange( + GetScalingFilters( + state, + inputWidth, + inputHeight, + threeDFormat, + videoDecoder, + outputVideoCodec, + request.Width, + request.Height, + request.MaxWidth, + request.MaxHeight)); + // Convert to hardware pixel format p010 when using SW decoder. + filters.Add("format=p010"); + } + + // Upload the HDR10 or HLG data to the OpenCL device, + // use tonemap_opencl filter for tone mapping, + // and then download the SDR data to memory. + filters.Add("hwupload"); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + parameters, + options.TonemappingAlgorithm, + options.TonemappingDesat, + options.TonemappingThreshold, + options.TonemappingPeak, + options.TonemappingParam, + options.TonemappingRange)); + filters.Add("hwdownload"); + + if (isLibX264Encoder + || hasGraphicalSubs + || (isNvdecHevcDecoder && isDeinterlaceHevc) + || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc)) + { + filters.Add("format=nv12"); + } } } @@ -2202,7 +2207,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Add hardware deinterlace filter before scaling filter - if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true)) + if (isDeinterlaceH264) { if (isVaapiH264Encoder) { @@ -2215,10 +2220,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Add software deinterlace filter before scaling filter - if ((state.DeInterlace("h264", true) - || state.DeInterlace("avc", true) - || state.DeInterlace("h265", true) - || state.DeInterlace("hevc", true)) + if ((isDeinterlaceH264 || isDeinterlaceHevc) && !isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder) @@ -2242,7 +2244,21 @@ namespace MediaBrowser.Controller.MediaEncoding } // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr - filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); + if (!isScalingInAdvance) + { + filters.AddRange( + GetScalingFilters( + state, + inputWidth, + inputHeight, + threeDFormat, + videoDecoder, + outputVideoCodec, + request.Width, + request.Height, + request.MaxWidth, + request.MaxHeight)); + } // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642) if (isVaapiH264Encoder) @@ -2275,7 +2291,7 @@ namespace MediaBrowser.Controller.MediaEncoding { output += string.Format( CultureInfo.InvariantCulture, - " -vf \"{0}\"", + "{0}", string.Join(",", filters)); } @@ -3069,21 +3085,31 @@ namespace MediaBrowser.Controller.MediaEncoding var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1); var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va"); - if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) + if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) { - if (isLinux) + // Currently there is no AMF decoder on Linux, only have h264 encoder. + if (isDxvaSupported && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) { - return "-hwaccel vaapi"; - } + if (isWindows && isWindows8orLater) + { + return "-hwaccel d3d11va"; + } - if (isWindows && isWindows8orLater) - { - return "-hwaccel d3d11va"; + if (isWindows && !isWindows8orLater) + { + return "-hwaccel dxva2"; + } } + } - if (isWindows && !isWindows8orLater) + if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + { + if (IsVaapiSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) { - return "-hwaccel dxva2"; + if (isLinux) + { + return "-hwaccel vaapi"; + } } } From 4db5700e18b98cb34784930707dc4ee2833865b6 Mon Sep 17 00:00:00 2001 From: Ryan Petris Date: Wed, 23 Sep 2020 14:12:26 -0700 Subject: [PATCH 025/258] 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 293237b714835fadd0a7c387815d146a2e4b810b Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 25 Sep 2020 18:40:10 +0100 Subject: [PATCH 026/258] 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 027/258] 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 028/258] 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 029/258] 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 030/258] 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 031/258] 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 6ca313abc186d45aef8b6d949432a4c9ef9cc1d2 Mon Sep 17 00:00:00 2001 From: cvium Date: Fri, 25 Sep 2020 23:59:17 +0200 Subject: [PATCH 032/258] Add ProgressiveFileStream --- .../Helpers/FileStreamResponseHelpers.cs | 5 +- Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 164 ++++++++++++++++++ 2 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 Jellyfin.Api/Helpers/ProgressiveFileStream.cs diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 6b516977e8..366301d3ee 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -123,9 +123,8 @@ namespace Jellyfin.Api.Helpers state.Dispose(); } - await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None) - .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false); - return new FileStreamResult(httpContext.Response.Body, contentType); + var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper); + return new FileStreamResult(stream, contentType); } finally { diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs new file mode 100644 index 0000000000..e09f3dca94 --- /dev/null +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.PlaybackDtos; +using MediaBrowser.Model.IO; + +namespace Jellyfin.Api.Helpers +{ + /// + /// A progressive file stream for transferring transcoded files as they are written to. + /// + public class ProgressiveFileStream : Stream + { + private readonly FileStream _fileStream; + private readonly TranscodingJobDto? _job; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly bool _allowAsyncFileRead; + private int _bytesWritten; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the transcoded file. + /// The transcoding job information. + /// The transcoding job helper. + public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper) + { + _job = job; + _transcodingJobHelper = transcodingJobHelper; + _bytesWritten = 0; + + var fileOptions = FileOptions.SequentialScan; + _allowAsyncFileRead = false; + + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileOptions |= FileOptions.Asynchronous; + _allowAsyncFileRead = true; + } + + _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions); + } + + /// + public override bool CanRead => _fileStream.CanRead; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override void Flush() + { + _fileStream.Flush(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + return _fileStream.Read(buffer, offset, count); + } + + /// + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var eofCount = 0; + const int EmptyReadLimit = 20; + + int totalBytesRead = 0; + int remainingBytesToRead = count; + + while (eofCount < EmptyReadLimit && remainingBytesToRead > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + int bytesRead; + if (_allowAsyncFileRead) + { + bytesRead = await _fileStream.ReadAsync(buffer, offset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); + } + else + { + bytesRead = _fileStream.Read(buffer, offset, remainingBytesToRead); + } + + remainingBytesToRead -= bytesRead; + if (bytesRead > 0) + { + _bytesWritten += bytesRead; + totalBytesRead += bytesRead; + + if (_job != null) + { + _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); + } + } + + if (bytesRead == 0) + { + if (_job == null || _job.HasExited) + { + eofCount++; + } + + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + else + { + eofCount = 0; + } + } + + return totalBytesRead; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + /// + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _fileStream.Dispose(); + + if (_job != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(_job); + } + } + + _disposed = true; + } + } +} From 146cad61505147ee1e118135a396eb6ed3e0fc78 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 26 Sep 2020 19:03:23 +0200 Subject: [PATCH 033/258] Remove EOF counter --- Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index e09f3dca94..b3566b6f80 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -79,13 +79,10 @@ namespace Jellyfin.Api.Helpers /// public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - var eofCount = 0; - const int EmptyReadLimit = 20; - int totalBytesRead = 0; int remainingBytesToRead = count; - while (eofCount < EmptyReadLimit && remainingBytesToRead > 0) + while (remainingBytesToRead > 0) { cancellationToken.ThrowIfCancellationRequested(); int bytesRead; @@ -109,20 +106,15 @@ namespace Jellyfin.Api.Helpers _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); } } - - if (bytesRead == 0) + else { if (_job == null || _job.HasExited) { - eofCount++; + break; } await Task.Delay(100, cancellationToken).ConfigureAwait(false); } - else - { - eofCount = 0; - } } return totalBytesRead; @@ -148,17 +140,23 @@ namespace Jellyfin.Api.Helpers return; } - if (disposing) + try { - _fileStream.Dispose(); - - if (_job != null) + if (disposing) { - _transcodingJobHelper.OnTranscodeEndRequest(_job); + _fileStream.Dispose(); + + if (_job != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(_job); + } } } - - _disposed = true; + finally + { + _disposed = true; + base.Dispose(disposing); + } } } } From 05fa95f149582b3de1881c42aaa0ac00408a9947 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sat, 26 Sep 2020 19:33:59 -0700 Subject: [PATCH 034/258] Increase scan speed for music libraries --- .../Resolvers/Audio/MusicAlbumResolver.cs | 66 ++++++++++--------- .../Resolvers/Audio/MusicArtistResolver.cs | 14 +++- 2 files changed, 47 insertions(+), 33 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 - { - var fullName = fileSystemInfo.FullName; - - if (libraryManager.IsAudioFile(fullName)) + else { - return true; + // 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) { 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 035/258] 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 036/258] 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 037/258] 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 038/258] 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 303eccaffeb42eb39b04f3ed372dc9fe84de5455 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 16:22:52 -0700 Subject: [PATCH 039/258] 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 040/258] 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 3ca9b13f99a863fee2a2b335555311cc1e9665aa Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 27 Sep 2020 21:47:30 -0700 Subject: [PATCH 041/258] 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 042/258] 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 e9911b70ddfd9a9ee1f9b89579f5dbec154129a4 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Mon, 28 Sep 2020 15:37:10 -0700 Subject: [PATCH 043/258] 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 044/258] 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 045/258] 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 53d8023defe19ef943f72964d93dbed40b6f1180 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 30 Sep 2020 17:37:30 -0600 Subject: [PATCH 046/258] Update all on-disk plugins --- .../ApplicationHost.cs | 94 ++++++--------- .../Updates/InstallationManager.cs | 10 +- MediaBrowser.Common/Plugins/LocalPlugin.cs | 113 ++++++++++++++++++ .../IServerApplicationHost.cs | 10 +- 4 files changed, 162 insertions(+), 65 deletions(-) create mode 100644 MediaBrowser.Common/Plugins/LocalPlugin.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7a46fdf2e7..984ab41f0a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Dto; -using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; @@ -258,8 +256,8 @@ namespace Emby.Server.Implementations IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); - _jsonSerializer = new JsonSerializer(); - + _jsonSerializer = new JsonSerializer(); + ServiceCollection = serviceCollection; _networkManager = networkManager; @@ -1026,80 +1024,54 @@ namespace Emby.Server.Implementations protected abstract void RestartInternal(); - /// - /// Comparison function used in . - /// - /// Item to compare. - /// Item to compare with. - /// Boolean result of the operation. - private static int VersionCompare( - (Version PluginVersion, string Name, string Path) a, - (Version PluginVersion, string Name, string Path) b) - { - int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture); - - if (compare == 0) - { - return a.PluginVersion.CompareTo(b.PluginVersion); - } - - return compare; - } - - /// - /// Returns a list of plugins to install. - /// - /// Path to check. - /// True if an attempt should be made to delete old plugs. - /// Enumerable list of dlls to load. - private IEnumerable GetPlugins(string path, bool cleanup = true) + /// + public IEnumerable GetLocalPlugins(string path, bool cleanup = true) { - var dllList = new List(); - var versions = new List<(Version PluginVersion, string Name, string Path)>(); + var minimumVersion = new Version(0, 0, 0, 1); + var versions = new List(); var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); - string metafile; foreach (var dir in directories) { try { - metafile = Path.Combine(dir, "meta.json"); + var metafile = Path.Combine(dir, "meta.json"); if (File.Exists(metafile)) { var manifest = _jsonSerializer.DeserializeFromFile(metafile); if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) { - targetAbi = new Version(0, 0, 0, 1); + targetAbi = minimumVersion; } if (!Version.TryParse(manifest.Version, out var version)) { - version = new Version(0, 0, 0, 1); + version = minimumVersion; } if (ApplicationVersion >= targetAbi) { // Only load Plugins if the plugin is built for this version or below. - versions.Add((version, manifest.Name, dir)); + versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir)); } } else { // No metafile, so lets see if the folder is versioned. metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; - + int versionIndex = dir.LastIndexOf('_'); - if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver)) + if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion)) { // Versioned folder. - versions.Add((ver, metafile, dir)); + versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir)); } else { - // Un-versioned folder - Add it under the path name and version 0.0.0.1. - versions.Add((new Version(0, 0, 0, 1), metafile, dir)); - } + // Un-versioned folder - Add it under the path name and version 0.0.0.1. + versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir)); + } } } catch @@ -1109,14 +1081,14 @@ namespace Emby.Server.Implementations } string lastName = string.Empty; - versions.Sort(VersionCompare); + versions.Sort(LocalPlugin.Compare); // Traverse backwards through the list. // The first item will be the latest version. for (int x = versions.Count - 1; x >= 0; x--) { if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase)) { - dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories)); + versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories)); lastName = versions[x].Name; continue; } @@ -1124,6 +1096,7 @@ namespace Emby.Server.Implementations if (!string.IsNullOrEmpty(lastName) && cleanup) { // Attempt a cleanup of old folders. + versions.RemoveAt(x); try { Logger.LogDebug("Deleting {Path}", versions[x].Path); @@ -1136,7 +1109,7 @@ namespace Emby.Server.Implementations } } - return dllList; + return versions; } /// @@ -1147,21 +1120,24 @@ namespace Emby.Server.Implementations { if (Directory.Exists(ApplicationPaths.PluginsPath)) { - foreach (var file in GetPlugins(ApplicationPaths.PluginsPath)) + foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath)) { - Assembly plugAss; - try + foreach (var file in plugin.DllFiles) { - plugAss = Assembly.LoadFrom(file); - } - catch (FileLoadException ex) - { - Logger.LogError(ex, "Failed to load assembly {Path}", file); - continue; - } + Assembly plugAss; + try + { + plugAss = Assembly.LoadFrom(file); + } + catch (FileLoadException ex) + { + Logger.LogError(ex, "Failed to load assembly {Path}", file); + continue; + } - Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); - yield return plugAss; + Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); + yield return plugAss; + } } } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 003cf3c740..365d890655 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -15,14 +15,13 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; -using MediaBrowser.Common.System; +using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.System; namespace Emby.Server.Implementations.Updates { @@ -45,7 +44,7 @@ namespace Emby.Server.Implementations.Updates /// Gets the application host. /// /// The application host. - private readonly IApplicationHost _applicationHost; + private readonly IServerApplicationHost _applicationHost; private readonly IZipClient _zipClient; @@ -63,7 +62,7 @@ namespace Emby.Server.Implementations.Updates public InstallationManager( ILogger logger, - IApplicationHost appHost, + IServerApplicationHost appHost, IApplicationPaths appPaths, IHttpClientFactory httpClientFactory, IJsonSerializer jsonSerializer, @@ -237,7 +236,8 @@ namespace Emby.Server.Implementations.Updates private IEnumerable GetAvailablePluginUpdates(IReadOnlyList pluginCatalog) { - foreach (var plugin in _applicationHost.Plugins) + var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath); + foreach (var plugin in plugins) { var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs new file mode 100644 index 0000000000..e26631615a --- /dev/null +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace MediaBrowser.Common.Plugins +{ + /// + /// Local plugin struct. + /// + public readonly struct LocalPlugin : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The plugin id. + /// The plugin name. + /// The plugin version. + /// The plugin path. + public LocalPlugin(Guid id, string name, Version version, string path) + { + Id = id; + Name = name; + Version = version; + Path = path; + DllFiles = new List(); + } + + /// + /// Gets the plugin id. + /// + public Guid Id { get; } + + /// + /// Gets the plugin name. + /// + public string Name { get; } + + /// + /// Gets the plugin version. + /// + public Version Version { get; } + + /// + /// Gets the plugin path. + /// + public string Path { get; } + + /// + /// Gets the list of dll files for this plugin. + /// + public List DllFiles { get; } + + /// + /// == operator. + /// + /// Left item. + /// Right item. + /// Comparison result. + public static bool operator ==(LocalPlugin left, LocalPlugin right) + { + return left.Equals(right); + } + + /// + /// != operator. + /// + /// Left item. + /// Right item. + /// Comparison result. + public static bool operator !=(LocalPlugin left, LocalPlugin right) + { + return !(left == right); + } + + /// + /// Compare two . + /// + /// The first item. + /// The second item. + /// Comparison result. + public static int Compare(LocalPlugin a, LocalPlugin b) + { + var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture); + + // Id is not equal but name is. + if (a.Id != b.Id && compare == 0) + { + compare = a.Id.CompareTo(b.Id); + } + + return compare == 0 ? a.Version.CompareTo(b.Version) : compare; + } + + /// + public override bool Equals(object obj) + { + return obj is LocalPlugin other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return Name.GetHashCode(StringComparison.OrdinalIgnoreCase); + } + + /// + public bool Equals(LocalPlugin other) + { + return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) + && Id.Equals(other.Id); + } + } +} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index cfad17fb72..8a55437c5d 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -6,8 +6,8 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; +using MediaBrowser.Common.Plugins; using MediaBrowser.Model.System; -using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller { @@ -119,5 +119,13 @@ namespace MediaBrowser.Controller string ExpandVirtualPath(string path); string ReverseVirtualPath(string path); + + /// + /// Gets the list of local plugins. + /// + /// Plugin base directory. + /// Cleanup old plugins. + /// Enumerable of local plugins. + IEnumerable GetLocalPlugins(string path, bool cleanup = true); } } From 3b0c6c660d057336e26aa67cb36abaaccb00771d Mon Sep 17 00:00:00 2001 From: Justin LeCheminant Date: Wed, 30 Sep 2020 20:26:59 -0600 Subject: [PATCH 047/258] 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 048/258] 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 049/258] 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 050/258] 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 051/258] 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 052/258] 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 053/258] 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 054/258] 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 055/258] 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 056/258] 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 057/258] 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 058/258] 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 059/258] 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 060/258] 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 061/258] 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 062/258] 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 063/258] 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 064/258] 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 065/258] 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 066/258] 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 067/258] 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 068/258] 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 069/258] 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 070/258] 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 071/258] 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 072/258] 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 073/258] 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 074/258] 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 075/258] 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 076/258] 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 077/258] 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 078/258] 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 04cdc89a5c9f92c70078520eb5c63fd2606f2a22 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Fri, 2 Oct 2020 17:14:57 -0700 Subject: [PATCH 079/258] Make MusicBrainzAlbumProvider thread safe --- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index abfa1c6e71..2d37422d04 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -46,6 +46,7 @@ namespace MediaBrowser.Providers.Music private readonly string _musicBrainzBaseUrl; + private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1); private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); public MusicBrainzAlbumProvider( @@ -742,45 +743,55 @@ namespace MediaBrowser.Providers.Music /// internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url); - - // MusicBrainz request a contact email address is supplied, as comment, in user agent field: - // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent - options.Headers.UserAgent.ParseAdd(string.Format( - CultureInfo.InvariantCulture, - "{0} ( {1} )", - _appHost.ApplicationUserAgent, - _appHost.ApplicationUserAgentAddress)); - HttpResponseMessage response; var attempts = 0u; + var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url; - do - { - attempts++; + await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false); - if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs) + try + { + do { - // MusicBrainz is extremely adamant about limiting to one request per second - var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; - await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); - } + attempts++; - // Write time since last request to debug log as evidence we're meeting rate limit - // requirement, before resetting stopwatch back to zero. - _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); - _stopWatchMusicBrainz.Restart(); + if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs) + { + // MusicBrainz is extremely adamant about limiting to one request per second + var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; + await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); + } - response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false); + // Write time since last request to debug log as evidence we're meeting rate limit + // requirement, before resetting stopwatch back to zero. + _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); + _stopWatchMusicBrainz.Restart(); - // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) + using var request = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url); + + // MusicBrainz request a contact email address is supplied, as comment, in user agent field: + // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent + request.Headers.UserAgent.ParseAdd(string.Format( + CultureInfo.InvariantCulture, + "{0} ( {1} )", + _appHost.ApplicationUserAgent, + _appHost.ApplicationUserAgentAddress)); + + response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(request).ConfigureAwait(false); + + // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) + } + while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); + } + finally + { + _apiRequestLock.Release(); } - while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); // Log error if unable to query MB database due to throttling if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) { - _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri); + _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); } return response; From db2e667936a3c982ad28fb383efd427f24e4e37d Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Fri, 2 Oct 2020 17:19:35 -0700 Subject: [PATCH 080/258] expand try finally --- .../Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 2d37422d04..5fe9ef7fa6 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -782,18 +782,18 @@ namespace MediaBrowser.Providers.Music // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) } while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); + + // Log error if unable to query MB database due to throttling + if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) + { + _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); + } } finally { _apiRequestLock.Release(); } - // Log error if unable to query MB database due to throttling - if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) - { - _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); - } - return response; } From 7841378506a04dd02d8e8459a274e740ed5da3f5 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Fri, 2 Oct 2020 17:27:43 -0700 Subject: [PATCH 081/258] cleaner --- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 5fe9ef7fa6..bf3088aa83 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -743,14 +743,14 @@ namespace MediaBrowser.Providers.Music /// internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - HttpResponseMessage response; - var attempts = 0u; - var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url; - await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { + HttpResponseMessage response; + var attempts = 0u; + var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url; + do { attempts++; @@ -767,7 +767,7 @@ namespace MediaBrowser.Providers.Music _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); _stopWatchMusicBrainz.Restart(); - using var request = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); // MusicBrainz request a contact email address is supplied, as comment, in user agent field: // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent @@ -788,13 +788,13 @@ namespace MediaBrowser.Providers.Music { _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); } + + return response; } finally { _apiRequestLock.Release(); } - - return response; } /// From 86cbefb059e55680bbf40769cdf13849e19b31bb Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Fri, 2 Oct 2020 20:32:52 -0400 Subject: [PATCH 082/258] 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 2124bc2e1894e5a09e5843cfcf19ab189e70eac1 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 3 Oct 2020 16:04:39 +0800 Subject: [PATCH 083/258] enhance workload when tone mapping with AMF zscale filter is required. --- .../MediaEncoding/EncodingHelper.cs | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 790b26f699..4a55840d52 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -859,29 +859,44 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) { - switch (encodingOptions.EncoderPreset) + var videoStream = state.VideoStream; + var isColorDepth10 = IsColorDepth10(state); + + if (isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) { - case "veryslow": - case "slow": - case "slower": - param += "-quality quality"; - break; + // Enhance quality and workload when tone mapping with AMF + param += "-quality quality -preanalysis true"; + } + else + { + switch (encodingOptions.EncoderPreset) + { + case "veryslow": + case "slow": + case "slower": + param += "-quality quality"; + break; - case "medium": - param += "-quality balanced"; - break; + case "medium": + param += "-quality balanced"; + break; - case "fast": - case "faster": - case "veryfast": - case "superfast": - case "ultrafast": - param += "-quality speed"; - break; + case "fast": + case "faster": + case "veryfast": + case "superfast": + case "ultrafast": + param += "-quality speed"; + break; - default: - param += "-quality speed"; - break; + default: + param += "-quality speed"; + break; + } } } else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm @@ -2123,19 +2138,18 @@ namespace MediaBrowser.Controller.MediaEncoding if (isSwDecoder || isD3d11vaDecoder) { isScalingInAdvance = true; - // Add scaling filter before tonemapping filter for performance. - filters.AddRange( - GetScalingFilters( - state, - inputWidth, - inputHeight, - threeDFormat, - videoDecoder, - outputVideoCodec, - request.Width, - request.Height, - request.MaxWidth, - request.MaxHeight)); + // Add zscale filter before tone mapping filter for performance. + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + if (width.HasValue && height.HasValue) + { + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "zscale=s={0}x{1}", + width.Value, + height.Value)); + } + // Convert to hardware pixel format p010 when using SW decoder. filters.Add("format=p010"); } From 9fbf725a6de88b8381f3d1607a8c722a59b92fbd Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 3 Oct 2020 17:53:10 +0800 Subject: [PATCH 084/258] Enhance workload when tone mapping on some APUs --- .../MediaEncoding/EncodingHelper.cs | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 4a55840d52..0125e909fe 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -859,6 +859,31 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) { + switch (encodingOptions.EncoderPreset) + { + case "veryslow": + case "slow": + case "slower": + param += "-quality quality"; + break; + + case "medium": + param += "-quality balanced"; + break; + + case "fast": + case "faster": + case "veryfast": + case "superfast": + case "ultrafast": + param += "-quality speed"; + break; + + default: + param += "-quality speed"; + break; + } + var videoStream = state.VideoStream; var isColorDepth10 = IsColorDepth10(state); @@ -868,35 +893,8 @@ namespace MediaBrowser.Controller.MediaEncoding && !string.IsNullOrEmpty(videoStream.VideoRange) && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) { - // Enhance quality and workload when tone mapping with AMF - param += "-quality quality -preanalysis true"; - } - else - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - case "slow": - case "slower": - param += "-quality quality"; - break; - - case "medium": - param += "-quality balanced"; - break; - - case "fast": - case "faster": - case "veryfast": - case "superfast": - case "ultrafast": - param += "-quality speed"; - break; - - default: - param += "-quality speed"; - break; - } + // Enhance workload when tone mapping with AMF on some APUs + param += " -preanalysis true"; } } else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm From ec0ff5d02fa53ca5b902d0bd5b477199170f3d28 Mon Sep 17 00:00:00 2001 From: "github@esslinger.dev" Date: Sat, 3 Oct 2020 12:40:28 +0200 Subject: [PATCH 085/258] 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 086/258] 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 087/258] 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 088/258] 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 | 254 ++------- .../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 | 458 ++++++---------- .../Plugins/Tmdb/Movies/TmdbSearch.cs | 302 ----------- .../Tmdb/Music/TmdbMusicVideoProvider.cs | 34 -- .../Tmdb/People/TmdbPersonImageProvider.cs | 100 +--- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 244 ++------- .../Tmdb/TV/TmdbEpisodeImageProvider.cs | 112 ++-- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 235 ++++---- .../Tmdb/TV/TmdbEpisodeProviderBase.cs | 156 ------ .../Tmdb/TV/TmdbSeasonImageProvider.cs | 106 +--- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 241 +++----- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 159 ++---- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 513 ++++++------------ .../Plugins/Tmdb/TmdbClientManager.cs | 469 ++++++++++++++++ .../Plugins/Tmdb/TmdbUtils.cs | 90 ++- .../Tmdb/Trailers/TmdbTrailerProvider.cs | 43 -- 73 files changed, 1570 insertions(+), 3822 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); + return Enumerable.Empty(); + } - if (mainResult != null) - { - var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var language = item.GetPreferredMetadataLanguage(); - var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - return GetImages(mainResult, language, tmdbImageUrl); - } + if (collection?.Images == null) + { + return Enumerable.Empty(); } - return new List(); - } + var remoteImages = new List(); - private IEnumerable GetImages(CollectionResult obj, string language, string baseUrl) - { - var list = new List(); - - var images = obj.Images ?? new CollectionImages(); - - list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo - { - 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 - })); - - list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo + 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 isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (!isLanguageEn) + var poster = collection.Images.Posters[i]; + remoteImages.Add(new RemoteImageInfo { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } + 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 + }); + } - if (string.IsNullOrEmpty(i.Language)) + for (var i = 0; i < collection.Images.Backdrops.Count; i++) + { + var backdrop = collection.Images.Backdrops[i]; + remoteImages.Add(new RemoteImageInfo { - return isLanguageEn ? 3 : 2; - } - - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - /// - /// 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; + 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 eligibleBackdrops.OrderByDescending(i => i.Vote_Average) - .ThenByDescending(i => i.Vote_Count); + return remoteImages.OrderByLanguageDescending(language); } - 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,268 +3,116 @@ 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 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 collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), 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)); - - return new[] { result }; - } - - return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken) - { - var tmdbId = id.GetProviderId(MetadataProvider.Tmdb); - - // We don't already have an Id, need to fetch it - if (string.IsNullOrEmpty(tmdbId)) - { - var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false); - - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb); - } - } - - var result = new MetadataResult(); - - if (!string.IsNullOrEmpty(tmdbId)) - { - var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult != null) + if (collection.Images != null) { - result.HasMetadata = true; - result.Item = GetItem(mainResult); + result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath); } - } - return result; - } + result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); - internal async Task GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException(nameof(tmdbId)); + return new[] { result }; } - await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language); + var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(dataFilePath)) + var collections = new List(); + for (var i = 0; i < collectionSearchResults.Count; i++) { - 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); + var collection = new RemoteSearchResult + { + Name = collectionSearchResults[i].Name, + SearchProviderName = Name + }; + collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture)); - if (mainResult == null) - { - return; + collections.Add(collection); } - var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage); - - Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _json.SerializeToFile(mainResult, dataFilePath); + return collections; } - private async Task FetchMainResult(string id, string language, CancellationToken cancellationToken) + public async Task> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken) { - var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey); - - if (!string.IsNullOrEmpty(language)) + 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 (tmdbId <= 0) { - url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); + var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false); - // Get images in english and with no language - url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); + if (searchResults != null && searchResults.Count > 0) + { + tmdbId = searchResults[0].Id; + } } - cancellationToken.ThrowIfCancellationRequested(); + var result = new MetadataResult(); - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in TmdbUtils.AcceptHeaders) + if (tmdbId > 0) { - 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); + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - - if (mainResult != null && string.IsNullOrEmpty(mainResult.Name)) - { - if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + if (collection != null) { - url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) + var item = new BoxSet { - // 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); - } - } + Name = collection.Name, + Overview = collection.Overview + }; - return mainResult; - } - - internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage); + item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); - 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; + result.HasMetadata = true; + result.Item = item; } } - 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; + return result; } public Task GetImageResponse(string url, CancellationToken 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