From 5e5407f8dece094a6a3955cb1f2faebee3e68a0a Mon Sep 17 00:00:00 2001 From: senyksia Date: Tue, 17 Sep 2024 14:05:24 +1000 Subject: [PATCH 01/10] Implement VNDB API integration --- src/api/apis/VNDBAPI.ts | 195 ++++++++++++++++++++++++++++++++++++++++ src/main.ts | 2 + 2 files changed, 197 insertions(+) create mode 100644 src/api/apis/VNDBAPI.ts diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts new file mode 100644 index 0000000..26a5fbe --- /dev/null +++ b/src/api/apis/VNDBAPI.ts @@ -0,0 +1,195 @@ +import { APIModel } from '../APIModel'; +import { MediaTypeModel } from '../../models/MediaTypeModel'; +import MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import { requestUrl } from 'obsidian'; +import { MediaType } from '../../utils/MediaType'; + +/** + * A partial `POST /vn` response payload; desired fields should be listed in the request body. + */ +interface VNJSONResponse { + more: boolean; + results: [ + { + id: string; + title: string; + titles: [ + { + title: string; + lang: string; + }, + ]; + devstatus: 0 | 1 | 2; // Released | In-development | Cancelled + released: string | 'TBA' | null; + image: { + url: string; + } | null; + rating: number | null; + tags: [ + { + id: string; + name: string; + category: 'cont' | 'ero' | 'tech'; + rating: number; + spoiler: 0 | 1 | 2; // None | Minor | Major + }, + ]; + developers: [ + { + id: string; + name: string; + }, + ]; + }, + ]; +} + +/** + * A partial `POST /release` response payload; desired fields should be listed in the request body. + */ +interface ReleaseJSONResponse { + more: boolean; + results: [ + { + id: string; + producers: [ + { + id: string; + name: string; + developer: boolean; + publisher: boolean; + }, + ]; + }, + ]; +} + +export class VNDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'VNDB API'; + this.apiDescription = 'A free API for visual novels.'; + this.apiUrl = 'https://api.vndb.org/kana'; + this.types = [MediaType.Game]; + } + + postVNQuery = (body: string): Promise => this.postQuery('/vn', body); + postReleaseQuery = (body: string): Promise => this.postQuery('/release', body); + async postQuery(endpoint: string, body: string): Promise { + const fetchData = await requestUrl({ + url: `${this.apiUrl}${endpoint}`, + method: 'POST', + contentType: 'application/json', + body: body, + throw: false, + }); + + if (fetchData.status !== 200) { + switch (fetchData.status) { + case 400: + throw Error(`MDB | Invalid request body or query [${fetchData.text}].`); + case 404: + throw Error(`MDB | Invalid API path or HTTP method.`); + case 429: + throw Error(`MDB | Throttled.`); + case 500: + throw Error(`MDB | VNDB server error.`); + case 502: + throw Error(`MDB | VNDB server is down.`); + default: + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + } + + return fetchData.json; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const vnData = await this.postVNQuery(`{ + "filters": ["search", "=", "${title}"], + "fields": "title, titles{title, lang}, released", + "sort": "searchrank", + "results": 20 + }`); + + const ret: MediaTypeModel[] = []; + for (const vn of vnData.results) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', + dataSource: this.apiName, + id: vn.id, + } as GameModel), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const vnData = await this.postVNQuery(`{ + "filters": ["id", "=", "${id}"], + "fields": "title, titles{title, lang}, devstatus, released, image{url}, rating, tags{name, category, rating, spoiler}, developers{name}" + }`); + + if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); + const vn = vnData.results[0]; + + const releaseData = await this.postReleaseQuery(`{ + "filters": ["and", + ["vn", "=", + ["id", "=", "${id}"] + ], + ["official", "=", 1], + ["patch", "!=", 1] + ], + "fields": "producers.name, producers.publisher, producers.developer", + "results": 100 + }`); + + return new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', + dataSource: this.apiName, + url: `https://vndb.org/${vn.id}`, + id: vn.id, + + developers: vn.developers.map(d => d.name), + publishers: releaseData.results + .flatMap(r => r.producers) + .filter(p => p.publisher) + .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list + .map(p => p.name) + .unique(), + genres: vn.tags + .filter(t => t.category === 'cont' && t.spoiler === 0 && t.rating >= 2) + .sort((t1, t2) => t2.rating - t1.rating) + .map(t => t.name), + onlineRating: vn.rating ?? NaN, + image: vn.image?.url, + + released: vn.devstatus === 0, + releaseDate: this.plugin.dateFormatter.format(vn.released, this.apiDateFormat), + + userData: { + played: false, + personalRating: 0, + }, + } as GameModel); + } +} diff --git a/src/main.ts b/src/main.ts index 678505a..158a25e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ import { SteamAPI } from './api/apis/SteamAPI'; import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; +import { VNDBAPI } from './api/apis/VNDBAPI'; import { PropertyMapper } from './settings/PropertyMapper'; import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal'; import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping'; @@ -58,6 +59,7 @@ export default class MediaDbPlugin extends Plugin { this.apiManager.registerAPI(new BoardGameGeekAPI(this)); this.apiManager.registerAPI(new OpenLibraryAPI(this)); this.apiManager.registerAPI(new MobyGamesAPI(this)); + this.apiManager.registerAPI(new VNDBAPI(this)); // this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data this.mediaTypeManager = new MediaTypeManager(); From 9960a1e3f3a9cfd049900fad7f1601f7abdedd3f Mon Sep 17 00:00:00 2001 From: senyksia Date: Tue, 17 Sep 2024 16:01:30 +1000 Subject: [PATCH 02/10] Add SFW support for VNDB --- src/api/apis/VNDBAPI.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index 26a5fbe..e6285bb 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -113,8 +113,15 @@ export class VNDBAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); + // prettier-ignore const vnData = await this.postVNQuery(`{ - "filters": ["search", "=", "${title}"], + "filters": ["and", ${!this.plugin.settings.sfwFilter ? `` : + `["release", "!=", ["and", + ["official", "=", "1"], + ["has_ero", "=", "1"] + ]],`} + ["search", "=", "${title}"] + ], "fields": "title, titles{title, lang}, released", "sort": "searchrank", "results": 20 From 2c993149c8416fca693e439844d9fbe96b0bfcf6 Mon Sep 17 00:00:00 2001 From: senyksia Date: Tue, 17 Sep 2024 16:27:48 +1000 Subject: [PATCH 03/10] Add VNDB documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 37138fc..01b6074 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Now you select the result you want and the plugin will cast it's magic and creat | [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No | | [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID so there shouldn't be a rate limit. | No | | [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/) | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No | +| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | #### Notes @@ -156,6 +157,9 @@ Now you select the result you want and the plugin will cast it's magic and creat - [Moby Games](https://www.mobygames.com) - you can find this ID in the URL - e.g. for "Bioshock 2" the URL looks like this `https://www.mobygames.com/game/45089/bioshock-2/` so the ID is `45089` +- [VNDB](https://vndb.org/) + - Located in the novel's VNDB URL path + - e.g. The ID for [Katawa Shoujo](https://vndb.org/v945) (`https://vndb.org/v945`) is `v945` ### Problems, unexpected behavior or improvement suggestions? From 57bb7c40944777055ce17dd5497ad81767c568fb Mon Sep 17 00:00:00 2001 From: senyksia Date: Wed, 18 Sep 2024 15:15:52 +1000 Subject: [PATCH 04/10] Relax SFW filtering for VNDB Previously entries with any NSFW releases would be filtered. This has been relaxed to allow entries with either one standalone SFW release, zero standalone NSFW releases, or the `In-game Sexual Content Toggle` tag. The `GameModel.image` field is now rudimentarily expunged of sensitive images when the SFW filter is enabled. This is not ideal, but likely requires `GameModel` changes to address properly. --- src/api/apis/VNDBAPI.ts | 42 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index e6285bb..77bd27b 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -24,6 +24,7 @@ interface VNJSONResponse { released: string | 'TBA' | null; image: { url: string; + sexual: number; } | null; rating: number | null; tags: [ @@ -113,14 +114,27 @@ export class VNDBAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); + /* SFW Filter: has ANY official&&complete&&standalone&&SFW release + OR has NO official&&standalone&&NSFW release + OR has the `In-game Sexual Content Toggle` (g2708) tag */ // prettier-ignore const vnData = await this.postVNQuery(`{ - "filters": ["and", ${!this.plugin.settings.sfwFilter ? `` : - `["release", "!=", ["and", - ["official", "=", "1"], - ["has_ero", "=", "1"] - ]],`} - ["search", "=", "${title}"] + "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : + `, ["or" + , ["release", "=", ["and" + , ["official", "=", "1"] + , ["rtype", "=", "complete"] + , ["patch", "!=", "1"] + , ["has_ero", "!=", "1"] + ]] + , ["release", "!=", ["and" + , ["official", "=", "1"] + , ["patch", "!=", "1"] + , ["has_ero", "=", "1"] + ]] + , ["tag", "=", "g2708"] + ]`} + , ["search", "=", "${title}"] ], "fields": "title, titles{title, lang}, released", "sort": "searchrank", @@ -149,19 +163,18 @@ export class VNDBAPI extends APIModel { const vnData = await this.postVNQuery(`{ "filters": ["id", "=", "${id}"], - "fields": "title, titles{title, lang}, devstatus, released, image{url}, rating, tags{name, category, rating, spoiler}, developers{name}" + "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" }`); if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); const vn = vnData.results[0]; const releaseData = await this.postReleaseQuery(`{ - "filters": ["and", - ["vn", "=", - ["id", "=", "${id}"] - ], - ["official", "=", 1], - ["patch", "!=", 1] + "filters": ["and" + , ["vn", "=" + , ["id", "=", "${id}"] + ] + , ["official", "=", 1] ], "fields": "producers.name, producers.publisher, producers.developer", "results": 100 @@ -188,7 +201,8 @@ export class VNDBAPI extends APIModel { .sort((t1, t2) => t2.rating - t1.rating) .map(t => t.name), onlineRating: vn.rating ?? NaN, - image: vn.image?.url, + // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively + image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, released: vn.devstatus === 0, releaseDate: this.plugin.dateFormatter.format(vn.released, this.apiDateFormat), From 0e51e1b15c3e127ef00f5ac36dd49d7c9d0f8da2 Mon Sep 17 00:00:00 2001 From: senyksia Date: Thu, 20 Feb 2025 02:57:17 +1100 Subject: [PATCH 05/10] Fix unhandled null in VN release date --- README.md | 3 +-- src/api/apis/VNDBAPI.ts | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d622c2..5434c6b 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,6 @@ Now you select the result you want and the plugin will cast it's magic and creat ### Currently supported APIs: - | Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes | @@ -124,7 +123,7 @@ Now you select the result you want and the plugin will cast it's magic and creat | [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID so there shouldn't be a rate limit. | No | | [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/). NOTE: As of September 2024 the API key is no longer free so consider using Giant Bomb or steam instead | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No | | [Giant Bomb](https://www.giantbomb.com) | The Giant Bomb API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.giantbomb.com/login-signup/) | API requests are limited to 200 requests per resource, per hour. In addition, they implement velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | -| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No +| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | | [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | #### Notes diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index 77bd27b..9fd787e 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -168,6 +168,8 @@ export class VNDBAPI extends APIModel { if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); const vn = vnData.results[0]; + const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; + vn.released ??= 'Unknown'; const releaseData = await this.postReleaseQuery(`{ "filters": ["and" @@ -184,7 +186,7 @@ export class VNDBAPI extends APIModel { type: MediaType.Game, title: vn.title, englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', + year: releasedIsDate ? new Date(vn.released).getFullYear().toString() : vn.released, dataSource: this.apiName, url: `https://vndb.org/${vn.id}`, id: vn.id, @@ -205,7 +207,7 @@ export class VNDBAPI extends APIModel { image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, released: vn.devstatus === 0, - releaseDate: this.plugin.dateFormatter.format(vn.released, this.apiDateFormat), + releaseDate: releasedIsDate ? this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) : vn.released, userData: { played: false, From b6b4955d80fefe8e31d4160dbe4bd7de2c43b3aa Mon Sep 17 00:00:00 2001 From: senyksia Date: Wed, 5 Mar 2025 14:41:12 +1100 Subject: [PATCH 06/10] Document and format VNDB query methods --- src/api/apis/VNDBAPI.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index 9fd787e..e55d73b 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -80,8 +80,14 @@ export class VNDBAPI extends APIModel { this.types = [MediaType.Game]; } - postVNQuery = (body: string): Promise => this.postQuery('/vn', body); - postReleaseQuery = (body: string): Promise => this.postQuery('/release', body); + /** + * Make a `POST` request to the VNDB API. + * @param endpoint The API endpoint to query. E.g. "/vn". + * @param body A JSON object defining the query, following the VNDB API structure. + * @returns A JSON object representing the query response. + * @throws Error The request returned an unsuccessful or unexpected HTTP status code. + * @see {@link https://api.vndb.org/kana#api-structure} + */ async postQuery(endpoint: string, body: string): Promise { const fetchData = await requestUrl({ url: `${this.apiUrl}${endpoint}`, @@ -111,6 +117,24 @@ export class VNDBAPI extends APIModel { return fetchData.json; } + /** + * Make a `POST` request to the `/vn` endpoint. + * Queries visual novel entries. + * @see {@link https://api.vndb.org/kana#post-vn} + */ + postVNQuery(body: string): Promise { + return this.postQuery('/vn', body); + } + + /** + * Make a `POST` request to the `/release` endpoint. + * Queries release entries. + * @see {@link https://api.vndb.org/kana#post-release} + */ + postReleaseQuery(body: string): Promise { + return this.postQuery('/release', body); + } + async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); From d7467861b7e0fb95b5a09338f9ac21ac9f6f89b9 Mon Sep 17 00:00:00 2001 From: senyksia Date: Wed, 5 Mar 2025 14:54:33 +1100 Subject: [PATCH 07/10] style(VNDB): Satisfy ESLint ordering and typing --- src/api/apis/VNDBAPI.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index e55d73b..e15446d 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -1,9 +1,9 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; /** * A partial `POST /vn` response payload; desired fields should be listed in the request body. @@ -21,7 +21,7 @@ interface VNJSONResponse { }, ]; devstatus: 0 | 1 | 2; // Released | In-development | Cancelled - released: string | 'TBA' | null; + released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents image: { url: string; sexual: number; @@ -88,7 +88,7 @@ export class VNDBAPI extends APIModel { * @throws Error The request returned an unsuccessful or unexpected HTTP status code. * @see {@link https://api.vndb.org/kana#api-structure} */ - async postQuery(endpoint: string, body: string): Promise { + async postQuery(endpoint: string, body: string): Promise { const fetchData = await requestUrl({ url: `${this.apiUrl}${endpoint}`, method: 'POST', @@ -123,7 +123,7 @@ export class VNDBAPI extends APIModel { * @see {@link https://api.vndb.org/kana#post-vn} */ postVNQuery(body: string): Promise { - return this.postQuery('/vn', body); + return this.postQuery('/vn', body) as Promise; } /** @@ -132,7 +132,7 @@ export class VNDBAPI extends APIModel { * @see {@link https://api.vndb.org/kana#post-release} */ postReleaseQuery(body: string): Promise { - return this.postQuery('/release', body); + return this.postQuery('/release', body) as Promise; } async searchByTitle(title: string): Promise { From 03e5b9df5a83f1bd03fdc5ec9f73e2fb06784c33 Mon Sep 17 00:00:00 2001 From: senyksia Date: Wed, 5 Mar 2025 15:36:40 +1100 Subject: [PATCH 08/10] style(VNDB): Type JSON response fields as enums --- src/api/apis/VNDBAPI.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index e15446d..a70e5c8 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -5,6 +5,24 @@ import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; +enum VNDevStatus { + Finished, + InDevelopment, + Cancelled, +} + +enum TagSpoiler { + None, + Minor, + Major, +} + +enum TagCategory { + Content = 'cont', + Sexual = 'ero', + Technical = 'tech', +} + /** * A partial `POST /vn` response payload; desired fields should be listed in the request body. */ @@ -20,7 +38,7 @@ interface VNJSONResponse { lang: string; }, ]; - devstatus: 0 | 1 | 2; // Released | In-development | Cancelled + devstatus: VNDevStatus; released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents image: { url: string; @@ -31,9 +49,9 @@ interface VNJSONResponse { { id: string; name: string; - category: 'cont' | 'ero' | 'tech'; + category: TagCategory; rating: number; - spoiler: 0 | 1 | 2; // None | Minor | Major + spoiler: TagSpoiler; }, ]; developers: [ @@ -223,14 +241,14 @@ export class VNDBAPI extends APIModel { .map(p => p.name) .unique(), genres: vn.tags - .filter(t => t.category === 'cont' && t.spoiler === 0 && t.rating >= 2) + .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) .sort((t1, t2) => t2.rating - t1.rating) .map(t => t.name), onlineRating: vn.rating ?? NaN, // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, - released: vn.devstatus === 0, + released: vn.devstatus === VNDevStatus.Finished, releaseDate: releasedIsDate ? this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) : vn.released, userData: { From 37c1ac5db79da18524002de7c7a79c29a34274d3 Mon Sep 17 00:00:00 2001 From: senyksia Date: Wed, 5 Mar 2025 15:48:34 +1100 Subject: [PATCH 09/10] fix(VNDB): Remove `GameModel` type cast --- src/api/apis/VNDBAPI.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index a70e5c8..b507f93 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -193,7 +193,7 @@ export class VNDBAPI extends APIModel { year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', dataSource: this.apiName, id: vn.id, - } as GameModel), + }), ); } @@ -249,12 +249,12 @@ export class VNDBAPI extends APIModel { image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, released: vn.devstatus === VNDevStatus.Finished, - releaseDate: releasedIsDate ? this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) : vn.released, + releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, userData: { played: false, personalRating: 0, }, - } as GameModel); + }); } } From 3b2104387f27986fbcb3d04903ab4649453ff2b6 Mon Sep 17 00:00:00 2001 From: senyksia Date: Wed, 5 Mar 2025 16:21:18 +1100 Subject: [PATCH 10/10] style(VNDB): Privatise query methods --- src/api/apis/VNDBAPI.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index b507f93..3867657 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -106,7 +106,7 @@ export class VNDBAPI extends APIModel { * @throws Error The request returned an unsuccessful or unexpected HTTP status code. * @see {@link https://api.vndb.org/kana#api-structure} */ - async postQuery(endpoint: string, body: string): Promise { + private async postQuery(endpoint: string, body: string): Promise { const fetchData = await requestUrl({ url: `${this.apiUrl}${endpoint}`, method: 'POST', @@ -140,7 +140,7 @@ export class VNDBAPI extends APIModel { * Queries visual novel entries. * @see {@link https://api.vndb.org/kana#post-vn} */ - postVNQuery(body: string): Promise { + private postVNQuery(body: string): Promise { return this.postQuery('/vn', body) as Promise; } @@ -149,7 +149,7 @@ export class VNDBAPI extends APIModel { * Queries release entries. * @see {@link https://api.vndb.org/kana#post-release} */ - postReleaseQuery(body: string): Promise { + private postReleaseQuery(body: string): Promise { return this.postQuery('/release', body) as Promise; }