diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Api/JCoverSharedController.cs | 168 | ||||
-rw-r--r-- | Api/JCoverStaticProvider.cs | 40 | ||||
-rw-r--r-- | Api/ScriptInjector.cs | 74 | ||||
-rw-r--r-- | Api/coverscript.js | 93 | ||||
-rw-r--r-- | ImageProvider.cs | 51 | ||||
-rw-r--r-- | Jellyfin.Plugin.JCoverXtremePro.csproj | 6 | ||||
-rw-r--r-- | MediuxDownloader.cs | 28 | ||||
-rw-r--r-- | POJO.cs | 63 | ||||
-rw-r--r-- | Plugin.cs | 12 | ||||
-rw-r--r-- | SeriesImageProvider.cs | 81 | ||||
-rwxr-xr-x | develop.sh | 7 | ||||
-rw-r--r-- | docker-compose.yml | 16 |
13 files changed, 602 insertions, 40 deletions
@@ -2,6 +2,7 @@ bin obj .idea/ - +testenv +*.user diff --git a/Api/JCoverSharedController.cs b/Api/JCoverSharedController.cs new file mode 100644 index 0000000..dea10c4 --- /dev/null +++ b/Api/JCoverSharedController.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Jellyfin.Api; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.JCoverXtremePro.Api; + +[ApiController] +[Route("JCoverXtreme")] +// [Authorize(Policy = "RequiresElevation")] +public class JCoverSharedController : BaseJellyfinApiController +{ + private readonly IProviderManager _providerManager; + private readonly ILibraryManager _libraryManager; + + /// <summary> + /// This key is appended to image URLs to inform the frontend about the presence of a potential mass-download. + /// Keep in sync with coverscript.js#URL_META_KEY. + /// </summary> + public static string URL_META_KEY = "JCoverXtremeProMeta"; + + public JCoverSharedController( + IProviderManager providerManager, + IServerApplicationPaths applicationPaths, + ILibraryManager libraryManager) + { + _providerManager = providerManager; + _libraryManager = libraryManager; + } + + public static string AppendUrlMeta(string baseUrl, string key, string value) + { + return baseUrl + (baseUrl.Contains('?', StringComparison.InvariantCulture) ? "&" : "?") + + HttpUtility.UrlEncode(key) + "=" + HttpUtility.UrlEncode(value); + } + + public class SetMeta + { + public Guid seriesId { get; set; } + public string setId { get; set; } + } + + public static string PackSetInfo(string baseUrl, Series series, POJO.Set set) + { + return AppendUrlMeta( + baseUrl, + URL_META_KEY, + JsonSerializer.Serialize(new SetMeta + { + setId = set.id, + seriesId = series.Id + })); + } + + private static Dictionary<(int, int), POJO.File> CreateCoverFileLUT(POJO.Set set) + { + Dictionary<string, (int, int)> episodeIdToEpisodeNumber = new(); + foreach (var showSeason in set.show.seasons) + { + episodeIdToEpisodeNumber[showSeason.id] = (showSeason.season_number, -10); + foreach (var showEpisode in showSeason.episodes) + { + episodeIdToEpisodeNumber[showEpisode.id] = (showSeason.season_number, showEpisode.episode_number); + } + } + + Dictionary<(int, int), POJO.File> episodeNumberToFile = new(); + foreach (var file in set.files) + { + string id = string.Empty; + if (file.episode_id != null) + { + id = file.episode_id.id; + } + + if (file.season_id != null) + { + id = file.season_id.id; + } + + var tup = episodeIdToEpisodeNumber.GetValueOrDefault(id); + episodeNumberToFile[tup] = file; + } + + return episodeNumberToFile; + } + + [HttpPost("DownloadSeries")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task<ActionResult> DownloadEntireSeriesImages( + [FromBody, Required] JsonObject setMeta + ) + { + // TODO: handle missing fields, local seasons missing, series missing, etc. + var setMetaObj = JsonSerializer.Deserialize<SetMeta>(setMeta); + var series = _libraryManager.GetItemById(setMetaObj.seriesId) as Series; + var jsonMeta = await MediuxDownloader.instance.GetMediuxMetadata($"https://mediux.pro/sets/{setMetaObj.setId}") + .ConfigureAwait(false); + var set = JsonSerializer.Deserialize<POJO.SetData>(jsonMeta).set; + var files = CreateCoverFileLUT(set); + foreach (var item in series.GetSeasons(null, new DtoOptions(true))) + { + var season = item as Season; + var seasonNumber = season.GetLookupInfo().IndexNumber.Value; + Plugin.Logger.LogInformation($"Season id: {seasonNumber}:"); + await TryDownloadEpisode(season, files, (seasonNumber, -10)) + .ConfigureAwait(false); + foreach (var itemAgain in season.GetEpisodes()) + { + var episode = itemAgain as Episode; + var episodeNumber = episode.GetLookupInfo().IndexNumber.Value; + Plugin.Logger.LogInformation($" * Episode id: {episodeNumber} {episode.Name}"); + await TryDownloadEpisode(episode, files, (seasonNumber, episodeNumber)) + .ConfigureAwait(false); + } + } + + return Empty; + } + + private async Task TryDownloadEpisode( + BaseItem item, + Dictionary<(int, int), POJO.File> files, + (int, int) episodeNumber) + { + POJO.File file; + if (files.TryGetValue(episodeNumber, out file)) + { + Plugin.Logger.LogInformation($" Found cover: {file.downloadUrl}"); + await SaveCoverFileForItem(item, file.downloadUrl) + .ConfigureAwait(false); + } + } + + private async Task SaveCoverFileForItem( + BaseItem item, + string downloadUrl + ) + { + await _providerManager.SaveImage( + item, downloadUrl, + // Note: this needs to be updated if SeriesImageProvider ever supports more image types + ImageType.Primary, + null, + CancellationToken.None + ).ConfigureAwait(false); + + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None) + .ConfigureAwait(false); + } +}
\ No newline at end of file diff --git a/Api/JCoverStaticProvider.cs b/Api/JCoverStaticProvider.cs new file mode 100644 index 0000000..e4587ba --- /dev/null +++ b/Api/JCoverStaticProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.JCoverXtremePro.Api; + +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +/// <summary> +/// Static file server for the JavaScript snippet injected by <see cref="ScriptInjector"/> +/// </summary> +[ApiController] +[Route("JCoverXtremeProStatic")] +public class JCoverStaticProvider : ControllerBase +{ + private readonly Assembly assembly; + private readonly string scriptPath; + + public JCoverStaticProvider() + { + assembly = Assembly.GetExecutingAssembly(); + scriptPath = GetType().Namespace + ".coverscript.js"; + } + + [HttpGet("ClientScript")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Produces("application/javascript")] + public ActionResult GetClientScript() + { + Plugin.Logger.LogInformation($"Requesting ClientScript {scriptPath}"); + var scriptStream = assembly.GetManifestResourceStream(scriptPath); + if (scriptStream == null) + { + return NotFound(); + } + + return File(scriptStream, "application/javascript"); + } +}
\ No newline at end of file diff --git a/Api/ScriptInjector.cs b/Api/ScriptInjector.cs new file mode 100644 index 0000000..d6b1865 --- /dev/null +++ b/Api/ScriptInjector.cs @@ -0,0 +1,74 @@ +namespace Jellyfin.Plugin.JCoverXtremePro.Api; + +using System; +using System.IO; +using System.Text.RegularExpressions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Logging; + +/// <summary> +/// Utility for injecting a JavaScript script tag into the Jellyfin web frontend. +/// </summary> +public static class ScriptInjector +{ + public static void PerformInjection( + IApplicationPaths applicationPaths, + IServerConfigurationManager configurationManager + ) + { + var indexHtmlFilePath = Path.Combine(applicationPaths.WebPath, "index.html"); + if (!File.Exists(indexHtmlFilePath)) + { + Plugin.Logger.LogWarning("Could not find index html file"); + return; + } + + var html = File.ReadAllText(indexHtmlFilePath); + var snippet = GetInjectedSnippet(GetHTTPBasePath(configurationManager)); + if (html.Contains(snippet, StringComparison.InvariantCulture)) + { + Plugin.Logger.LogInformation("Not injecting existing HTML snippet."); + return; + } + + html = Regex.Replace(html, $"<script[^>]*guid=\"{Plugin.GUID}\"[^>]*></script>", string.Empty); + var bodyEnd = html.LastIndexOf("</body>", StringComparison.InvariantCulture); + if (bodyEnd < 0) + { + Plugin.Logger.LogError("Could not find end of body to inject script"); + return; + } + + html = html.Insert(bodyEnd, snippet); + try + { + File.WriteAllText(indexHtmlFilePath, html); + Plugin.Logger.LogInformation("Injected index.html"); + } + catch (Exception e) + { + Plugin.Logger.LogError(e, "Failed to write patched index.html"); + } + } + + public static string GetHTTPBasePath(IServerConfigurationManager configurationManager) + { + var networkConfig = configurationManager.GetConfiguration("network"); + var configType = networkConfig.GetType(); + var baseUrlField = configType.GetProperty("BaseUrl"); + var baseUrl = baseUrlField!.GetValue(networkConfig)!.ToString()!.Trim('/'); + return baseUrl; + } + + public static string GetScriptUrl(string basePath) + { + return basePath + "/JCoverXtremeProStatic/ClientScript"; + } + + public static string GetInjectedSnippet(string basePath) + { + return + $"<script guid=\"{Plugin.GUID}\" plugin=\"{Plugin.Instance!.Name}\" src=\"{GetScriptUrl(basePath)}\" defer></script>"; + } +}
\ No newline at end of file diff --git a/Api/coverscript.js b/Api/coverscript.js new file mode 100644 index 0000000..a4f0c04 --- /dev/null +++ b/Api/coverscript.js @@ -0,0 +1,93 @@ +(function () { + /** + * + * @param {HTMLElement} element + * @param {string} selector + * @returns {HTMLElement | null} + */ + function findParent(element, selector) { + let p = element + while (p) { + if (p.matches(selector)) return p + p = p.parentNode + } + return null + } + + const injectionMarker = "JCoverXtremePro-injection-marker"; + + /** + * + * @param {HTMLElement} cloneFrom + * @param {string} setMeta + * @return {HTMLElement} + */ + function createDownloadSeriesButton( + cloneFrom, + setMeta) { + /*<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize paper-icon-button-light" raised"="" title="Download"><span class="material-icons cloud_download" aria-hidden="true"></span></button>*/ + //import LayersIcon from '@mui/icons-material/Layers'; + //import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; + const element = document.createElement("button") + element.classList.add(...cloneFrom.classList) + element.classList.add(injectionMarker) + element.title = "Download Series" + const icon = document.createElement("span") + icon.classList.add("material-icons", "burst_mode") + icon.setAttribute("aria-hidden", "true") + element.appendChild(icon) + element.addEventListener("click", ev => { + ev.preventDefault() + console.log("Executing mass covering event! We will try to download the entirety of set " + setMeta) + fetch("/JCoverXtreme/DownloadSeries", + { + method: 'POST', + body: setMeta, + headers: { + "content-type": "application/json" + } + }).then(console.log) // TODO: check out the root somehow. for now just assume / + }) + return element + } + + /** + * Keep in sync with JCoverSharedController.URL_META_KEY + * @type {string} + */ + const URL_META_KEY = "JCoverXtremeProMeta" + + /** + * Extract the JCoverXtremePro metadata from an image url. + * + * @param {string} url + * @return {string} + */ + function extractSetMeta(url) { + return new URL(url).searchParams.get(URL_META_KEY) + } + + const observer = new MutationObserver(() => { + console.log("JCoverXtremePro observation was triggered!") + console.log("Listing all download buttons") + /** + * @type {NodeListOf<Element>} + */ + const buttons = document.querySelectorAll(".imageEditorCard .cardFooter .btnDownloadRemoteImage") + + buttons.forEach(element => { + const downloadRowContainer = findParent(element, ".cardText") + const cardContainer = findParent(element, ".cardBox") + const cardImage = cardContainer.querySelector("a.cardImageContainer[href]") + const setMeta = extractSetMeta(cardImage.href) + if (!setMeta) return; + if (downloadRowContainer.querySelector(`.${injectionMarker}`)) return + downloadRowContainer.appendChild(createDownloadSeriesButton(element, setMeta)) + }) + + }) + observer.observe(document.body, {// TODO: selectively observe the body if at all possible + subtree: true, + childList: true, + }); +})()
\ No newline at end of file diff --git a/ImageProvider.cs b/ImageProvider.cs index df349eb..94f2598 100644 --- a/ImageProvider.cs +++ b/ImageProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -37,58 +38,46 @@ public class ImageProvider return new List<ImageType> { ImageType.Primary, - // ImageType.Backdrop, + ImageType.Backdrop, }; } public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var movie = item as Movie; var movieId = item.GetProviderId(MetadataProvider.Tmdb); - var collectionId = item.GetProviderId(MetadataProvider.TmdbCollection); - _logger.LogInformation( - $"Help i am stuck in a movie labeling factory and have been taskted to label {movie.Name} " + - $"({movieId} in collection {collectionId})" - ); - var movieData = + var movieJson = await MediuxDownloader.instance.GetMediuxMetadata("https://mediux.pro/movies/" + movieId) .ConfigureAwait(false); - var deserMovieData = JsonSerializer.Deserialize<POJO.MovieData>(movieData as JsonObject); - _logger.LogInformation("Movie Data: {JsonData}", movieData.ToJsonString()); - _logger.LogInformation("Movie Data Decoded: {Data}", JsonSerializer.Serialize(deserMovieData)); + var movieData = JsonSerializer.Deserialize<POJO.MovieData>(movieJson as JsonObject); List<RemoteImageInfo> images = new(); - foreach (var set in deserMovieData.allSets) + foreach (var set in movieData.allSets) { - _logger.LogInformation("Set Data: {Name} {Data}", set.set_name, set.files.Count); foreach (var file in set.files) { - _logger.LogInformation("Matching file {Name}", JsonSerializer.Serialize(file)); - if (file.fileType != "poster") + var ft = file.JellyFinFileType(); + if (ft == null) { - _logger.LogInformation("Skipping non poster file"); continue; } - if (file.title.Contains(deserMovieData.movie.title)) + if (!file.title.Contains(movieData.movie.title, StringComparison.InvariantCulture)) { - _logger.LogInformation("Adding image"); - var imageInfo = new RemoteImageInfo - { - Url = file.downloadUrl, - ProviderName = Name, - ThumbnailUrl = file.downloadUrl, - Language = "en", - RatingType = RatingType.Likes, - Type = ImageType.Primary, - }; - _logger.LogInformation("Constructed image"); - images.Add(imageInfo); - _logger.LogInformation("Appended image"); + continue; } + + var imageInfo = new RemoteImageInfo + { + Url = file.downloadUrl, + ProviderName = set.user_created.username + " (via Mediux)", + ThumbnailUrl = file.downloadUrl, // TODO: use generated thumbnails from /_next/image?url= + Language = "en", + RatingType = RatingType.Likes, + Type = ft.Value + }; + images.Add(imageInfo); } } - _logger.LogInformation("Collected images {0}", images); return images; } diff --git a/Jellyfin.Plugin.JCoverXtremePro.csproj b/Jellyfin.Plugin.JCoverXtremePro.csproj index 8455ca0..33412ff 100644 --- a/Jellyfin.Plugin.JCoverXtremePro.csproj +++ b/Jellyfin.Plugin.JCoverXtremePro.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <RootNamespace>Jellyfin.Plugin.JCoverXtremePro</RootNamespace> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>false</TreatWarningsAsErrors> @@ -13,6 +13,9 @@ <ItemGroup> <PackageReference Include="Jellyfin.Controller" Version="10.8.13"/> <PackageReference Include="Jellyfin.Model" Version="10.8.13"/> + <Reference Include="Jellyfin.Api"> + <HintPath>/usr/lib/jellyfin/Jellyfin.Api.dll</HintPath> + </Reference> </ItemGroup> <ItemGroup> @@ -24,6 +27,7 @@ <ItemGroup> <None Remove="Configuration\configPage.html"/> <EmbeddedResource Include="Configuration\configPage.html"/> + <EmbeddedResource Include="Api\coverscript.js"/> </ItemGroup> </Project> diff --git a/MediuxDownloader.cs b/MediuxDownloader.cs index a027773..c7c415d 100644 --- a/MediuxDownloader.cs +++ b/MediuxDownloader.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -71,11 +73,31 @@ public class MediuxDownloader .Content.ReadAsStringAsync().ConfigureAwait(false); } + private ConcurrentDictionary<string, SemaphoreSlim> cacheLock = new(); + private ConcurrentDictionary<string, JsonNode> cache = new(); + public async Task<JsonNode> GetMediuxMetadata(string url) { - Plugin.Logger.LogInformation("Loading data from {Url}", url); - var text = await GetString(url).ConfigureAwait(false); - return ExtractJsonNodes(text).First(); + var semaphore = cacheLock.GetOrAdd(url, ignored => new SemaphoreSlim(1)); + try + { + await semaphore.WaitAsync().ConfigureAwait(false); + if (cache.TryGetValue(url, out var data)) + { + Plugin.Logger.LogInformation("Loading cached data from {Url}", url); + return data; + } + + Plugin.Logger.LogInformation("Loading data from {Url}", url); + var text = await GetString(url).ConfigureAwait(false); + var node = ExtractJsonNodes(text).First(); + cache[url] = node; + return node; + } + finally + { + semaphore.Release(); + } } public async Task<HttpResponseMessage> DownloadFile(string url) @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using MediaBrowser.Model.Entities; namespace Jellyfin.Plugin.JCoverXtremePro; @@ -14,6 +16,17 @@ public class POJO [JsonIgnore] public IEnumerable<Set> allSets => sets.Concat(collectionSets); } + public class SetData + { + public Set set { get; set; } + } + + public class ShowData + { + // public Show show { get; set; } + public List<Set> sets { get; set; } + } + public class Set { public string id { get; set; } @@ -22,6 +35,29 @@ public class POJO public User user_created { get; set; } public List<File> files { get; set; } + public Show? show { get; set; } + } + + public class Show + { + public string id { get; set; } + public string name { get; set; } + public List<Season> seasons { get; set; } + } + + public class Season + { + public string id { get; set; } + public int season_number { get; set; } + public string name { get; set; } + public List<Episode> episodes { get; set; } + } + + public class Episode + { + public int episode_number { get; set; } + public string episode_name { get; set; } + public string id { get; set; } } public class User @@ -29,11 +65,38 @@ public class POJO public string username { get; set; } } + public class EpisodeId + { + public string id { get; set; } + } + + public class SeasonId + { + public string id { get; set; } + } + public class File { public string fileType { get; set; } public string title { get; set; } public string id { get; set; } + public EpisodeId? episode_id { get; set; } + public SeasonId? season_id { get; set; } + + public ImageType? JellyFinFileType() + { + switch (fileType) + { + case "backdrop": + return ImageType.Backdrop; + case "poster": + case "title_card": + return ImageType.Primary; + } + + return null; + } + [JsonIgnore] public string downloadUrl => "https://api.mediux.pro/assets/" + id; } @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Net.Http; +using Jellyfin.Plugin.JCoverXtremePro.Api; using Jellyfin.Plugin.JellyFed.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Plugins; @@ -18,18 +21,19 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages public Plugin( IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILibraryManager libraryManager, ILogger<Plugin> logger, - IHttpClientFactory httpClientFactory + IHttpClientFactory httpClientFactory, + IServerConfigurationManager configurationManager ) : base(applicationPaths, xmlSerializer) { - logger.LogInformation("Loaded plugin with library manager {}", libraryManager); MediuxDownloader.instance = new MediuxDownloader(httpClientFactory); Instance = this; Logger = logger; + ScriptInjector.PerformInjection(applicationPaths, configurationManager); } public override string Name => "JCoverXtremePro"; - - public override Guid Id => Guid.Parse("f3e43e23-4b28-4b2f-a29d-37267e2ea2e2"); + public static Guid GUID = Guid.Parse("f3e43e23-4b28-4b2f-a29d-37267e2ea2e2"); + public override Guid Id => GUID; public static Plugin? Instance { get; private set; } diff --git a/SeriesImageProvider.cs b/SeriesImageProvider.cs new file mode 100644 index 0000000..dd0f3b3 --- /dev/null +++ b/SeriesImageProvider.cs @@ -0,0 +1,81 @@ +using System.Linq; + +namespace Jellyfin.Plugin.JCoverXtremePro; + +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Plugin.JCoverXtremePro.Api; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +public class SeriesImageProvider + : IRemoteImageProvider, IHasOrder +{ + public bool Supports(BaseItem item) + { + return item is Episode or Series; + } + + public string Name => "Mediux Series"; + + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) + { + return + [ + // Note: update JCoverSharedController if more image types are supported + ImageType.Primary + ]; + } + + public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) + { + // TODO: handle specific episodes directly + if (item is Series series) + { + return await HandleSeries(series, cancellationToken); + } + + return []; + } + + public async Task<IEnumerable<RemoteImageInfo>> HandleSeries(Series series, CancellationToken token) + { + var tmdbId = series.GetProviderId(MetadataProvider.Tmdb); + if (tmdbId == null) + { + return []; // TODO: handle missing id + } + + var metadata = await MediuxDownloader.instance.GetMediuxMetadata("https://mediux.pro/shows/" + tmdbId) + .ConfigureAwait(false); + var show = JsonSerializer.Deserialize<POJO.ShowData>(metadata as JsonObject)!; + + return from set in show.sets + let representativeImage = set.files.Find(it => it.fileType is "poster" or "title_card")! + let enrichedUrl = JCoverSharedController.PackSetInfo(representativeImage.downloadUrl, series, set) + select new RemoteImageInfo + { + Url = enrichedUrl, + ProviderName = set.user_created.username + " (via Mediux)", + ThumbnailUrl = enrichedUrl, // TODO: use generated thumbnails from /_next/image?url= + Language = "en", + RatingType = RatingType.Likes, + Type = representativeImage.JellyFinFileType().Value + }; + } + + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return MediuxDownloader.instance.DownloadFile(url); + } + + public int Order => 0; +}
\ No newline at end of file diff --git a/develop.sh b/develop.sh new file mode 100755 index 0000000..440ad42 --- /dev/null +++ b/develop.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +dotnet build +sudo cp ./bin/Debug/net8.0/Jellyfin.Plugin.JCoverXtremePro.* testenv/config/plugins/JCoverXtremePro +docker compose up + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a6ade5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ + +services: + jellyfin: + image: jellyfin/jellyfin + network_mode: host + volumes: + - ./testenv/config:/config + - ./testenv/cache:/cache + - ./testenv/media:/media + develop: + watch: + - action: sync+restart + path: ./bin/Debug/net8.0/ + target: /config/plugins/JCoverXtremePro/ + + |