diff options
-rw-r--r-- | .gitignore | 7 | ||||
-rw-r--r-- | Configuration/PluginConfiguration.cs | 57 | ||||
-rw-r--r-- | Configuration/configPage.html | 80 | ||||
-rw-r--r-- | ImageProvider.cs | 101 | ||||
-rw-r--r-- | Jellyfin.Plugin.JCoverXtremePro.csproj | 29 | ||||
-rw-r--r-- | MediuxDownloader.cs | 85 | ||||
-rw-r--r-- | POJO.cs | 48 | ||||
-rw-r--r-- | Plugin.cs | 51 | ||||
-rw-r--r-- | jellyfin.ruleset | 109 |
9 files changed, 567 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b393798 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin +obj +.idea/ + + + + diff --git a/Configuration/PluginConfiguration.cs b/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000..8268858 --- /dev/null +++ b/Configuration/PluginConfiguration.cs @@ -0,0 +1,57 @@ +using MediaBrowser.Model.Plugins; + +namespace Jellyfin.Plugin.JellyFed.Configuration; + +/// <summary> +/// The configuration options. +/// </summary> +public enum SomeOptions +{ + /// <summary> + /// Option one. + /// </summary> + OneOption, + + /// <summary> + /// Second option. + /// </summary> + AnotherOption +} + +/// <summary> +/// Plugin configuration. +/// </summary> +public class PluginConfiguration : BasePluginConfiguration +{ + /// <summary> + /// Initializes a new instance of the <see cref="PluginConfiguration" /> class. + /// </summary> + public PluginConfiguration() + { + // set default options here + Options = SomeOptions.AnotherOption; + TrueFalseSetting = true; + AnInteger = 2; + AString = "string"; + } + + /// <summary> + /// Gets or sets a value indicating whether some true or false setting is enabled. + /// </summary> + public bool TrueFalseSetting { get; set; } + + /// <summary> + /// Gets or sets an integer setting. + /// </summary> + public int AnInteger { get; set; } + + /// <summary> + /// Gets or sets a string setting. + /// </summary> + public string AString { get; set; } + + /// <summary> + /// Gets or sets an enum option. + /// </summary> + public SomeOptions Options { get; set; } +}
\ No newline at end of file diff --git a/Configuration/configPage.html b/Configuration/configPage.html new file mode 100644 index 0000000..b72df9f --- /dev/null +++ b/Configuration/configPage.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Template</title> +</head> +<body> +<div class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox" data-role="page" + id="TemplateConfigPage"> + <div data-role="content"> + <div class="content-primary"> + <form id="TemplateConfigForm"> + <div class="selectContainer"> + <label class="selectLabel" for="Options">Several Options</label> + <select class="emby-select-withcolor emby-select" id="Options" is="emby-select" name="Options"> + <option id="optOneOption" value="OneOption">One Option</option> + <option id="optAnotherOption" value="AnotherOption">Another Option</option> + </select> + </div> + <div class="inputContainer"> + <label class="inputLabel inputLabelUnfocused" for="AnInteger">An Integer</label> + <input id="AnInteger" is="emby-input" min="0" name="AnInteger" type="number"/> + <div class="fieldDescription">A Description</div> + </div> + <div class="checkboxContainer checkboxContainer-withDescription"> + <label class="emby-checkbox-label"> + <input id="TrueFalseSetting" is="emby-checkbox" name="TrueFalseCheckBox" type="checkbox"/> + <span>A Checkbox</span> + </label> + </div> + <div class="inputContainer"> + <label class="inputLabel inputLabelUnfocused" for="AString">A String</label> + <input id="AString" is="emby-input" name="AString" type="text"/> + <div class="fieldDescription">Another Description</div> + </div> + <div> + <button class="raised button-submit block emby-button" is="emby-button" type="submit"> + <span>Save</span> + </button> + </div> + </form> + </div> + </div> + <script type="text/javascript"> + var TemplateConfig = { + pluginUniqueId: 'eb5d7894-8eef-4b36-aa6f-5d124e828ce1' + }; + + document.querySelector('#TemplateConfigPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) { + document.querySelector('#Options').value = config.Options; + document.querySelector('#AnInteger').value = config.AnInteger; + document.querySelector('#TrueFalseSetting').checked = config.TrueFalseSetting; + document.querySelector('#AString').value = config.AString; + Dashboard.hideLoadingMsg(); + }); + }); + + document.querySelector('#TemplateConfigForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) { + config.Options = document.querySelector('#Options').value; + config.AnInteger = document.querySelector('#AnInteger').value; + config.TrueFalseSetting = document.querySelector('#TrueFalseSetting').checked; + config.AString = document.querySelector('#AString').value; + ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) { + Dashboard.processPluginConfigurationUpdateResult(result); + }); + }); + + e.preventDefault(); + return false; + }); + </script> +</div> +</body> +</html> diff --git a/ImageProvider.cs b/ImageProvider.cs new file mode 100644 index 0000000..df349eb --- /dev/null +++ b/ImageProvider.cs @@ -0,0 +1,101 @@ +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 MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Model.Dto; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.JCoverXtremePro; + +public class ImageProvider + : IRemoteImageProvider, IHasOrder +{ + private ILogger _logger; + + + public ImageProvider(ILogger<ImageProvider> logger) + { + _logger = logger; + } + + public bool Supports(BaseItem item) + { + return item is Movie; + } + + public string Name => "Mediux"; + + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) + { + return new List<ImageType> + { + ImageType.Primary, + // 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 = + 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)); + List<RemoteImageInfo> images = new(); + foreach (var set in deserMovieData.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") + { + _logger.LogInformation("Skipping non poster file"); + continue; + } + + if (file.title.Contains(deserMovieData.movie.title)) + { + _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"); + } + } + } + + _logger.LogInformation("Collected images {0}", images); + return images; + } + + public int Order => 0; + + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return MediuxDownloader.instance.DownloadFile(url); + } +}
\ No newline at end of file diff --git a/Jellyfin.Plugin.JCoverXtremePro.csproj b/Jellyfin.Plugin.JCoverXtremePro.csproj new file mode 100644 index 0000000..8455ca0 --- /dev/null +++ b/Jellyfin.Plugin.JCoverXtremePro.csproj @@ -0,0 +1,29 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <RootNamespace>Jellyfin.Plugin.JCoverXtremePro</RootNamespace> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>false</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>./jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Jellyfin.Controller" Version="10.8.13"/> + <PackageReference Include="Jellyfin.Model" Version="10.8.13"/> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All"/> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="All"/> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All"/> + </ItemGroup> + + <ItemGroup> + <None Remove="Configuration\configPage.html"/> + <EmbeddedResource Include="Configuration\configPage.html"/> + </ItemGroup> + +</Project> diff --git a/MediuxDownloader.cs b/MediuxDownloader.cs new file mode 100644 index 0000000..a027773 --- /dev/null +++ b/MediuxDownloader.cs @@ -0,0 +1,85 @@ +using System; +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.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.JCoverXtremePro; + +public class MediuxDownloader +{ + public static MediuxDownloader instance; + + private Regex contentRegex = new(@"<script[^>]*>self\.__next_f\.push(.*?)</script>"); + private readonly HttpClient httpClientFactory; + private string sentinel = "date"; + + public MediuxDownloader(IHttpClientFactory httpClientFactory) + { + this.httpClientFactory = httpClientFactory.CreateClient("MediuxDownloader"); + } + + private List<JsonNode> ExtractJsonNodes(string httpText) + { + List<JsonNode> list = new(); + foreach (Match match in contentRegex.Matches(httpText)) + { + var pushArg = match.Groups[1].Value; + var strippedString = StripPushArg(pushArg); + if (!strippedString.Contains(sentinel)) + { + Plugin.Logger.LogTrace("Ignoring chunk without sentinel {Sentinel}: {Chunk}", sentinel, strippedString); + continue; + } + + list.Add(ParseStrippedJsonChunk(strippedString)); + } + + if (list.Count != 1) + { + Plugin.Logger.LogError("Found too many or too few chunks: {0}", list); + } + + return list; + } + + private JsonNode ParseStrippedJsonChunk(string text) + { + return JsonSerializer.Deserialize<JsonArray>(text.Substring(text.IndexOf(':') + 1))[3]; + } + + private string StripPushArg(string text) + { + var stringStart = text.IndexOf('"'); + var stringEnd = text.LastIndexOf('"'); + if (stringStart == stringEnd || stringStart == -1) + { + return ""; + } + + // TODO: 1 is regular data, 3 is base64 partial data + return JsonSerializer.Deserialize<string>(text.Substring(stringStart, stringEnd + 1 - stringStart)) ?? ""; + } + + private async Task<string> GetString(string url) + { + return await (await httpClientFactory.GetAsync(url).ConfigureAwait(false)) + .Content.ReadAsStringAsync().ConfigureAwait(false); + } + + 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(); + } + + public async Task<HttpResponseMessage> DownloadFile(string url) + { + return await httpClientFactory.GetAsync(url).ConfigureAwait(false); + } +}
\ No newline at end of file @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.JCoverXtremePro; + +public class POJO +{ + public class MovieData + { + public Movie movie { get; set; } + public List<Set> sets { get; set; } + public List<Set> collectionSets { get; set; } + [JsonIgnore] public IEnumerable<Set> allSets => sets.Concat(collectionSets); + } + + public class Set + { + public string id { get; set; } + + public string set_name { get; set; } + + public User user_created { get; set; } + public List<File> files { get; set; } + } + + public class User + { + public string username { get; set; } + } + + public class File + { + public string fileType { get; set; } + public string title { get; set; } + public string id { get; set; } + + [JsonIgnore] public string downloadUrl => "https://api.mediux.pro/assets/" + id; + } + + public class Movie + { + public string id { get; set; } + public string title { get; set; } + public string tagline { get; set; } + public string imdb_id { get; set; } + } +}
\ No newline at end of file diff --git a/Plugin.cs b/Plugin.cs new file mode 100644 index 0000000..a4e1f6b --- /dev/null +++ b/Plugin.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using Jellyfin.Plugin.JellyFed.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.JCoverXtremePro; + +public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages +{ + public Plugin( + IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, + ILibraryManager libraryManager, ILogger<Plugin> logger, + IHttpClientFactory httpClientFactory + ) : base(applicationPaths, xmlSerializer) + { + logger.LogInformation("Loaded plugin with library manager {}", libraryManager); + MediuxDownloader.instance = new MediuxDownloader(httpClientFactory); + Instance = this; + Logger = logger; + } + + public override string Name => "JCoverXtremePro"; + + public override Guid Id => Guid.Parse("f3e43e23-4b28-4b2f-a29d-37267e2ea2e2"); + + public static Plugin? Instance { get; private set; } + + public static ILogger<Plugin> Logger { get; private set; } + + /// <inheritdoc /> + public IEnumerable<PluginPageInfo> GetPages() + { + return new[] + { + new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", + GetType().Namespace) + } + }; + } +}
\ No newline at end of file diff --git a/jellyfin.ruleset b/jellyfin.ruleset new file mode 100644 index 0000000..9f7bf2a --- /dev/null +++ b/jellyfin.ruleset @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="utf-8"?> +<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" + ToolsVersion="14.0"> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers"> + <!-- disable warning SA1009: Closing parenthesis should be followed by a space. --> + <Rule Id="SA1009" Action="None"/> + <!-- disable warning SA1011: Closing square bracket should be followed by a space. --> + <Rule Id="SA1011" Action="None"/> + <!-- disable warning SA1101: Prefix local calls with 'this.' --> + <Rule Id="SA1101" Action="None"/> + <!-- disable warning SA1108: Block statements should not contain embedded comments --> + <Rule Id="SA1108" Action="None"/> + <!-- disable warning SA1118: Parameter must not span multiple lines. --> + <Rule Id="SA1118" Action="None"/> + <!-- disable warning SA1128:: Put constructor initializers on their own line --> + <Rule Id="SA1128" Action="None"/> + <!-- disable warning SA1130: Use lambda syntax --> + <Rule Id="SA1130" Action="None"/> + <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration --> + <Rule Id="SA1200" Action="None"/> + <!-- disable warning SA1202: 'public' members must come before 'private' members --> + <Rule Id="SA1202" Action="None"/> + <!-- disable warning SA1204: Static members must appear before non-static members --> + <Rule Id="SA1204" Action="None"/> + <!-- disable warning SA1309: Fields must not begin with an underscore --> + <Rule Id="SA1309" Action="None"/> + <!-- disable warning SA1413: Use trailing comma in multi-line initializers --> + <Rule Id="SA1413" Action="None"/> + <!-- disable warning SA1512: Single-line comments must not be followed by blank line --> + <Rule Id="SA1512" Action="None"/> + <!-- disable warning SA1515: Single-line comment should be preceded by blank line --> + <Rule Id="SA1515" Action="None"/> + <!-- disable warning SA1600: Elements should be documented --> + <Rule Id="SA1600" Action="None"/> + <!-- disable warning SA1602: Enumeration items should be documented --> + <Rule Id="SA1602" Action="None"/> + <!-- disable warning SA1633: The file header is missing or not located at the top of the file --> + <Rule Id="SA1633" Action="None"/> + </Rules> + + <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design"> + <!-- error on CA1305: Specify IFormatProvider --> + <Rule Id="CA1305" Action="Error"/> + <!-- error on CA1725: Parameter names should match base declaration --> + <Rule Id="CA1725" Action="Error"/> + <!-- error on CA1725: Call async methods when in an async method --> + <Rule Id="CA1727" Action="Error"/> + <!-- error on CA1843: Do not use 'WaitAll' with a single task --> + <Rule Id="CA1843" Action="Error"/> + <!-- error on CA2016: Forward the CancellationToken parameter to methods that take one + or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token --> + <Rule Id="CA2016" Action="Error"/> + <!-- error on CA2254: Template should be a static expression --> + <Rule Id="CA2254" Action="Info"/> + + <!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute --> + <Rule Id="CA1014" Action="Info"/> + <!-- disable warning CA1024: Use properties where appropriate --> + <Rule Id="CA1024" Action="Info"/> + <!-- disable warning CA1031: Do not catch general exception types --> + <Rule Id="CA1031" Action="Info"/> + <!-- disable warning CA1032: Implement standard exception constructors --> + <Rule Id="CA1032" Action="Info"/> + <!-- disable warning CA1040: Avoid empty interfaces --> + <Rule Id="CA1040" Action="Info"/> + <!-- disable warning CA1062: Validate arguments of public methods --> + <Rule Id="CA1062" Action="Info"/> + <!-- TODO: enable when false positives are fixed --> + <!-- disable warning CA1508: Avoid dead conditional code --> + <Rule Id="CA1508" Action="Info"/> + <!-- disable warning CA1716: Identifiers should not match keywords --> + <Rule Id="CA1716" Action="Info"/> + <!-- disable warning CA1720: Identifiers should not contain type names --> + <Rule Id="CA1720" Action="Info"/> + <!-- disable warning CA1724: Type names should not match namespaces --> + <Rule Id="CA1724" Action="Info"/> + <!-- disable warning CA1805: Do not initialize unnecessarily --> + <Rule Id="CA1805" Action="Info"/> + <!-- disable warning CA1812: internal class that is apparently never instantiated. + If so, remove the code from the assembly. + If this class is intended to contain only static members, make it static --> + <Rule Id="CA1812" Action="Info"/> + <!-- disable warning CA1822: Member does not access instance data and can be marked as static --> + <Rule Id="CA1822" Action="Info"/> + <!-- disable warning CA2000: Dispose objects before losing scope --> + <Rule Id="CA2000" Action="Info"/> + <!-- disable warning CA2253: Named placeholders should not be numeric values --> + <Rule Id="CA2253" Action="Info"/> + <!-- disable warning CA5394: Do not use insecure randomness --> + <Rule Id="CA5394" Action="Info"/> + + <!-- disable warning CA1054: Change the type of parameter url from string to System.Uri --> + <Rule Id="CA1054" Action="None"/> + <!-- disable warning CA1055: URI return values should not be strings --> + <Rule Id="CA1055" Action="None"/> + <!-- disable warning CA1056: URI properties should not be strings --> + <Rule Id="CA1056" Action="None"/> + <!-- disable warning CA1303: Do not pass literals as localized parameters --> + <Rule Id="CA1303" Action="None"/> + <!-- disable warning CA1308: Normalize strings to uppercase --> + <Rule Id="CA1308" Action="None"/> + <!-- disable warning CA1848: Use the LoggerMessage delegates --> + <Rule Id="CA1848" Action="None"/> + <!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments --> + <Rule Id="CA2101" Action="None"/> + <!-- disable warning CA2234: Pass System.Uri objects instead of strings --> + <Rule Id="CA2234" Action="None"/> + </Rules> +</RuleSet> |