From 6f2c165cc3d0000bda5722200971dd619833c349 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 4 Oct 2023 16:06:26 +0200 Subject: Use Authorization header in integration tests instead of X-Emby-Authorization And ensure the response has a successful status code --- tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 3dc62afaf8..5ddbd30d1e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests { public static class AuthHelper { - public const string AuthHeaderName = "X-Emby-Authorization"; - public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\""; + public const string AuthHeaderName = "Authorization"; + public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\""; public static async Task CompleteStartupAsync(HttpClient client) { @@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty())); Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); - using var content = JsonContent.Create( + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName"); + httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader); + httpRequest.Content = JsonContent.Create( new AuthenticateUserByName() { Username = user!.Name, Pw = user.Password, }, options: jsonOptions); - content.Headers.Add("X-Emby-Authorization", DummyAuthHeader); - using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content); + using var authResponse = await client.SendAsync(httpRequest); + authResponse.EnsureSuccessStatusCode(); + var auth = await JsonSerializer.DeserializeAsync( await authResponse.Content.ReadAsStreamAsync(), jsonOptions); -- cgit v1.2.3 From 852f1dc0c1e3178955e2d1b2791b1e45f3f956b3 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:16:17 +0200 Subject: Don't create non existent persons in LibraryManager.GetPerson return null instead. GetStudio, GetGenre, GetMusicGenre, GetYear, GetArtist still create a new one when the requested one doesn't exist Fixes #3901 --- .../Library/LibraryManager.cs | 27 +++++++++++----------- .../Controllers/PersonsControllerTests.cs | 26 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs (limited to 'tests') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151a..bdbf5f703c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,7 +46,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; @@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(name); var id = GetItemByNameId(path); - if (GetItemById(id) is not Person item) + if (GetItemById(id) is Person item) { - item = new Person - { - Name = name, - Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - Path = path - }; + return item; } - return item; + return null; } /// @@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library var saveEntity = false; var personEntity = GetPerson(person.Name); - // if PresentationUniqueKey is empty it's likely a new item. - if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + if (personEntity is null) { + var path = Person.GetPath(person.Name); + personEntity = new Person() + { + Name = person.Name, + Id = GetItemByNameId(path), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); saveEntity = true; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs new file mode 100644 index 0000000000..38c64547c2 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public class PersonsControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public PersonsControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetPerson_DoesntExist_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); + + using var response = await client.GetAsync($"Persons/DoesntExist"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} -- cgit v1.2.3 From c707baed8366cd44ea80713cf93e5f92971815bf Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:57:11 +0200 Subject: Jellyfin.Drawing minor improvements Reduce duplicate/dead code --- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 11 ----- .../Drawing/ImageProcessingOptions.cs | 3 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 10 +--- .../Drawing/ImageFormatExtensions.cs | 17 +++++++ .../MediaInfo/EmbeddedImageProvider.cs | 6 --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 +----- src/Jellyfin.Drawing/ImageProcessor.cs | 56 +--------------------- .../Drawing/ImageFormatExtensionsTests.cs | 13 +++++ 8 files changed, 37 insertions(+), 93 deletions(-) (limited to 'tests') diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index cdc3d52b9b..0d1e2a5a07 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; @@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing string? GetImageCacheTag(User user); - /// - /// Processes the image. - /// - /// The options. - /// To stream. - /// Task. - Task ProcessImage(ImageProcessingOptions options, Stream toStream); - /// /// Processes the image. /// @@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing /// The options. /// The library name to draw onto the collage. void CreateImageCollage(ImageCollageOptions options, string? libraryName); - - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 7912c5e87e..953cfe698e 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing private bool IsFormatSupported(string originalImagePath) { var ext = Path.GetExtension(originalImagePath); - return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); + ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase); + return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase)); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff196658..26f47a18f6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -650,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { ArgumentException.ThrowIfNullOrEmpty(inputPath); - var outputExtension = targetFormat switch - { - ImageFormat.Bmp => ".bmp", - ImageFormat.Gif => ".gif", - ImageFormat.Jpg => ".jpg", - ImageFormat.Png => ".png", - ImageFormat.Webp => ".webp", - _ => ".jpg" - }; + var outputExtension = targetFormat?.GetExtension() ?? ".jpg"; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs index 68a5c25345..1bb24112ec 100644 --- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs +++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs @@ -24,4 +24,21 @@ public static class ImageFormatExtensions ImageFormat.Webp => "image/webp", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; + + /// + /// Returns the correct extension for this . + /// + /// This . + /// The is an invalid enumeration value. + /// The correct extension for this . + public static string GetExtension(this ImageFormat format) + => format switch + { + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) + }; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index c24f4e2fcc..0bfee07fd6 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo ? Path.GetExtension(attachmentStream.FileName) : MimeTypes.ToExtension(attachmentStream.MimeType); - if (string.IsNullOrEmpty(extension)) - { - extension = ".jpg"; - } - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, ".png" => ImageFormat.Png, ".webp" => ImageFormat.Webp, _ => ImageFormat.Jpg diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 416f602179..03f90da8eb 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder { if (!orientation.HasValue) { - return SKEncodedOrigin.TopLeft; + return SKEncodedOrigin.Default; } - return orientation.Value switch - { - ImageOrientation.TopRight => SKEncodedOrigin.TopRight, - ImageOrientation.RightTop => SKEncodedOrigin.RightTop, - ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, - ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, - ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, - ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, - ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, - _ => SKEncodedOrigin.TopLeft - }; + return (SKEncodedOrigin)orientation.Value; } /// diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4f16e294bc..65a8f4e832 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - /// - public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) - { - var file = await ProcessImage(options).ConfigureAwait(false); - using var fileStream = AsyncFile.OpenRead(file.Path); - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } - /// public IReadOnlyCollection GetSupportedImageOutputFormats() => _imageEncoder.SupportedOutputFormats; - /// - public bool SupportsTransparency(string path) - => _transparentImageTypes.Contains(Path.GetExtension(path)); - /// public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) { @@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable } } - return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); + return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); } catch (Exception ex) { @@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return ImageFormat.Jpg; } - private string GetMimeType(ImageFormat format, string path) - => format switch - { - ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), - ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), - ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), - ImageFormat.Png => MimeTypes.GetMimeType("i.png"), - ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), - _ => MimeTypes.GetMimeType(path) - }; - /// /// Gets the cache file path based on a set of parameters. /// @@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable filename.Append(",v="); filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension()); } /// @@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return Task.FromResult((originalImagePath, dateModified)); } - // TODO _mediaEncoder.ConvertImage is not implemented - // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) - // { - // try - // { - // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); - // - // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; - // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); - // - // var file = _fileSystem.GetFileInfo(outputPath); - // if (!file.Exists) - // { - // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); - // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); - // } - // else - // { - // dateModified = file.LastWriteTimeUtc; - // } - // - // originalImagePath = outputPath; - // } - // catch (Exception ex) - // { - // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); - // } - // } - return Task.FromResult((originalImagePath, dateModified)); } diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs index a5bdb42d89..198ad5a274 100644 --- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs @@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)5)] public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws(() => format.GetMimeType()); + + [Theory] + [MemberData(nameof(GetAllImageFormats))] + public static void GetExtension_Valid_Valid(ImageFormat format) + => Assert.Null(Record.Exception(() => format.GetExtension())); + + [Theory] + [InlineData((ImageFormat)int.MinValue)] + [InlineData((ImageFormat)int.MaxValue)] + [InlineData((ImageFormat)(-1))] + [InlineData((ImageFormat)5)] + public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) + => Assert.Throws(() => format.GetExtension()); } -- cgit v1.2.3 From 305405c9a1ac4a37ac9e8eda44899c33cf76eda3 Mon Sep 17 00:00:00 2001 From: scampower3 <81431263+scampower3@users.noreply.github.com> Date: Tue, 10 Oct 2023 19:12:09 +0800 Subject: Combine Title and Overview for multi-episodes files for NFO file (#10080) --- .../Parsers/EpisodeNfoParser.cs | 41 ++++++++++++++++++++-- .../Parsers/EpisodeNfoProviderTests.cs | 6 ++-- 2 files changed, 42 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index d2f349ad7a..432b89c313 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.IO; +using System.Text; using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; @@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers } // Extract the last episode number from nfo + // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag + var name = new StringBuilder(item.Item.Name); + var overview = new StringBuilder(item.Item.Overview); while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1) { xml = xmlFile.Substring(0, index + srch.Length); @@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers { reader.MoveToContent(); - if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num)) + while (!reader.EOF && reader.ReadState == ReadState.Interactive) { - item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "name": + case "title": + case "localtitle": + name.Append(" / ").Append(reader.ReadElementContentAsString()); + break; + case "episode": + { + if (int.TryParse(reader.ReadElementContentAsString(), out var num)) + { + item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); + } + + break; + } + + case "biography": + case "plot": + case "review": + overview.Append(" / ").Append(reader.ReadElementContentAsString()); + break; + } + } + + reader.Read(); } } } + + item.Item.Name = name.ToString(); + item.Item.Overview = overview.ToString(); } catch (XmlException) { @@ -172,6 +208,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "displayafterseason": case "airsafter_season": { var val = reader.ReadElementContentAsString(); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index f63bc0e1bc..c0d06116b5 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using Jellyfin.Data.Enums; @@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None); var item = result.Item; - Assert.Equal("Rising (1)", item.Name); + Assert.Equal("Rising (1) / Rising (2)", item.Name); Assert.Equal(1, item.IndexNumber); Assert.Equal(2, item.IndexNumberEnd); Assert.Equal(1, item.ParentIndexNumber); - Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview); + Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview); Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate); Assert.Equal(2004, item.ProductionYear); } -- cgit v1.2.3