From 41c536a3b5d282bd3bfe77e0da373fd5570e373d Mon Sep 17 00:00:00 2001 From: erri120 Date: Tue, 4 Jan 2022 11:50:37 +0100 Subject: [PATCH] Add Fanza Metadata Provider --- CHANGELOG.md | Bin 3944 -> 4102 bytes README.md | 34 ++- scripts/build.py | 3 +- src/DLSiteMetadata/DLSiteMetadata.csproj | 1 - src/DLSiteMetadata/Scrapper.cs | 5 + src/F95ZoneMetadata/F95ZoneMetadata.csproj | 1 - .../F95ZoneMetadataProvider.cs | 2 +- .../FanzaMetadata.Test.csproj | 33 +++ src/FanzaMetadata.Test/FanzaTests.cs | 54 +++++ src/FanzaMetadata.Test/files/.gitignore | 1 + src/FanzaMetadata/FanzaMetadata.csproj | 32 +++ src/FanzaMetadata/FanzaMetadataPlugin.cs | 43 ++++ src/FanzaMetadata/FanzaMetadataProvider.cs | 227 ++++++++++++++++++ src/FanzaMetadata/Scrapper.cs | 185 ++++++++++++++ src/FanzaMetadata/ScrapperResult.cs | 27 +++ src/FanzaMetadata/Settings.cs | 77 ++++++ src/FanzaMetadata/SettingsView.xaml | 29 +++ src/FanzaMetadata/SettingsView.xaml.cs | 12 + src/FanzaMetadata/extension.yaml | 10 + src/FanzaMetadata/icon.png | Bin 0 -> 6101 bytes src/Playnite.Extensions.sln | 14 ++ src/Playnite.Extensions.sln.DotSettings | 1 + 22 files changed, 785 insertions(+), 6 deletions(-) create mode 100644 src/FanzaMetadata.Test/FanzaMetadata.Test.csproj create mode 100644 src/FanzaMetadata.Test/FanzaTests.cs create mode 100644 src/FanzaMetadata.Test/files/.gitignore create mode 100644 src/FanzaMetadata/FanzaMetadata.csproj create mode 100644 src/FanzaMetadata/FanzaMetadataPlugin.cs create mode 100644 src/FanzaMetadata/FanzaMetadataProvider.cs create mode 100644 src/FanzaMetadata/Scrapper.cs create mode 100644 src/FanzaMetadata/ScrapperResult.cs create mode 100644 src/FanzaMetadata/Settings.cs create mode 100644 src/FanzaMetadata/SettingsView.xaml create mode 100644 src/FanzaMetadata/SettingsView.xaml.cs create mode 100644 src/FanzaMetadata/extension.yaml create mode 100644 src/FanzaMetadata/icon.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d1e15d4de44903557fc77411bb885fb0af99db4..ab92eeceabe8c8862ee89927ea4a05d776730055 100644 GIT binary patch delta 86 zcmaDM*QPK*i_viMSsrmt1qL^UM20+us>y-e+LKrC3h3uB7V(XGR{Q{x=oA6~ delta 10 RcmZoucp*1IYvTz!egGIz1T6pn diff --git a/README.md b/README.md index aa46454..32cd110 100644 --- a/README.md +++ b/README.md @@ -72,14 +72,44 @@ The following settings can be adjusted in _Add-ons..._ -> _Extensions settings_ | Cookie `xf_csrf` | - | Cookie used for authentication. | | Cookie `xf_user` | - | Cookie used for authentication. | | Cookie `xf_tfa_trust` | - | Cookie used for authentication if the user has Two-Factor authentication enabled. If you do not have TFA enabled you don't need to set this value. | -| Which property should the F95zone labels be assigned to in Playnite | Features | You can decide whether the "Categories" from DLsite should go into the "Features", "Genres" or "Tags" field in Playnite | -| Which property should the F95zone tags be assigned to in Playnite | Tags | You can decide whether the "Genres" from DLsite should go into the "Features", "Genres" or "Tags" field in Playnite | +| Which property should the F95zone labels be assigned to in Playnite | Features | You can decide whether the "Categories" from F95zone should go into the "Features", "Genres" or "Tags" field in Playnite | +| Which property should the F95zone tags be assigned to in Playnite | Tags | You can decide whether the "Genres" from F95zone should go into the "Features", "Genres" or "Tags" field in Playnite | | Check for Updates on Startup | false | This Plugin will check for updates on startup if you want. You can also specify the minimum wait time between updates per game. | | Update Interval in Days | 7 | This is the minimum wait time between update checking **per game**. This value must be a positive number. | | Look for Updates of finished Games | false | By default, the Plugin will not look for update of games that are _finished_ (have either the "Completed" or "Abandoned" F95zone label). You can change this behaviour with this option however I advise against doing so because these games will not receive any updates. This is disabled by default to reduce server load. | **Config location**: `3af84c02-7825-4cd6-b0bd-d0800d26ffc5/config.json` +## Fanza + +**Website**: [Fanza](https://www.dmm.co.jp) + +**Supported Fields**: + +- Name +- Developers +- Features/Genres/Tags +- Community Score +- Cover/Background Image +- Icon +- Series +- Release Date + +**Usage**: + +This Metadata Provider expects a link to the game on Fanza (eg: `https://www.dmm.co.jp/dc/doujin/-/detail/=/cid=d_216826/`) as the name of the game. + +**Settings**: + +The following settings can be adjusted in _Add-ons..._ -> _Extensions settings_ -> _Metadata Sources_ -> _Fanza_: + +| Name | Default Value | Description | +|-----------------------------------------------------------------------|---------------|---------------------------------------------------------------------------------------------------------------------| +| Which property should the Fanza genres be assigned to in Playnite | Features | You can decide whether the "ジャンル" from Fanza should go into the "Features", "Genres" or "Tags" field in Playnite | +| Which property should the Fanza game theme be assigned to in Playnite | Tags | You can decide whether the "ゲームジャンル" from Fanza should go into the "Features", "Genres" or "Tags" field in Playnite | + +**Config location**: `efc848be-82e1-4e3d-a151-59e5fab3e39a/config.json` + ## Troubleshooting Always check the log file `playnite.log` in `%appdata%/Playnite` or in the Playnite installation directory first. You can also try deleting your config file for a specific extension if you think that is the problem, the paths to the config files are in the description of the each extension. diff --git a/scripts/build.py b/scripts/build.py index 64d4bcb..3a012b4 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -10,7 +10,8 @@ plugins = [ "F95ZoneMetadata", - "DLSiteMetadata" + "DLSiteMetadata", + "FanzaMetadata" ] diff --git a/src/DLSiteMetadata/DLSiteMetadata.csproj b/src/DLSiteMetadata/DLSiteMetadata.csproj index 86c0d5b..f25d0dd 100644 --- a/src/DLSiteMetadata/DLSiteMetadata.csproj +++ b/src/DLSiteMetadata/DLSiteMetadata.csproj @@ -12,7 +12,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/DLSiteMetadata/Scrapper.cs b/src/DLSiteMetadata/Scrapper.cs index e2c83c9..dd44fb7 100644 --- a/src/DLSiteMetadata/Scrapper.cs +++ b/src/DLSiteMetadata/Scrapper.cs @@ -35,6 +35,11 @@ public Scrapper(ILogger logger, HttpMessageHandler messageHandler) public async Task ScrapGamePage(string url, CancellationToken cancellationToken = default, string language = DefaultLanguage) { + if (!url.Contains("/?locale=")) + { + url += url.EndsWith("/") ? $"?locale={language}" : $"/?locale={language}"; + } + var context = BrowsingContext.New(_configuration); var document = await context.OpenAsync(url, cancellationToken); diff --git a/src/F95ZoneMetadata/F95ZoneMetadata.csproj b/src/F95ZoneMetadata/F95ZoneMetadata.csproj index dd0edd5..114e59e 100644 --- a/src/F95ZoneMetadata/F95ZoneMetadata.csproj +++ b/src/F95ZoneMetadata/F95ZoneMetadata.csproj @@ -12,7 +12,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/F95ZoneMetadata/F95ZoneMetadataProvider.cs b/src/F95ZoneMetadata/F95ZoneMetadataProvider.cs index 2df9e2c..b66243b 100644 --- a/src/F95ZoneMetadata/F95ZoneMetadataProvider.cs +++ b/src/F95ZoneMetadata/F95ZoneMetadataProvider.cs @@ -83,7 +83,7 @@ public static Scrapper SetupScrapper(Settings settings) if (cookieContainer is not null) { clientHandler.UseCookies = true; - clientHandler.CookieContainer = settings.CreateCookieContainer(); + clientHandler.CookieContainer = cookieContainer; } var scrapper = new Scrapper(CustomLogger.GetLogger(nameof(Scrapper)), clientHandler); diff --git a/src/FanzaMetadata.Test/FanzaMetadata.Test.csproj b/src/FanzaMetadata.Test/FanzaMetadata.Test.csproj new file mode 100644 index 0000000..2d44e1c --- /dev/null +++ b/src/FanzaMetadata.Test/FanzaMetadata.Test.csproj @@ -0,0 +1,33 @@ + + + net48 + False + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/FanzaMetadata.Test/FanzaTests.cs b/src/FanzaMetadata.Test/FanzaTests.cs new file mode 100644 index 0000000..df37d50 --- /dev/null +++ b/src/FanzaMetadata.Test/FanzaTests.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Moq; +using Moq.Contrib.HttpClient; +using Playnite.SDK.Models; +using TestUtils; +using Xunit; +using Xunit.Abstractions; + +namespace FanzaMetadata.Test; + +public class FanzaTests +{ + private readonly ITestOutputHelper _testOutputHelper; + + public FanzaTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Theory] + [InlineData("216826")] + public async Task TestScrapGamePage(string id) + { + var file = Path.Combine("files", $"{id}.html"); + Assert.True(File.Exists(file)); + + var handler = new Mock(); + handler + .SetupAnyRequest() + .ReturnsResponse(File.ReadAllBytes(file)); + + var scrapper = new Scrapper(new XunitLogger(_testOutputHelper), handler.Object); + var res = await scrapper.ScrapGamePage(id); + + Assert.NotNull(res.Link); + Assert.NotNull(res.Title); + Assert.NotNull(res.Circle); + Assert.NotNull(res.Genres); + Assert.NotEmpty(res.Genres!); + Assert.NotNull(res.GameGenre); + Assert.NotNull(res.PreviewImages); + Assert.NotEmpty(res.PreviewImages!); + } + + [Theory] + [InlineData("216826", "https://www.dmm.co.jp/dc/doujin/-/detail/=/cid=d_216826/")] + [InlineData("200809", "https://www.dmm.co.jp/dc/doujin/-/detail/=/cid=d_200809/?dmmref=ListRanking&i3_ref=list&i3_ord=5")] + public void TestGetIdFromGame(string id, string name) + { + Assert.Equal(id, FanzaMetadataProvider.GetIdFromGame(new Game(name))); + } +} diff --git a/src/FanzaMetadata.Test/files/.gitignore b/src/FanzaMetadata.Test/files/.gitignore new file mode 100644 index 0000000..6c3f2eb --- /dev/null +++ b/src/FanzaMetadata.Test/files/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/src/FanzaMetadata/FanzaMetadata.csproj b/src/FanzaMetadata/FanzaMetadata.csproj new file mode 100644 index 0000000..a8132fb --- /dev/null +++ b/src/FanzaMetadata/FanzaMetadata.csproj @@ -0,0 +1,32 @@ + + + net462 + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + diff --git a/src/FanzaMetadata/FanzaMetadataPlugin.cs b/src/FanzaMetadata/FanzaMetadataPlugin.cs new file mode 100644 index 0000000..825ae67 --- /dev/null +++ b/src/FanzaMetadata/FanzaMetadataPlugin.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Windows.Controls; +using Extensions.Common; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Playnite.SDK; +using Playnite.SDK.Plugins; + +namespace FanzaMetadata; + +[UsedImplicitly] +public class FanzaMetadataPlugin : MetadataPlugin +{ + private readonly IPlayniteAPI _playniteAPI; + private readonly ILogger _logger; + private readonly Settings _settings; + + public override string Name => "Fanza"; + public override Guid Id => Guid.Parse("efc848be-82e1-4e3d-a151-59e5fab3e39a"); + + public override List SupportedFields { get; } = new(); + + public FanzaMetadataPlugin(IPlayniteAPI playniteAPI) : base(playniteAPI) + { + _playniteAPI = playniteAPI; + _logger = CustomLogger.GetLogger(nameof(FanzaMetadataPlugin)); + + _settings = new Settings(this, playniteAPI); + } + + public override OnDemandMetadataProvider GetMetadataProvider(MetadataRequestOptions options) + { + return new FanzaMetadataProvider(_playniteAPI, _settings, options); + } + + public override ISettings GetSettings(bool firstRunSettings) => _settings; + + public override UserControl GetSettingsView(bool firstRunView) + { + return new SettingsView(); + } +} diff --git a/src/FanzaMetadata/FanzaMetadataProvider.cs b/src/FanzaMetadata/FanzaMetadataProvider.cs new file mode 100644 index 0000000..329ff3c --- /dev/null +++ b/src/FanzaMetadata/FanzaMetadataProvider.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using Extensions.Common; +using Microsoft.Extensions.Logging; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; + +namespace FanzaMetadata; + +public class FanzaMetadataProvider : OnDemandMetadataProvider +{ + private readonly IPlayniteAPI _playniteAPI; + private readonly Settings _settings; + private readonly ILogger _logger; + + private readonly MetadataRequestOptions _options; + private Game Game => _options.GameData; + private bool IsBackgroundDownload => _options.IsBackgroundDownload; + + // useless + public override List AvailableFields { get; } = new(); + + public FanzaMetadataProvider(IPlayniteAPI playniteAPI, Settings settings, MetadataRequestOptions options) + { + _playniteAPI = playniteAPI; + _settings = settings; + _options = options; + + _logger = CustomLogger.GetLogger(nameof(FanzaMetadataProvider)); + } + + private ScrapperResult? _result; + private bool _didRun; + + private static string? GetIdFromLink(string link) + { + if (!link.StartsWith(Scrapper.GameBaseUrl, StringComparison.OrdinalIgnoreCase)) return null; + + var id = link.Substring(Scrapper.GameBaseUrl.Length); + var index = id.IndexOf('/'); + return index == -1 ? id : id.Substring(0, index); + } + + public static string? GetIdFromGame(Game game) + { + if (game.Name is not null) + { + var id = GetIdFromLink(game.Name); + if (id is not null) return id; + } + + var link = game.Links?.FirstOrDefault(link => link.Name.Equals("Fanza", StringComparison.OrdinalIgnoreCase)); + if (link is not null && !string.IsNullOrWhiteSpace(link.Url)) + { + return GetIdFromLink(link.Url); + } + + return null; + } + + public static Scrapper SetupScrapper() + { + var clientHandler = new HttpClientHandler(); + clientHandler.Properties.Add("User-Agent", "Playnite.Extensions"); + + var cookieContainer = new CookieContainer(); + cookieContainer.Add(new Cookie("age_check_done", "1", "/", ".dmm.co.jp") + { + Expires = DateTime.Now + TimeSpan.FromDays(30), + HttpOnly = true + }); + + clientHandler.UseCookies = true; + clientHandler.CookieContainer = cookieContainer; + + var scrapper = new Scrapper(CustomLogger.GetLogger(nameof(Scrapper)), clientHandler); + return scrapper; + } + + private ScrapperResult? GetResult(GetMetadataFieldArgs args) + { + if (_didRun) return _result; + + var scrapper = SetupScrapper(); + + var id = GetIdFromGame(Game); + if (id is null) + { + throw new NotImplementedException(); + } + + var task = scrapper.ScrapGamePage(id, args.CancelToken); + task.Wait(args.CancelToken); + _result = task.Result; + _didRun = true; + + return _result; + } + + public override string GetName(GetMetadataFieldArgs args) + { + return GetResult(args)?.Title ?? base.GetName(args); + } + + public override MetadataFile GetIcon(GetMetadataFieldArgs args) + { + var iconUrl = GetResult(args)?.IconUrl; + return iconUrl is null ? base.GetIcon(args) : new MetadataFile(iconUrl); + } + + public override IEnumerable GetDevelopers(GetMetadataFieldArgs args) + { + var circle = GetResult(args)?.Circle; + if (circle is null) return base.GetDevelopers(args); + + var company = _playniteAPI.Database.Companies.Where(x => x.Name is not null).FirstOrDefault(x => x.Name.Equals(circle, StringComparison.OrdinalIgnoreCase)); + if (company is not null) return new[] { new MetadataIdProperty(company.Id) }; + return new[] { new MetadataNameProperty(circle) }; + } + + public override IEnumerable GetLinks(GetMetadataFieldArgs args) + { + var link = GetResult(args)?.Link; + if (link is null) yield break; + yield return new Link("Fanza", link); + } + + public override IEnumerable GetSeries(GetMetadataFieldArgs args) + { + var series = GetResult(args)?.Series; + if (series is null) return base.GetSeries(args); + + var item = _playniteAPI.Database.Series.Where(x => x.Name is not null).FirstOrDefault(x => x.Name.Equals(series, StringComparison.OrdinalIgnoreCase)); + if (item is not null) return new[] { new MetadataIdProperty(item.Id) }; + return new[] { new MetadataNameProperty(series) }; + } + + private MetadataFile? SelectImage(GetMetadataFieldArgs args, string caption) + { + var images = GetResult(args)?.PreviewImages; + if (images is null || !images.Any()) return null; + + if (IsBackgroundDownload) + { + return new MetadataFile(images.First()); + } + + var imageFileOption = _playniteAPI.Dialogs.ChooseImageFile(images.Select(image => new ImageFileOption(image)).ToList(), caption); + return imageFileOption == null ? null : new MetadataFile(imageFileOption.Path); + } + + public override MetadataFile? GetCoverImage(GetMetadataFieldArgs args) + { + return SelectImage(args, "Select Cover Image"); + } + + public override MetadataFile? GetBackgroundImage(GetMetadataFieldArgs args) + { + return SelectImage(args, "Select Background Image"); + } + + public override int? GetCommunityScore(GetMetadataFieldArgs args) + { + var rating = GetResult(args)?.Rating; + return rating switch + { + null => base.GetCommunityScore(args), + double.NaN => base.GetCommunityScore(args), + _ => (int)(rating.Value / 5 * 100) + }; + } + + public override ReleaseDate? GetReleaseDate(GetMetadataFieldArgs args) + { + var result = GetResult(args); + if (result is null) return base.GetReleaseDate(args); + + var releaseDate = result.ReleaseDate; + return releaseDate.Equals(DateTime.MinValue) ? base.GetReleaseDate(args) : new ReleaseDate(releaseDate); + } + + private IEnumerable? GetProperties(GetMetadataFieldArgs args, PlayniteProperty currentProperty) + { + // Genres + var genreProperties = PlaynitePropertyHelper.ConvertValuesIfPossible( + _playniteAPI, + _settings.GenreProperty, + currentProperty, + () => GetResult(args)?.Genres); + + if (genreProperties is not null) return genreProperties; + + // Game Genre/Theme + var gameGenreProperties = PlaynitePropertyHelper.ConvertValuesIfPossible( + _playniteAPI, + _settings.GameGenreProperty, + currentProperty, + () => + { + var gameGenre = GetResult(args)?.GameGenre; + if (gameGenre is null) return null; + return new[] { gameGenre }; + }); + + // Default + return null; + } + + public override IEnumerable GetTags(GetMetadataFieldArgs args) + { + return GetProperties(args, PlayniteProperty.Tags) ?? base.GetTags(args); + } + + public override IEnumerable GetFeatures(GetMetadataFieldArgs args) + { + return GetProperties(args, PlayniteProperty.Features) ?? base.GetFeatures(args); + } + + public override IEnumerable GetGenres(GetMetadataFieldArgs args) + { + return GetProperties(args, PlayniteProperty.Genres) ?? base.GetGenres(args); + } +} diff --git a/src/FanzaMetadata/Scrapper.cs b/src/FanzaMetadata/Scrapper.cs new file mode 100644 index 0000000..81c54ed --- /dev/null +++ b/src/FanzaMetadata/Scrapper.cs @@ -0,0 +1,185 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AngleSharp; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using Microsoft.Extensions.Logging; + +namespace FanzaMetadata; + +public class Scrapper +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public const string GameBaseUrl = "https://www.dmm.co.jp/dc/doujin/-/detail/=/cid=d_"; + public const string IconUrlFormat = "https://doujin-assets.dmm.co.jp/digital/game/d_{0}/d_{0}pt.jpg"; + + public Scrapper(ILogger logger, HttpMessageHandler messageHandler) + { + _logger = logger; + + _configuration = Configuration.Default + .WithRequesters(messageHandler) + .WithDefaultLoader(); + } + + public async Task ScrapGamePage(string id, CancellationToken cancellationToken = default) + { + var url = GameBaseUrl + id; + + var context = BrowsingContext.New(_configuration); + var document = await context.OpenAsync(url, cancellationToken); + + var result = new ScrapperResult + { + Link = url + }; + + var productTitleElement = document + .GetElementsByClassName("productTitle__txt") + .FirstOrDefault(elem => elem.TagName.Equals(TagNames.H1, StringComparison.OrdinalIgnoreCase)); + + if (productTitleElement is not null) + { + var productTitleText = productTitleElement.Text(); + + var prefixElements = productTitleElement.GetElementsByClassName("productTitle__txt--campaign"); + if (prefixElements.Any()) + { + var prefixElement = prefixElements.Last(); + var prefixElementText = prefixElement.Text(); + var index = productTitleText.IndexOf(prefixElementText, StringComparison.OrdinalIgnoreCase); + + if (index != -1) + { + productTitleText = productTitleText.Substring(index + prefixElementText.Length + 1); + } + } + + result.Title = productTitleText.Trim(); + } + + var circleNameElement = document.GetElementsByClassName("circleName__txt").FirstOrDefault(); + if (circleNameElement is not null) + { + result.Circle = circleNameElement.Text().Trim(); + } + + var productPreviewElement = document.GetElementsByClassName("productPreview").FirstOrDefault(); + if (productPreviewElement is not null) + { + var previewImages = productPreviewElement.GetElementsByClassName("productPreview__item") + .Where(elem => elem.TagName.Equals(TagNames.Li, StringComparison.OrdinalIgnoreCase)) + .Select(elem => elem.GetElementsByClassName("fn-colorbox").FirstOrDefault()) + .Where(elem => elem is not null) + .Where(elem => elem!.TagName.Equals(TagNames.A, StringComparison.OrdinalIgnoreCase)) + .Cast() + .Select(anchor => anchor.Href) + .Where(href => !string.IsNullOrWhiteSpace(href)) + .ToList(); + + result.PreviewImages = previewImages.Any() ? previewImages : null; + } + + // result.PreviewImages = document.GetElementsByClassName("previewList__item") + // .Select(elem => elem.Children.FirstOrDefault(x => x.TagName.Equals(TagNames.Img, StringComparison.OrdinalIgnoreCase))) + // .Where(elem => elem is not null) + // .Cast() + // .Select(img => img.Source) + // .Where(src => !string.IsNullOrWhiteSpace(src)) + // .Select(src => src!) + // .ToList(); + + var userReviewElement = document.GetElementsByClassName("userReview__item").FirstOrDefault(); + if (userReviewElement is not null) + { + var reviewElement = userReviewElement.GetElementsByTagName(TagNames.A) + .Select(elem => elem.Children.FirstOrDefault(x => x.ClassName is not null && x.ClassName.StartsWith("u-common__ico--review"))) + .FirstOrDefault(); + + if (reviewElement is not null) + { + var rating = reviewElement.ClassName! switch + { + "u-common__ico--review50" => 5.0, + "u-common__ico--review45" => 4.5, + "u-common__ico--review40" => 4.0, + "u-common__ico--review35" => 3.5, + "u-common__ico--review30" => 3.0, + "u-common__ico--review25" => 2.5, + "u-common__ico--review20" => 2.0, + "u-common__ico--review15" => 1.5, + "u-common__ico--review10" => 1.0, + "u-common__ico--review05" => 0.5, + "u-common__ico--review00" => 0.0, + _ => double.NaN + }; + + result.Rating = rating; + } + } + + var informationListElements = document.GetElementsByClassName("informationList"); + if (informationListElements.Any()) + { + foreach (var informationListElement in informationListElements) + { + var ttlElement = informationListElement.GetElementsByClassName("informationList__ttl").FirstOrDefault(); + if (ttlElement is null) continue; + + var ttlText = ttlElement.Text().Trim(); + + var txtElement = informationListElement.GetElementsByClassName("informationList__txt").FirstOrDefault(); + var txt = txtElement?.Text().Trim(); + + if (ttlText.Equals("配信開始日", StringComparison.OrdinalIgnoreCase)) + { + // release date + if (txt is null) continue; + + // "2021/12/25 00:00" + var index = txt.IndexOf(' '); + if (index == -1) continue; + + // "2021/12/25" + txt = txt.Substring(0, index); + + if (DateTime.TryParseExact(txt, "yyyy/MM/dd", null, DateTimeStyles.None, out var releaseDate)) + { + result.ReleaseDate = releaseDate; + } + } else if (ttlText.Equals("ゲームジャンル", StringComparison.OrdinalIgnoreCase)) + { + // game genres, not the same as genres (this is more like a theme, eg "RPG") + if (txt is null) continue; + + result.GameGenre = txt; + } else if (ttlText.Equals("シリーズ", StringComparison.OrdinalIgnoreCase)) + { + // series + if (txt is null) continue; + if (txt.Equals("----")) continue; + + result.Series = txt; + } else if (ttlText.Equals("ジャンル", StringComparison.OrdinalIgnoreCase)) + { + // genres, not the same as game genre (this is more like tags) + var genreTagTextElements = informationListElement.GetElementsByClassName("genreTag__txt"); + + result.Genres = genreTagTextElements + .Select(elem => elem.Text().Trim()) + .ToList(); + } + } + } + + result.IconUrl = string.Format(IconUrlFormat, id); + + return result; + } +} diff --git a/src/FanzaMetadata/ScrapperResult.cs b/src/FanzaMetadata/ScrapperResult.cs new file mode 100644 index 0000000..fa5e438 --- /dev/null +++ b/src/FanzaMetadata/ScrapperResult.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace FanzaMetadata; + +public class ScrapperResult +{ + public string? Link { get; set; } + + public string? Title { get; set; } + + public string? Circle { get; set; } + + public List? PreviewImages { get; set; } + + public double Rating { get; set; } = double.NaN; + + public DateTime ReleaseDate { get; set; } = DateTime.MinValue; + + public string? GameGenre { get; set; } + + public string? Series { get; set; } + + public List? Genres { get; set; } + + public string? IconUrl { get; set; } +} diff --git a/src/FanzaMetadata/Settings.cs b/src/FanzaMetadata/Settings.cs new file mode 100644 index 0000000..01412a4 --- /dev/null +++ b/src/FanzaMetadata/Settings.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Extensions.Common; +using Playnite.SDK; +using Playnite.SDK.Plugins; + +namespace FanzaMetadata; + +public class Settings : ISettings +{ + private IPlayniteAPI? _playniteAPI; + private Plugin? _plugin; + + public PlayniteProperty GenreProperty { get; set; } = PlayniteProperty.Genres; + public PlayniteProperty GameGenreProperty { get; set; } = PlayniteProperty.Features; + + public Settings() { } + + public Settings(Plugin plugin, IPlayniteAPI playniteAPI) + { + _plugin = plugin; + _playniteAPI = playniteAPI; + + var savedSettings = plugin.LoadPluginSettings(); + if (savedSettings is not null) + { + } + } + + private Settings? _previousSettings; + + public void BeginEdit() + { + _previousSettings = new Settings + { + GenreProperty = GenreProperty, + GameGenreProperty = GameGenreProperty + }; + } + + public void EndEdit() + { + _previousSettings = null; + _plugin?.SavePluginSettings(this); + } + + public void CancelEdit() + { + if (_previousSettings is null) return; + + GenreProperty = _previousSettings.GenreProperty; + GameGenreProperty = _previousSettings.GameGenreProperty; + } + + public bool VerifySettings(out List errors) + { + errors = new List(); + + if (!Enum.IsDefined(typeof(PlayniteProperty), GenreProperty)) + { + errors.Add($"Unknown value \"{GenreProperty}\""); + } + + if (!Enum.IsDefined(typeof(PlayniteProperty), GameGenreProperty)) + { + errors.Add($"Unknown value \"{GameGenreProperty}\""); + } + + if (GenreProperty == GameGenreProperty) + { + errors.Add($"{nameof(GenreProperty)} == {nameof(GameGenreProperty)}"); + } + + return !errors.Any(); + } +} diff --git a/src/FanzaMetadata/SettingsView.xaml b/src/FanzaMetadata/SettingsView.xaml new file mode 100644 index 0000000..0c7819a --- /dev/null +++ b/src/FanzaMetadata/SettingsView.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/FanzaMetadata/SettingsView.xaml.cs b/src/FanzaMetadata/SettingsView.xaml.cs new file mode 100644 index 0000000..ba667b0 --- /dev/null +++ b/src/FanzaMetadata/SettingsView.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace FanzaMetadata; + +public partial class SettingsView : UserControl +{ + public SettingsView() + { + InitializeComponent(); + } +} + diff --git a/src/FanzaMetadata/extension.yaml b/src/FanzaMetadata/extension.yaml new file mode 100644 index 0000000..346ceaa --- /dev/null +++ b/src/FanzaMetadata/extension.yaml @@ -0,0 +1,10 @@ +Id: FanzaMetadata +Name: Fanza Metadata Provider +Author: erri120 +Version: 2.0.0 +Module: FanzaMetadata.dll +Type: MetadataProvider +Icon: icon.png +Links: + - Name: GitHub + Url: https://github.com/erri120/Playnite.Extensions diff --git a/src/FanzaMetadata/icon.png b/src/FanzaMetadata/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..489f5eb4ced015238471b00ebf633ac1d2010706 GIT binary patch literal 6101 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 a0)CARNR^YxjJ?U}0s1;mwW~F4BmN)Ll5?{F literal 0 HcmV?d00001 diff --git a/src/Playnite.Extensions.sln b/src/Playnite.Extensions.sln index 68dad0e..052aae5 100644 --- a/src/Playnite.Extensions.sln +++ b/src/Playnite.Extensions.sln @@ -28,6 +28,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DLSiteMetadata.Test", "DLSi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtils", "TestUtils\TestUtils.csproj", "{AB5279EA-A45C-44C3-AF60-640E5300279B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanzaMetadata", "FanzaMetadata\FanzaMetadata.csproj", "{AB8A8353-50B9-409B-8EF5-752F515BB1AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanzaMetadata.Test", "FanzaMetadata.Test\FanzaMetadata.Test.csproj", "{FBA046B1-B0D4-46B0-A26E-78D6ECF2B99D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +62,14 @@ Global {AB5279EA-A45C-44C3-AF60-640E5300279B}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB5279EA-A45C-44C3-AF60-640E5300279B}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB5279EA-A45C-44C3-AF60-640E5300279B}.Release|Any CPU.Build.0 = Release|Any CPU + {AB8A8353-50B9-409B-8EF5-752F515BB1AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB8A8353-50B9-409B-8EF5-752F515BB1AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB8A8353-50B9-409B-8EF5-752F515BB1AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB8A8353-50B9-409B-8EF5-752F515BB1AB}.Release|Any CPU.Build.0 = Release|Any CPU + {FBA046B1-B0D4-46B0-A26E-78D6ECF2B99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBA046B1-B0D4-46B0-A26E-78D6ECF2B99D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBA046B1-B0D4-46B0-A26E-78D6ECF2B99D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBA046B1-B0D4-46B0-A26E-78D6ECF2B99D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {AA0C3ED0-7CE2-44FE-A158-110A14B5DDC6} = {43D0787B-36A2-455F-B7C6-FC05395BB1CD} @@ -66,5 +78,7 @@ Global {BB987509-4466-4F43-8205-39FA863A366A} = {43D0787B-36A2-455F-B7C6-FC05395BB1CD} {587E7146-A376-480E-B662-BB0C92F598A3} = {B59EF2DD-90D0-4D0A-BAB5-EF095FB7A481} {AB5279EA-A45C-44C3-AF60-640E5300279B} = {B59EF2DD-90D0-4D0A-BAB5-EF095FB7A481} + {AB8A8353-50B9-409B-8EF5-752F515BB1AB} = {43D0787B-36A2-455F-B7C6-FC05395BB1CD} + {FBA046B1-B0D4-46B0-A26E-78D6ECF2B99D} = {B59EF2DD-90D0-4D0A-BAB5-EF095FB7A481} EndGlobalSection EndGlobal diff --git a/src/Playnite.Extensions.sln.DotSettings b/src/Playnite.Extensions.sln.DotSettings index 50d9f9a..30e7511 100644 --- a/src/Playnite.Extensions.sln.DotSettings +++ b/src/Playnite.Extensions.sln.DotSettings @@ -1,2 +1,3 @@  + True True \ No newline at end of file