From 9a06e4e555fe10edc9df4693e63b07aa6b568ddf Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 10 Dec 2024 00:56:16 +0100 Subject: feat: Add downloader --- Api/JCoverSharedController.cs | 132 +++++++++++++++++++++++++++++++++++++++++- Api/coverscript.js | 37 ++++++++++-- Folder.DotSettings.user | 19 +++++- ImageProvider.cs | 4 +- POJO.cs | 42 ++++++++++++++ SeriesImageProvider.cs | 25 ++++++-- 6 files changed, 247 insertions(+), 12 deletions(-) diff --git a/Api/JCoverSharedController.cs b/Api/JCoverSharedController.cs index a5eecc1..47a8c8a 100644 --- a/Api/JCoverSharedController.cs +++ b/Api/JCoverSharedController.cs @@ -1,9 +1,139 @@ +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.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")] -public class JCoverSharedController : ControllerBase +// [Authorize(Policy = "RequiresElevation")] +public class JCoverSharedController : BaseJellyfinApiController { + private readonly IProviderManager _providerManager; + private readonly ILibraryManager _libraryManager; + + /// + /// 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. + /// + 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 + })); + } + + [HttpPost("DownloadSeries")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task DownloadEntireSeriesImages( + [FromBody, Required] JsonObject setMeta + ) + { + // TODO: handle missing fields, local seasons missing, series missing, etc. + var setMetaObj = JsonSerializer.Deserialize(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(jsonMeta).set; + Dictionary lut = new(); + foreach (var showSeason in set.show.seasons) + { + foreach (var showEpisode in showSeason.episodes) + { + lut[showEpisode.id] = (showSeason.season_number, showEpisode.episode_number); + } + } + + Dictionary<(int, int), POJO.File> files = new(); + foreach (var file in set.files) + { + var episode = file.episode_id; + if (episode == null) + { + continue; + } + + var tup = lut[episode.id]; + files[tup] = file; + } + + 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}:"); + 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}"); + POJO.File file; + if (files.TryGetValue((seasonNumber, episodeNumber), out file)) + { + Plugin.Logger.LogInformation($" Found cover: {file.downloadUrl}"); + // await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) + // .ConfigureAwait(false); + + // await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage( + episode, file.downloadUrl, + // Note: this needs to be updated if SeriesImageProvider ever supports more image types + ImageType.Primary, + null, + CancellationToken.None + ).ConfigureAwait(false); + + await episode.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None) + .ConfigureAwait(false); + } + } + } + + return Empty; + } } \ No newline at end of file diff --git a/Api/coverscript.js b/Api/coverscript.js index 24f3c34..9f3eb7f 100644 --- a/Api/coverscript.js +++ b/Api/coverscript.js @@ -19,9 +19,12 @@ /** * * @param {HTMLElement} cloneFrom + * @param {string} setMeta * @return {HTMLElement} */ - function createDownloadSeriesButton(cloneFrom) { + function createDownloadSeriesButton( + cloneFrom, + setMeta) { /**/ //import LayersIcon from '@mui/icons-material/Layers'; //import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; @@ -35,12 +38,35 @@ element.appendChild(icon) element.addEventListener("click", ev => { ev.preventDefault() - - alert("YOU HAVE JUST BEEN INTERDICTED BY THE JCOVERXTREMEPRO SERIES DOWNLOADIFICATOR") + 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") @@ -51,9 +77,12 @@ 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 (downloadRowContainer.querySelector(`.${injectionMarker}`)) return // TODO: extract information about the series, and check if this is at all viable - downloadRowContainer.appendChild(createDownloadSeriesButton(element)) + downloadRowContainer.appendChild(createDownloadSeriesButton(element, setMeta)) }) }) diff --git a/Folder.DotSettings.user b/Folder.DotSettings.user index 87f4e76..8cd3da1 100644 --- a/Folder.DotSettings.user +++ b/Folder.DotSettings.user @@ -1,5 +1,22 @@  + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded - ForceIncluded \ No newline at end of file + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/ImageProvider.cs b/ImageProvider.cs index 8274a27..94f2598 100644 --- a/ImageProvider.cs +++ b/ImageProvider.cs @@ -68,8 +68,8 @@ public class ImageProvider var imageInfo = new RemoteImageInfo { Url = file.downloadUrl, - ProviderName = set.user_created.username + "(from Mediux)", - ThumbnailUrl = 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 diff --git a/POJO.cs b/POJO.cs index 7f44d53..dcc91a0 100644 --- a/POJO.cs +++ b/POJO.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -15,6 +16,17 @@ public class POJO [JsonIgnore] public IEnumerable allSets => sets.Concat(collectionSets); } + public class SetData + { + public Set set { get; set; } + } + + public class ShowData + { + // public Show show { get; set; } + public List sets { get; set; } + } + public class Set { public string id { get; set; } @@ -23,6 +35,29 @@ public class POJO public User user_created { get; set; } public List files { get; set; } + public Show? show { get; set; } + } + + public class Show + { + public string id { get; set; } + public string name { get; set; } + public List seasons { get; set; } + } + + public class Season + { + public string id { get; set; } + public int season_number { get; set; } + public string name { get; set; } + public List 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 @@ -30,11 +65,17 @@ public class POJO public string username { get; set; } } + public class EpisodeId + { + 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 ImageType? JellyFinFileType() { @@ -43,6 +84,7 @@ public class POJO case "backdrop": return ImageType.Backdrop; case "poster": + case "title_card": return ImageType.Primary; } diff --git a/SeriesImageProvider.cs b/SeriesImageProvider.cs index 7f48223..dd0f3b3 100644 --- a/SeriesImageProvider.cs +++ b/SeriesImageProvider.cs @@ -1,14 +1,18 @@ -using Microsoft.Extensions.Logging; +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; @@ -26,13 +30,14 @@ public class SeriesImageProvider { return [ + // Note: update JCoverSharedController if more image types are supported ImageType.Primary ]; } public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - // TODO: hadnle episodes + // TODO: handle specific episodes directly if (item is Series series) { return await HandleSeries(series, cancellationToken); @@ -51,8 +56,20 @@ public class SeriesImageProvider var metadata = await MediuxDownloader.instance.GetMediuxMetadata("https://mediux.pro/shows/" + tmdbId) .ConfigureAwait(false); - Plugin.Logger.LogInformation("JSON: " + metadata); - return []; + var show = JsonSerializer.Deserialize(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 GetImageResponse(string url, CancellationToken cancellationToken) -- cgit