summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Api/JCoverSharedController.cs168
-rw-r--r--Api/JCoverStaticProvider.cs40
-rw-r--r--Api/ScriptInjector.cs74
-rw-r--r--Api/coverscript.js93
-rw-r--r--ImageProvider.cs51
-rw-r--r--Jellyfin.Plugin.JCoverXtremePro.csproj6
-rw-r--r--MediuxDownloader.cs28
-rw-r--r--POJO.cs63
-rw-r--r--Plugin.cs12
-rw-r--r--SeriesImageProvider.cs81
-rwxr-xr-xdevelop.sh7
-rw-r--r--docker-compose.yml16
13 files changed, 602 insertions, 40 deletions
diff --git a/.gitignore b/.gitignore
index b393798..80f672b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)
diff --git a/POJO.cs b/POJO.cs
index a19a98e..e54b462 100644
--- a/POJO.cs
+++ b/POJO.cs
@@ -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;
}
diff --git a/Plugin.cs b/Plugin.cs
index a4e1f6b..f0784fc 100644
--- a/Plugin.cs
+++ b/Plugin.cs
@@ -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/
+
+