diff options
| -rw-r--r-- | .github/workflows/ci-codeql-analysis.yml | 6 | ||||
| -rw-r--r-- | .github/workflows/ci-compat.yml | 8 | ||||
| -rw-r--r-- | .github/workflows/ci-openapi.yml | 12 | ||||
| -rw-r--r-- | Emby.Naming/Book/BookFileNameParser.cs | 75 | ||||
| -rw-r--r-- | Emby.Naming/Book/BookFileNameParserResult.cs | 41 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Library/LibraryManager.cs | 2 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs | 34 | ||||
| -rw-r--r-- | Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs | 53 | ||||
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 32 | ||||
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 13 | ||||
| -rw-r--r-- | Jellyfin.Server/Startup.cs | 3 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Persistence/IItemRepository.cs | 2 | ||||
| -rw-r--r-- | src/Jellyfin.Extensions/EnumerableExtensions.cs | 6 |
13 files changed, 179 insertions, 108 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 5a6cccda0..0823cf9be 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 8a755a317..a3c49969c 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 0a391dbe1..46af68e58 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -27,7 +27,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: openapi-head retention-days: 14 @@ -61,7 +61,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: openapi-base retention-days: 14 @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-base path: openapi-base @@ -158,7 +158,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-head path: openapi-head diff --git a/Emby.Naming/Book/BookFileNameParser.cs b/Emby.Naming/Book/BookFileNameParser.cs new file mode 100644 index 000000000..28625f16d --- /dev/null +++ b/Emby.Naming/Book/BookFileNameParser.cs @@ -0,0 +1,75 @@ +using System.Text.RegularExpressions; + +namespace Emby.Naming.Book +{ + /// <summary> + /// Helper class to retrieve basic metadata from a book filename. + /// </summary> + public static class BookFileNameParser + { + private const string NameMatchGroup = "name"; + private const string IndexMatchGroup = "index"; + private const string YearMatchGroup = "year"; + private const string SeriesNameMatchGroup = "seriesName"; + + private static readonly Regex[] _nameMatches = + [ + // seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required + new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"), + new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"), + new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"), + new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"), + // last resort matches the whole string as the name + new Regex(@"(?<name>.*)") + ]; + + /// <summary> + /// Parse a filename name to retrieve the book name, series name, index, and year. + /// </summary> + /// <param name="name">Book filename to parse for information.</param> + /// <returns>Returns <see cref="BookFileNameParserResult"/> object.</returns> + public static BookFileNameParserResult Parse(string? name) + { + var result = new BookFileNameParserResult(); + + if (name == null) + { + return result; + } + + foreach (var regex in _nameMatches) + { + var match = regex.Match(name); + + if (!match.Success) + { + continue; + } + + if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success) + { + result.Name = nameGroup.Value.Trim(); + } + + if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index)) + { + result.Index = index; + } + + if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year)) + { + result.Year = year; + } + + if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success) + { + result.SeriesName = seriesGroup.Value.Trim(); + } + + break; + } + + return result; + } + } +} diff --git a/Emby.Naming/Book/BookFileNameParserResult.cs b/Emby.Naming/Book/BookFileNameParserResult.cs new file mode 100644 index 000000000..f29716b9e --- /dev/null +++ b/Emby.Naming/Book/BookFileNameParserResult.cs @@ -0,0 +1,41 @@ +using System; + +namespace Emby.Naming.Book +{ + /// <summary> + /// Data object used to pass metadata parsed from a book filename. + /// </summary> + public class BookFileNameParserResult + { + /// <summary> + /// Initializes a new instance of the <see cref="BookFileNameParserResult"/> class. + /// </summary> + public BookFileNameParserResult() + { + Name = null; + Index = null; + Year = null; + SeriesName = null; + } + + /// <summary> + /// Gets or sets the name of the book. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the book index. + /// </summary> + public int? Index { get; set; } + + /// <summary> + /// Gets or sets the publication year. + /// </summary> + public int? Year { get; set; } + + /// <summary> + /// Gets or sets the series name. + /// </summary> + public string? SeriesName { get; set; } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cab87e53d..30c3e89b4 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2143,7 +2143,7 @@ namespace Emby.Server.Implementations.Library item.ValidateImages(); - _itemRepository.SaveImages(item); + await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false); RegisterItem(item); } diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 464a548ab..1e885aad6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -5,12 +5,12 @@ using System; using System.IO; using System.Linq; +using Emby.Naming.Book; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers.Books { @@ -35,17 +35,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var extension = Path.GetExtension(args.Path.AsSpan()); - if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { - // It's a book - return new Book - { - Path = args.Path, - IsInMixedFolder = true - }; + return null; } - return null; + var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path)); + + return new Book + { + Path = args.Path, + Name = result.Name ?? string.Empty, + IndexNumber = result.Index, + ProductionYear = result.Year, + SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)), + IsInMixedFolder = true, + }; } private Book GetBook(ItemResolveArgs args) @@ -59,15 +64,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books StringComparison.OrdinalIgnoreCase); }).ToList(); - // Don't return a Book if there is more (or less) than one document in the directory + // directory is only considered a book when it contains exactly one supported file + // other library structures with multiple books to a directory will get picked up as individual files if (bookFiles.Count != 1) { return null; } + var result = BookFileNameParser.Parse(Path.GetFileName(args.Path)); + return new Book { - Path = bookFiles[0].FullName + Path = bookFiles[0].FullName, + Name = result.Name ?? string.Empty, + IndexNumber = result.Index, + ProductionYear = result.Year, + SeriesName = result.SeriesName ?? string.Empty, }; } } diff --git a/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs deleted file mode 100644 index 2cbb18326..000000000 --- a/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Api.Middleware; - -/// <summary> -/// Removes /emby and /mediabrowser from requested route. -/// </summary> -public class LegacyEmbyRouteRewriteMiddleware -{ - private const string EmbyPath = "/emby"; - private const string MediabrowserPath = "/mediabrowser"; - - private readonly RequestDelegate _next; - private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - /// <param name="logger">The logger.</param> - public LegacyEmbyRouteRewriteMiddleware( - RequestDelegate next, - ILogger<LegacyEmbyRouteRewriteMiddleware> logger) - { - _next = next; - _logger = logger; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext) - { - var localPath = httpContext.Request.Path.ToString(); - if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) - { - httpContext.Request.Path = localPath[EmbyPath.Length..]; - _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); - } - else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) - { - httpContext.Request.Path = localPath[MediabrowserPath.Length..]; - _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); - } - - await _next(httpContext).ConfigureAwait(false); - } -} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index dfe46ef8f..9851d53c4 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -547,22 +547,34 @@ public sealed class BaseItemRepository } /// <inheritdoc /> - public void SaveImages(BaseItemDto item) + public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(item); - var images = item.ImageInfos.Select(e => Map(item.Id, e)); - using var context = _dbProvider.CreateDbContext(); + var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray(); - if (!context.BaseItems.Any(bi => bi.Id == item.Id)) + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); - return; - } + if (!await context.BaseItems + .AnyAsync(bi => bi.Id == item.Id, cancellationToken) + .ConfigureAwait(false)) + { + _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); + return; + } - context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); - context.BaseItemImageInfos.AddRange(images); - context.SaveChanges(); + await context.BaseItemImageInfos + .Where(e => e.ItemId == item.Id) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + + await context.BaseItemImageInfos + .AddRangeAsync(images, cancellationToken) + .ConfigureAwait(false); + + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } /// <inheritdoc /> diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index a56baba33..9fd853cf2 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -117,18 +117,5 @@ namespace Jellyfin.Server.Extensions { return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>(); } - - /// <summary> - /// Adds /emby and /mediabrowser route trimming to the application pipeline. - /// </summary> - /// <remarks> - /// This must be injected before any path related middleware. - /// </remarks> - /// <param name="appBuilder">The application builder.</param> - /// <returns>The updated application builder.</returns> - public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder) - { - return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>(); - } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 5032b2aec..f6a4ae7d6 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -173,9 +173,6 @@ namespace Jellyfin.Server mainApp.UseHttpsRedirection(); } - // This must be injected before any path related middleware. - mainApp.UsePathTrim(); - if (appConfig.HostWebClient()) { var extensionProvider = new FileExtensionContentTypeProvider(); diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0026ab2b5..00c492742 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -33,7 +33,7 @@ public interface IItemRepository /// <param name="cancellationToken">The cancellation token.</param> void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); - void SaveImages(BaseItem item); + Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); /// <summary> /// Retrieves the item. diff --git a/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs index 3eb9da01f..0c7875623 100644 --- a/src/Jellyfin.Extensions/EnumerableExtensions.cs +++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs @@ -64,13 +64,13 @@ public static class EnumerableExtensions /// <typeparam name="T">The type of item.</typeparam> /// <returns>The IEnumerable{Enum}.</returns> public static IEnumerable<T> GetUniqueFlags<T>(this T flags) - where T : Enum + where T : struct, Enum { - foreach (Enum value in Enum.GetValues(flags.GetType())) + foreach (T value in Enum.GetValues<T>()) { if (flags.HasFlag(value)) { - yield return (T)value; + yield return value; } } } |
