diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d1e15d..ab92eec 100644
Binary files a/CHANGELOG.md and b/CHANGELOG.md differ
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 0000000..489f5eb
Binary files /dev/null and b/src/FanzaMetadata/icon.png differ
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