| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.IO; |
| | | 4 | | using System.Linq; |
| | | 5 | | using System.Threading; |
| | | 6 | | using System.Threading.Tasks; |
| | | 7 | | using Jellyfin.Extensions; |
| | | 8 | | using MediaBrowser.Controller.Entities; |
| | | 9 | | using MediaBrowser.Controller.Providers; |
| | | 10 | | using MediaBrowser.Model.Drawing; |
| | | 11 | | using MediaBrowser.Model.Entities; |
| | | 12 | | using Microsoft.Extensions.Logging; |
| | | 13 | | using SharpCompress.Archives; |
| | | 14 | | |
| | | 15 | | namespace MediaBrowser.Providers.Books; |
| | | 16 | | |
| | | 17 | | /// <summary> |
| | | 18 | | /// The ComicImageProvider tries to find either an image named "cover" or, in case that |
| | | 19 | | /// fails, just takes the first image inside the archive, hoping that it is the cover. |
| | | 20 | | /// </summary> |
| | | 21 | | public class ComicImageProvider : IDynamicImageProvider |
| | | 22 | | { |
| | 21 | 23 | | private readonly string[] _comicBookExtensions = [".cb7", ".cbr", ".cbt", ".cbz"]; |
| | 21 | 24 | | private readonly string[] _coverExtensions = [".png", ".jpeg", ".jpg", ".webp", ".bmp", ".gif"]; |
| | | 25 | | |
| | | 26 | | private readonly ILogger<ComicImageProvider> _logger; |
| | | 27 | | |
| | | 28 | | /// <summary> |
| | | 29 | | /// Initializes a new instance of the <see cref="ComicImageProvider"/> class. |
| | | 30 | | /// </summary> |
| | | 31 | | /// <param name="logger">Instance of the <see cref="ILogger{ComicImageProvider}"/> interface.</param> |
| | | 32 | | public ComicImageProvider(ILogger<ComicImageProvider> logger) |
| | | 33 | | { |
| | 21 | 34 | | _logger = logger; |
| | 21 | 35 | | } |
| | | 36 | | |
| | | 37 | | /// <inheritdoc /> |
| | 0 | 38 | | public string Name => "Comic Book Archive Cover Extractor"; |
| | | 39 | | |
| | | 40 | | /// <inheritdoc /> |
| | | 41 | | public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken) |
| | | 42 | | { |
| | 0 | 43 | | var extension = Path.GetExtension(item.Path); |
| | | 44 | | |
| | 0 | 45 | | if (_comicBookExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) |
| | | 46 | | { |
| | 0 | 47 | | return LoadCover(item); |
| | | 48 | | } |
| | | 49 | | |
| | 0 | 50 | | return Task.FromResult(new DynamicImageResponse { HasImage = false }); |
| | | 51 | | } |
| | | 52 | | |
| | | 53 | | /// <inheritdoc /> |
| | | 54 | | public IEnumerable<ImageType> GetSupportedImages(BaseItem item) |
| | | 55 | | { |
| | 0 | 56 | | yield return ImageType.Primary; |
| | 0 | 57 | | } |
| | | 58 | | |
| | | 59 | | /// <inheritdoc /> |
| | | 60 | | public bool Supports(BaseItem item) |
| | | 61 | | { |
| | 56 | 62 | | return item is Book; |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// Tries to load a cover from the CBZ archive. Returns a response |
| | | 67 | | /// with no image if nothing is found. |
| | | 68 | | /// </summary> |
| | | 69 | | /// <param name="item">Item to check for covers.</param> |
| | | 70 | | private async Task<DynamicImageResponse> LoadCover(BaseItem item) |
| | | 71 | | { |
| | 0 | 72 | | var memoryStream = new MemoryStream(); |
| | | 73 | | |
| | | 74 | | try |
| | | 75 | | { |
| | | 76 | | ImageFormat imageFormat; |
| | | 77 | | |
| | 0 | 78 | | using (Stream stream = File.OpenRead(item.Path)) |
| | 0 | 79 | | using (var archive = ArchiveFactory.Open(stream)) |
| | | 80 | | { |
| | | 81 | | // throw exception to log results if no cover is found |
| | 0 | 82 | | (var cover, imageFormat) = FindCoverEntryInArchive(archive) ?? throw new InvalidOperationException("no s |
| | | 83 | | |
| | | 84 | | // copy the cover to memory stream |
| | 0 | 85 | | await cover.OpenEntryStream().CopyToAsync(memoryStream).ConfigureAwait(false); |
| | 0 | 86 | | } |
| | | 87 | | |
| | | 88 | | // reset stream position after copying |
| | 0 | 89 | | memoryStream.Position = 0; |
| | | 90 | | |
| | 0 | 91 | | return new DynamicImageResponse { HasImage = true, Stream = memoryStream, Format = imageFormat }; |
| | | 92 | | } |
| | 0 | 93 | | catch (Exception e) |
| | | 94 | | { |
| | 0 | 95 | | _logger.LogError(e, "failed to load cover from {Path}", item.Path); |
| | 0 | 96 | | return new DynamicImageResponse { HasImage = false }; |
| | | 97 | | } |
| | 0 | 98 | | } |
| | | 99 | | |
| | | 100 | | /// <summary> |
| | | 101 | | /// Tries to find the entry containing the cover. |
| | | 102 | | /// </summary> |
| | | 103 | | /// <param name="archive">The archive to search.</param> |
| | | 104 | | /// <returns>The search result.</returns> |
| | | 105 | | private (IArchiveEntry CoverEntry, ImageFormat ImageFormat)? FindCoverEntryInArchive(IArchive archive) |
| | | 106 | | { |
| | | 107 | | IArchiveEntry? cover; |
| | | 108 | | |
| | | 109 | | // only some comics will explicitly name their cover file |
| | | 110 | | // in many cases the cover will simply be the first image in the archive |
| | 0 | 111 | | foreach (var extension in _coverExtensions) |
| | | 112 | | { |
| | 0 | 113 | | cover = archive.Entries.FirstOrDefault(e => e.Key == "cover" + extension); |
| | | 114 | | |
| | 0 | 115 | | if (cover is not null) |
| | | 116 | | { |
| | 0 | 117 | | var imageFormat = GetImageFormat(extension); |
| | | 118 | | |
| | 0 | 119 | | return (cover, imageFormat); |
| | | 120 | | } |
| | | 121 | | } |
| | | 122 | | |
| | 0 | 123 | | cover = archive.Entries.OrderBy(x => x.Key).FirstOrDefault(x => _coverExtensions.Contains(Path.GetExtension(x.Ke |
| | | 124 | | |
| | 0 | 125 | | if (cover is not null) |
| | | 126 | | { |
| | 0 | 127 | | var imageFormat = GetImageFormat(Path.GetExtension(cover.Key ?? string.Empty)); |
| | | 128 | | |
| | 0 | 129 | | return (cover, imageFormat); |
| | | 130 | | } |
| | | 131 | | |
| | 0 | 132 | | return null; |
| | | 133 | | } |
| | | 134 | | |
| | 0 | 135 | | private static ImageFormat GetImageFormat(string extension) => extension.ToLowerInvariant() switch |
| | 0 | 136 | | { |
| | 0 | 137 | | ".jpg" => ImageFormat.Jpg, |
| | 0 | 138 | | ".jpeg" => ImageFormat.Jpg, |
| | 0 | 139 | | ".png" => ImageFormat.Png, |
| | 0 | 140 | | ".webp" => ImageFormat.Webp, |
| | 0 | 141 | | ".bmp" => ImageFormat.Bmp, |
| | 0 | 142 | | ".gif" => ImageFormat.Gif, |
| | 0 | 143 | | ".svg" => ImageFormat.Svg, |
| | 0 | 144 | | _ => throw new ArgumentException($"unsupported extension: {extension}"), |
| | 0 | 145 | | }; |
| | | 146 | | } |