From 552e921d53f67a18fb207184323ac4c3cd8ee749 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Thu, 22 Aug 2024 00:22:56 +0200 Subject: [PATCH 01/15] Fixed bug when API key is missing --- src/api/apis/MobyGamesAPI.ts | 5 +++-- src/api/apis/OMDbAPI.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index ad00759..d38a454 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -22,7 +22,8 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by Title`); if (!this.plugin.settings.MobyGamesKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); + console.error(Error(`MDB | API key for ${this.apiName} missing.`)); + return []; } const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`; @@ -65,7 +66,7 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); if (!this.plugin.settings.MobyGamesKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); + console.log(Error(`MDB | API key for ${this.apiName} missing.`)); } const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${this.plugin.settings.MobyGamesKey}`; diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 879a71f..4f8f39f 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -29,7 +29,8 @@ export class OMDbAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by Title`); if (!this.plugin.settings.OMDbKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); + console.error(Error(`MDB | API key for ${this.apiName} missing.`)); + return []; } const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`; @@ -222,4 +223,4 @@ export class OMDbAPI extends APIModel { return; } -} +} \ No newline at end of file From 08ca5e122fc24d6163f8c7a64d685680ab27ffb7 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:46:53 +0200 Subject: [PATCH 02/15] Use new Error construct and reverted and oversight Forgot to revert searchbyid for Mobygames --- src/api/apis/MobyGamesAPI.ts | 4 ++-- src/api/apis/OMDbAPI.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index d38a454..92ad406 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -22,7 +22,7 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by Title`); if (!this.plugin.settings.MobyGamesKey) { - console.error(Error(`MDB | API key for ${this.apiName} missing.`)); + console.error(new Error(`MDB | API key for ${this.apiName} missing.`)); return []; } @@ -66,7 +66,7 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); if (!this.plugin.settings.MobyGamesKey) { - console.log(Error(`MDB | API key for ${this.apiName} missing.`)); + throw Error(`MDB | API key for ${this.apiName} missing.`); } const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${this.plugin.settings.MobyGamesKey}`; diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 4f8f39f..175167a 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -29,7 +29,7 @@ export class OMDbAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by Title`); if (!this.plugin.settings.OMDbKey) { - console.error(Error(`MDB | API key for ${this.apiName} missing.`)); + console.error(new Error(`MDB | API key for ${this.apiName} missing.`)); return []; } From 6234677bde1d327322db0ca856e377067eb18aa4 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:51:34 +0200 Subject: [PATCH 03/15] Added notice when an API key is missing --- src/api/apis/MobyGamesAPI.ts | 3 +++ src/api/apis/OMDbAPI.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index 92ad406..93cd4a2 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -1,4 +1,5 @@ import { APIModel } from '../APIModel'; +import { Notice } from 'obsidian'; import { MediaTypeModel } from '../../models/MediaTypeModel'; import MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; @@ -23,6 +24,7 @@ export class MobyGamesAPI extends APIModel { if (!this.plugin.settings.MobyGamesKey) { console.error(new Error(`MDB | API key for ${this.apiName} missing.`)); + new Notice(`MediaDB | API key for ${this.apiName} missing.`); return []; } @@ -66,6 +68,7 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); if (!this.plugin.settings.MobyGamesKey) { + new Notice(`MediaDB | API key for ${this.apiName} missing.`); throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 175167a..ab0e95d 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -1,4 +1,5 @@ import { APIModel } from '../APIModel'; +import { Notice } from 'obsidian'; import { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; import MediaDbPlugin from '../../main'; @@ -30,6 +31,7 @@ export class OMDbAPI extends APIModel { if (!this.plugin.settings.OMDbKey) { console.error(new Error(`MDB | API key for ${this.apiName} missing.`)); + new Notice(`MediaDB | API key for ${this.apiName} missing.`); return []; } @@ -108,6 +110,7 @@ export class OMDbAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); if (!this.plugin.settings.OMDbKey) { + new Notice(`MediaDB | API key for ${this.apiName} missing.`); throw Error(`MDB | API key for ${this.apiName} missing.`); } From b4fdc14027fa6f428f54a28ac1a1318867484f0f Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:44:13 +0200 Subject: [PATCH 04/15] Added support for remapping fields to foreign languages --- src/utils/Utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index f6b4e9f..52b2c85 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -15,7 +15,7 @@ export function wrapAround(value: number, size: number): number { } export function containsOnlyLettersAndUnderscores(str: string): boolean { - return /^[a-zA-Z_]+$/.test(str); + return /^[\p{Letter}\s_]+$/u.test(str); } export function replaceIllegalFileNameCharactersInString(string: string): string { From 7681c8330a2b48109630713884f1bf5c61a42008 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:53:12 +0200 Subject: [PATCH 05/15] Added support for accented russian letters --- src/utils/Utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 52b2c85..1c1fb36 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -15,7 +15,7 @@ export function wrapAround(value: number, size: number): number { } export function containsOnlyLettersAndUnderscores(str: string): boolean { - return /^[\p{Letter}\s_]+$/u.test(str); + return /^[\p{Letter}\p{M}_]+$/u.test(str); } export function replaceIllegalFileNameCharactersInString(string: string): string { From 2dc37906fd1d5e2d1a13a3e411b987a66ac17245 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:40:39 +0200 Subject: [PATCH 06/15] Added more notices when an API errors out So users know what went wrong without going to the console --- src/api/apis/MobyGamesAPI.ts | 1 + src/api/apis/OMDbAPI.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index 93cd4a2..14e42c6 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -79,6 +79,7 @@ export class MobyGamesAPI extends APIModel { console.debug(fetchData); if (fetchData.status !== 200) { + new Notice(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); } diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index ab0e95d..451961a 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -118,9 +118,11 @@ export class OMDbAPI extends APIModel { const fetchData = await fetch(searchUrl); if (fetchData.status === 401) { + new Notice(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); } if (fetchData.status !== 200) { + new Notice(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); } @@ -128,6 +130,7 @@ export class OMDbAPI extends APIModel { // console.debug(result); if (result.Response === 'False') { + new Notice(`MDB | Received error from ${this.apiName}: ${result.Error}`); throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); } From ee19791dbaefb29da3c5be9d25c1dc2ff20de300 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Sun, 22 Sep 2024 01:26:02 +0200 Subject: [PATCH 07/15] Added support for giant bomb api and modified the readme Added a note about mobygames per #164 --- README.md | 6 +- src/api/apis/GiantBombAPI.ts | 109 +++++++++++++++++++++++++++++++++++ src/main.ts | 2 + src/settings/Settings.ts | 16 ++++- 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/api/apis/GiantBombAPI.ts diff --git a/README.md b/README.md index 37138fc..6118ac5 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,8 @@ Now you select the result you want and the plugin will cast it's magic and creat | [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No | | [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 | +| [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 | #### 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` +- [Giant Bomb](https://www.giantbomb.com) + - you can find this ID in the URL + - e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887` ### Problems, unexpected behavior or improvement suggestions? diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts new file mode 100644 index 0000000..dbac081 --- /dev/null +++ b/src/api/apis/GiantBombAPI.ts @@ -0,0 +1,109 @@ +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'; + +export class GiantBombAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'GiantBombAPI'; + this.apiDescription = 'A free API for games.'; + this.apiUrl = 'https://www.giantbomb.com/api'; + this.types = [MediaType.Game]; + } + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + if (!this.plugin.settings.GiantBombKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/games?api_key=${this.plugin.settings.GiantBombKey}&filter=name:${encodeURIComponent(title)}&format=json`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + // console.debug(fetchData); + + if (fetchData.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (fetchData.status === 429) { + throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); + } + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const ret: MediaTypeModel[] = []; + for (const result of data.results) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: new Date(result.original_release_date).getFullYear().toString(), + dataSource: this.apiName, + id: result.guid, + } as GameModel), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + if (!this.plugin.settings.GiantBombKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/game/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.GiantBombKey}&format=json`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + console.debug(fetchData); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const result = data.results; + + return new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: new Date(result.original_release_date).getFullYear().toString(), + dataSource: this.apiName, + url: result.site_detail_url, + id: result.guid, + developers: result.developers?.map((x: any) => x.name) ?? [], + publishers: result.publishers?.map((x: any) => x.name) ?? [], + genres: result.genres?.map((x: any) => x.name) ?? [], + onlineRating: 0, + image: result.image?.super_url ?? '', + + released: true, + releaseDate: result.original_release_date ?? 'unknown', + + userData: { + played: false, + + personalRating: 0, + }, + } as GameModel); + } +} diff --git a/src/main.ts b/src/main.ts index 678505a..f190df6 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 { GiantBombAPI } from './api/apis/GiantBombAPI'; 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 GiantBombAPI(this)); // this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data this.mediaTypeManager = new MediaTypeManager(); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 2d5e99c..761149e 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -12,6 +12,7 @@ import { fragWithHTML } from '../utils/Utils'; export interface MediaDbPluginSettings { OMDbKey: string; MobyGamesKey: string; + GiantBombKey: string; sfwFilter: boolean; templates: boolean; customDateFormat: string; @@ -78,6 +79,7 @@ export interface MediaDbPluginSettings { const DEFAULT_SETTINGS: MediaDbPluginSettings = { OMDbKey: '', MobyGamesKey: '', + GiantBombKey: '', sfwFilter: true, templates: true, customDateFormat: 'L', @@ -191,7 +193,7 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); - new Setting(containerEl) + new Setting(containerEl) .setName('Moby Games key') .setDesc('API key for "www.mobygames.com".') .addText(cb => { @@ -203,6 +205,18 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName('Giant Bomb Key') + .setDesc('API key for "www.giantbomb.com".') + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.GiantBombKey) + .onChange(data => { + this.plugin.settings.GiantBombKey = data; + void this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('SFW filter') .setDesc('Only shows SFW results for APIs that offer filtering.') From 4497991344a8f1af0854a42d037960be4c453f61 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 14 Jan 2025 22:30:28 +0100 Subject: [PATCH 08/15] big changes; we have a good TS config now --- .eslintignore | 2 - .eslintrc | 20 ---- .github/ISSUE_TEMPLATE/api-request.md | 4 +- .github/ISSUE_TEMPLATE/bug_report.md | 14 +-- CHANGELOG.md | 70 +++++------ README.md | 106 ++++++++--------- automation/build/esbuild.config.ts | 4 +- automation/build/esbuild.dev.config.ts | 4 +- bun.lockb | Bin 167843 -> 150642 bytes eslint.config.mjs | 54 +++++++++ package.json | 39 +++--- src/api/APIManager.ts | 27 +++-- src/api/APIModel.ts | 16 +-- src/api/apis/BoardGameGeekAPI.ts | 36 +++--- src/api/apis/GiantBombAPI.ts | 12 +- src/api/apis/MALAPI.ts | 20 ++-- src/api/apis/MALAPIManga.ts | 10 +- src/api/apis/MobyGamesAPI.ts | 16 +-- src/api/apis/MusicBrainzAPI.ts | 12 +- src/api/apis/OMDbAPI.ts | 34 +++--- src/api/apis/OpenLibraryAPI.ts | 10 +- src/api/apis/SteamAPI.ts | 16 +-- src/api/apis/WikipediaAPI.ts | 12 +- src/main.ts | 112 +++++++++--------- src/modals/MediaDbAdvancedSearchModal.ts | 43 +++---- src/modals/MediaDbFolderImportModal.ts | 9 +- src/modals/MediaDbIdSearchModal.ts | 26 ++-- src/modals/MediaDbPreviewModal.ts | 29 ++--- src/modals/MediaDbSearchModal.ts | 51 ++++---- src/modals/MediaDbSearchResultModal.ts | 29 ++--- src/modals/SelectModal.ts | 13 +- src/modals/SelectModalElement.ts | 4 +- src/models/BoardGameModel.ts | 30 ++--- src/models/BookModel.ts | 30 ++--- src/models/GameModel.ts | 28 +++-- src/models/MangaModel.ts | 47 ++++---- src/models/MediaTypeModel.ts | 18 +-- src/models/MovieModel.ts | 38 +++--- src/models/MusicReleaseModel.ts | 29 ++--- src/models/SeriesModel.ts | 51 ++++---- src/models/WikiModel.ts | 25 ++-- src/settings/Icon.svelte | 11 +- src/settings/PropertyMapper.ts | 6 +- src/settings/PropertyMapping.ts | 2 +- .../PropertyMappingModelComponent.svelte | 24 ++-- .../PropertyMappingModelsComponent.svelte | 8 +- src/settings/Settings.ts | 28 +++-- src/settings/suggesters/FileSuggest.ts | 3 +- src/settings/suggesters/FolderSuggest.ts | 3 +- src/settings/suggesters/Suggest.ts | 30 +++-- src/utils/MediaTypeManager.ts | 37 +++--- src/utils/ModalHelper.ts | 95 ++++++++------- src/utils/Utils.ts | 12 +- tsconfig.json | 12 +- 54 files changed, 750 insertions(+), 671 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 eslint.config.mjs diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 32909b2..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -npm node_modules -build \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index cf46c54..0000000 --- a/.eslintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "env": { "node": true }, - "plugins": ["@typescript-eslint", "only-warn"], - "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "@typescript-eslint/ban-ts-comment": "off", - "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/explicit-function-return-type": ["warn"] - } -} diff --git a/.github/ISSUE_TEMPLATE/api-request.md b/.github/ISSUE_TEMPLATE/api-request.md index e442a26..9a01d7f 100644 --- a/.github/ISSUE_TEMPLATE/api-request.md +++ b/.github/ISSUE_TEMPLATE/api-request.md @@ -15,5 +15,5 @@ A link to their API documentation **What does the API do/offer** A short description of what data the API offers -- [ ] Is the API free to use -- [ ] Does the API require authentication +- [ ] Is the API free to use +- [ ] Does the API require authentication diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5391c55..af88f16 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,8 +6,8 @@ labels: bug assignees: '' --- -- [ ] The Plugin is up to date -- [ ] Obsidian is up to date +- [ ] The Plugin is up to date +- [ ] Obsidian is up to date **Describe the bug** A clear and concise description of what the bug is. @@ -28,11 +28,11 @@ If applicable, add screenshots to help explain your problem. **Occurs on** -- [ ] Windows -- [ ] macOS -- [ ] Linux -- [ ] Android -- [ ] iOS +- [ ] Windows +- [ ] macOS +- [ ] Linux +- [ ] Android +- [ ] iOS **Plugin version** x.x.x diff --git a/CHANGELOG.md b/CHANGELOG.md index 62061c8..198a34b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,65 +2,65 @@ # 0.7.2 -- Improvements to UI text to match the Obsidian plugin guidelines [#153](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/153) (thanks kepano) +- Improvements to UI text to match the Obsidian plugin guidelines [#153](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/153) (thanks kepano) # 0.7.1 -- Fixed mobygames result without an image crashing the search [#148](https://github.com/mProjectsCode/obsidian-media-db-plugin/issues/148) [#149](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/149) (thanks ltctceplrm) -- Use Steam Community SearchApps for Steam search by title for fuzzy results [#146](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/146) (thanks ZackBoe) -- Don't search APIs that don't have an API key set [#147](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/147) (thanks ZackBoe) -- Use https for all API requests [#147](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/147) (thanks ZackBoe) -- Sped up multi API search -- Fixed unrelated APIs being searched when searching by a specific media type +- Fixed mobygames result without an image crashing the search [#148](https://github.com/mProjectsCode/obsidian-media-db-plugin/issues/148) [#149](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/149) (thanks ltctceplrm) +- Use Steam Community SearchApps for Steam search by title for fuzzy results [#146](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/146) (thanks ZackBoe) +- Don't search APIs that don't have an API key set [#147](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/147) (thanks ZackBoe) +- Use https for all API requests [#147](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/147) (thanks ZackBoe) +- Sped up multi API search +- Fixed unrelated APIs being searched when searching by a specific media type # 0.7.0 -- renamed the plugin to just `Media DB` -- Add plot field when fetching data from OMDb [#106](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/106) (thanks onesvat) -- Added support for Moby Games API [#131](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/131) (thanks ltctceplrm) -- Add index operator for arrays when templating [#129](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/129) (thanks kelszo) -- Support disabling default front matter and add support for Templater [#119](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/119) (thanks kelszo) -- Add option to open new note in a new tab [#128](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/128) (thanks kelszo) -- Added developers and publishers field to games [#122](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/122) (thanks ltctceplrm) +- renamed the plugin to just `Media DB` +- Add plot field when fetching data from OMDb [#106](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/106) (thanks onesvat) +- Added support for Moby Games API [#131](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/131) (thanks ltctceplrm) +- Add index operator for arrays when templating [#129](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/129) (thanks kelszo) +- Support disabling default front matter and add support for Templater [#119](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/119) (thanks kelszo) +- Add option to open new note in a new tab [#128](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/128) (thanks kelszo) +- Added developers and publishers field to games [#122](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/122) (thanks ltctceplrm) # 0.6.0 -- Added manga support through Jikan -- Added book support through Open Library -- Added album cover support for music releases -- Split up `producer` into `studio`, `director` and `writer` for movies and series -- fixed the preview modal not displaying the frontmatter anymore +- Added manga support through Jikan +- Added book support through Open Library +- Added album cover support for music releases +- Split up `producer` into `studio`, `director` and `writer` for movies and series +- fixed the preview modal not displaying the frontmatter anymore # 0.5.0 -- New simple search modal, select the media type and search all applicable APIs -- More data for Board Games -- Actors and Streaming Platforms for Movies and Series -- Separate new file location for all media types -- Separate command for each media type -- Fix problems with closing of preview modal +- New simple search modal, select the media type and search all applicable APIs +- More data for Board Games +- Actors and Streaming Platforms for Movies and Series +- Separate new file location for all media types +- Separate command for each media type +- Fix problems with closing of preview modal # 0.3.2 -- Added Board Game Geek API (documentation pending) -- More information in the search results -- various fixes +- Added Board Game Geek API (documentation pending) +- More information in the search results +- various fixes # 0.3.1 -- various fixes +- various fixes # 0.3.0 -- Added bulk import. Import a folder of media notes as Media DB entries (thanks to [PaperOrb](https://github.com/PaperOrb) on GitHub for their input and for helping me test this feature) -- Added a custom result select modal that allows you to select multiple results at once -- Fixed a bug where the note creation would fail when the metadata included a field with the values `null` or `undefined` +- Added bulk import. Import a folder of media notes as Media DB entries (thanks to [PaperOrb](https://github.com/PaperOrb) on GitHub for their input and for helping me test this feature) +- Added a custom result select modal that allows you to select multiple results at once +- Fixed a bug where the note creation would fail when the metadata included a field with the values `null` or `undefined` # 0.2.1 -- fixed a small bug with the initial selection of an API in the ID search modal +- fixed a small bug with the initial selection of an API in the ID search modal # 0.2.0 -- Added the option to rename metadata fields through property mappings -- fixed note creation falling, when the folder set in the settings did not exist +- Added the option to rename metadata fields through property mappings +- fixed note creation falling, when the folder set in the settings did not exist diff --git a/README.md b/README.md index 6118ac5..e0b04d8 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ Note that "template tags" are surrounded with two curly braces and that the spac For arrays there are two special ways of displaying them. -- using `{{ LIST:variable_name }}` will result in +- using `{{ LIST:variable_name }}` will result in ``` - element 1 - element 2 - element 3 - ... ``` -- using `{{ ENUM:variable_name }}` will result in +- using `{{ ENUM:variable_name }}` will result in ``` element 1, element 2, element 3, ... ``` @@ -102,64 +102,64 @@ Now you select the result you want and the plugin will cast it's magic and creat ### Currently supported media types -- movies (including specials) -- series (including OVAs) -- games -- music releases -- wiki articles -- books +- movies (including specials) +- series (including OVAs) +- games +- music releases +- wiki articles +- books ### 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 | -| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movie, series and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No | -| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No | -| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No | -| [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/). 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 | +| 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 | +| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movie, series and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No | +| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No | +| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No | +| [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/). 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 | #### Notes -- [Jikan](https://jikan.moe/) - - sometimes the api is very slow, this is normal - - you need to use the title the anime has on [My Anime List](https://myanimelist.net), which is in most cases the japanese title - - e.g. instead of "Demon Slayer" you have to search "Kimetsu no Yaiba" +- [Jikan](https://jikan.moe/) + - sometimes the api is very slow, this is normal + - you need to use the title the anime has on [My Anime List](https://myanimelist.net), which is in most cases the japanese title + - e.g. instead of "Demon Slayer" you have to search "Kimetsu no Yaiba" #### Search by ID -- [Jikan](https://jikan.moe/) - - the ID you need is the ID of the anime on [My Anime List](https://myanimelist.net) - - you can find this ID in the URL - - e.g. for "Beyond the Boundary" the URL looks like this `https://myanimelist.net/anime/18153/Kyoukai_no_Kanata` so the ID is `18153` -- [Jikan Manga](https://jikan.moe/) - - the ID you need is the ID of the manga on [My Anime List](https://myanimelist.net) - - you can find this ID in the URL - - e.g. for "All You Need Is Kill" the URL looks like this `https://myanimelist.net/manga/62887/All_You_Need_Is_Kill` so the ID is `62887` -- [OMDb](https://www.omdbapi.com/) - - the ID you need is the ID of the movie or show on [IMDb](https://www.imdb.com) - - you can find this ID in the URL - - e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528` -- [MusicBrainz](https://musicbrainz.org/) - - the id of a release is not easily accessible, you are better off just searching by title -- [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) - - [here](https://en.wikipedia.org/wiki/Wikipedia:Finding_a_Wikidata_ID) is a guide to finding the Wikipedia ID for an article -- [Steam](https://store.steampowered.com/) - - you can find this ID in the URL - - e.g. for "Factorio" the URL looks like this `https://store.steampowered.com/app/427520/Factorio/` so the ID is `427520` -- [Open Library](https://openlibrary.org) - - The ID you need is the "work" ID and not the "book" ID, it needs to start with `/works/`. You can find this ID in the URL - - e.g. for "Fantastic Mr. Fox" the URL looks like this `https://openlibrary.org/works/OL45804W` so the ID is `/works/OL45804W` - - This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) ` -- [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` -- [Giant Bomb](https://www.giantbomb.com) - - you can find this ID in the URL - - e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887` +- [Jikan](https://jikan.moe/) + - the ID you need is the ID of the anime on [My Anime List](https://myanimelist.net) + - you can find this ID in the URL + - e.g. for "Beyond the Boundary" the URL looks like this `https://myanimelist.net/anime/18153/Kyoukai_no_Kanata` so the ID is `18153` +- [Jikan Manga](https://jikan.moe/) + - the ID you need is the ID of the manga on [My Anime List](https://myanimelist.net) + - you can find this ID in the URL + - e.g. for "All You Need Is Kill" the URL looks like this `https://myanimelist.net/manga/62887/All_You_Need_Is_Kill` so the ID is `62887` +- [OMDb](https://www.omdbapi.com/) + - the ID you need is the ID of the movie or show on [IMDb](https://www.imdb.com) + - you can find this ID in the URL + - e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528` +- [MusicBrainz](https://musicbrainz.org/) + - the id of a release is not easily accessible, you are better off just searching by title +- [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) + - [here](https://en.wikipedia.org/wiki/Wikipedia:Finding_a_Wikidata_ID) is a guide to finding the Wikipedia ID for an article +- [Steam](https://store.steampowered.com/) + - you can find this ID in the URL + - e.g. for "Factorio" the URL looks like this `https://store.steampowered.com/app/427520/Factorio/` so the ID is `427520` +- [Open Library](https://openlibrary.org) + - The ID you need is the "work" ID and not the "book" ID, it needs to start with `/works/`. You can find this ID in the URL + - e.g. for "Fantastic Mr. Fox" the URL looks like this `https://openlibrary.org/works/OL45804W` so the ID is `/works/OL45804W` + - This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) ` +- [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` +- [Giant Bomb](https://www.giantbomb.com) + - you can find this ID in the URL + - e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887` ### Problems, unexpected behavior or improvement suggestions? @@ -175,5 +175,5 @@ Contributions are always welcome. If you have an idea, feel free to open a featu Credits go to: -- https://github.com/anpigon/obsidian-book-search-plugin for some inspiration and the idea to make this plugin -- https://github.com/liamcain/obsidian-periodic-notes for 99% of `Suggest.ts` and `FolderSuggest.ts` +- https://github.com/anpigon/obsidian-book-search-plugin for some inspiration and the idea to make this plugin +- https://github.com/liamcain/obsidian-periodic-notes for 99% of `Suggest.ts` and `FolderSuggest.ts` diff --git a/automation/build/esbuild.config.ts b/automation/build/esbuild.config.ts index 91c5fb9..20d35b8 100644 --- a/automation/build/esbuild.config.ts +++ b/automation/build/esbuild.config.ts @@ -1,7 +1,7 @@ import builtins from 'builtin-modules'; import esbuild from 'esbuild'; import esbuildSvelte from 'esbuild-svelte'; -import sveltePreprocess from 'svelte-preprocess'; +import { sveltePreprocess } from 'svelte-preprocess'; import { getBuildBanner } from 'build/buildBanner'; const banner = getBuildBanner('Release Build', version => version); @@ -41,7 +41,7 @@ const build = await esbuild.build({ }, plugins: [ esbuildSvelte({ - compilerOptions: { css: 'injected', dev: false, sveltePath: 'svelte' }, + compilerOptions: { css: 'injected', dev: false }, preprocess: sveltePreprocess(), filterWarnings: warning => { // we don't want warnings from node modules that we can do nothing about diff --git a/automation/build/esbuild.dev.config.ts b/automation/build/esbuild.dev.config.ts index 6b96549..8d0e515 100644 --- a/automation/build/esbuild.dev.config.ts +++ b/automation/build/esbuild.dev.config.ts @@ -1,7 +1,7 @@ import esbuild from 'esbuild'; import copy from 'esbuild-plugin-copy-watch'; import esbuildSvelte from 'esbuild-svelte'; -import sveltePreprocess from 'svelte-preprocess'; +import { sveltePreprocess } from 'svelte-preprocess'; import manifest from '../../manifest.json' assert { type: 'json' }; import { getBuildBanner } from 'build/buildBanner'; @@ -52,7 +52,7 @@ const context = await esbuild.context({ ], }), esbuildSvelte({ - compilerOptions: { css: 'injected', dev: true, sveltePath: 'svelte' }, + compilerOptions: { css: 'injected', dev: true }, preprocess: sveltePreprocess(), filterWarnings: warning => { // we don't want warnings from node modules that we can do nothing about diff --git a/bun.lockb b/bun.lockb index 233fe38d7df48ec891a9f6fab4c4630222822d91..67f6c255452f11fba272c8c9bde8f8c3eaae4f43 100755 GIT binary patch delta 44008 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 MDk|hFtLTmY2fZ{K%>V!Z delta 54372 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 { + async queryDetailedInfo(item: MediaTypeModel): Promise { return await this.queryDetailedInfoById(item.id, item.dataSource); } @@ -45,22 +49,31 @@ export class APIManager { * @param id * @param apiName */ - async queryDetailedInfoById(id: string, apiName: string): Promise { + async queryDetailedInfoById(id: string, apiName: string): Promise { for (const api of this.apis) { if (api.apiName === apiName) { - return api.getById(id); + try { + return api.getById(id); + } catch (e) { + new Notice(`Error querying ${api.apiName}: ${e}`); + console.warn(e); + + return undefined; + } } } + + return undefined; } - getApiByName(name: string): APIModel { + getApiByName(name: string): APIModel | undefined { for (const api of this.apis) { if (api.apiName === name) { return api; } } - return null; + return undefined; } registerAPI(api: APIModel): void { diff --git a/src/api/APIModel.ts b/src/api/APIModel.ts index 00ce961..d23f2a5 100644 --- a/src/api/APIModel.ts +++ b/src/api/APIModel.ts @@ -1,13 +1,13 @@ -import { MediaTypeModel } from '../models/MediaTypeModel'; -import { MediaType } from '../utils/MediaType'; -import MediaDbPlugin from '../main'; +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaType } from '../utils/MediaType'; export abstract class APIModel { - apiName: string; - apiUrl: string; - apiDescription: string; - types: MediaType[]; - plugin: MediaDbPlugin; + apiName!: string; + apiUrl!: string; + apiDescription!: string; + types!: MediaType[]; + plugin!: MediaDbPlugin; /** * This function should query the api and return a list of matches. The matches should be caped at 20. diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 6d4ba77..96ef76e 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -1,9 +1,9 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; -import { BoardGameModel } from 'src/models/BoardGameModel'; import { requestUrl } from 'obsidian'; +import { BoardGameModel } from 'src/models/BoardGameModel'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; export class BoardGameGeekAPI extends APIModel { plugin: MediaDbPlugin; @@ -38,8 +38,8 @@ export class BoardGameGeekAPI extends APIModel { const ret: MediaTypeModel[] = []; for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { - const id = boardgame.attributes.getNamedItem('objectid')!.value; - const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')!.textContent!; + const id = boardgame.attributes.getNamedItem('objectid')?.value; + const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined; const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; ret.push( @@ -49,7 +49,7 @@ export class BoardGameGeekAPI extends APIModel { title, englishTitle: title, year, - } as BoardGameModel), + }), ); } @@ -72,21 +72,29 @@ export class BoardGameGeekAPI extends APIModel { const response = new window.DOMParser().parseFromString(data, 'text/xml'); // console.debug(response); - const boardgame = response.querySelector('boardgame')!; - const title = boardgame.querySelector('name[primary=true]')!.textContent!; + const boardgame = response.querySelector('boardgame'); + if (!boardgame) { + throw Error(`MDB | Received invalid data from ${this.apiName}.`); + } + + const title = boardgame.querySelector('name[primary=true]')?.textContent; const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; const image = boardgame.querySelector('image')?.textContent ?? undefined; const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); - const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')).map(n => n!.textContent!); + const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')) + .map(n => n.textContent) + .filter(n => n !== null); const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0'); const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; - const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')).map(n => n!.textContent!); + const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')) + .map(n => n.textContent) + .filter(n => n !== null); return new BoardGameModel({ - title: title, - englishTitle: title, + title: title ?? undefined, + englishTitle: title ?? undefined, year: year === '0' ? '' : year, dataSource: this.apiName, url: `https://boardgamegeek.com/boardgame/${id}`, @@ -107,6 +115,6 @@ export class BoardGameGeekAPI extends APIModel { played: false, personalRating: 0, }, - } as BoardGameModel); + }); } } diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index dbac081..60bf4e8 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.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'; export class GiantBombAPI extends APIModel { plugin: MediaDbPlugin; @@ -54,7 +54,7 @@ export class GiantBombAPI extends APIModel { year: new Date(result.original_release_date).getFullYear().toString(), dataSource: this.apiName, id: result.guid, - } as GameModel), + }), ); } @@ -104,6 +104,6 @@ export class GiantBombAPI extends APIModel { personalRating: 0, }, - } as GameModel); + }); } } diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts index 0b3146f..fa0d322 100644 --- a/src/api/apis/MALAPI.ts +++ b/src/api/apis/MALAPI.ts @@ -1,9 +1,9 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; -import MediaDbPlugin from '../../main'; import { SeriesModel } from '../../models/SeriesModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; export class MALAPI extends APIModel { plugin: MediaDbPlugin; @@ -52,7 +52,7 @@ export class MALAPI extends APIModel { year: result.year ?? result.aired?.prop?.from?.year ?? '', dataSource: this.apiName, id: result.mal_id, - } as MovieModel), + }), ); } if (type === 'movie' || type === 'special') { @@ -64,7 +64,7 @@ export class MALAPI extends APIModel { year: result.year ?? result.aired?.prop?.from?.year ?? '', dataSource: this.apiName, id: result.mal_id, - } as MovieModel), + }), ); } else if (type === 'series' || type === 'ova') { ret.push( @@ -75,7 +75,7 @@ export class MALAPI extends APIModel { year: result.year ?? result.aired?.prop?.from?.year ?? '', dataSource: this.apiName, id: result.mal_id, - } as SeriesModel), + }), ); } } @@ -127,7 +127,7 @@ export class MALAPI extends APIModel { lastWatched: '', personalRating: 0, }, - } as MovieModel); + }); } if (type === 'movie' || type === 'special') { @@ -159,7 +159,7 @@ export class MALAPI extends APIModel { lastWatched: '', personalRating: 0, }, - } as MovieModel); + }); } else if (type === 'series' || type === 'ova') { return new SeriesModel({ subType: type, @@ -190,9 +190,9 @@ export class MALAPI extends APIModel { lastWatched: '', personalRating: 0, }, - } as SeriesModel); + }); } - return; + throw new Error(`MDB | Unknown media type for id ${id}`); } } diff --git a/src/api/apis/MALAPIManga.ts b/src/api/apis/MALAPIManga.ts index 96a8f00..770c273 100644 --- a/src/api/apis/MALAPIManga.ts +++ b/src/api/apis/MALAPIManga.ts @@ -1,8 +1,8 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; +import type MediaDbPlugin from '../../main'; import { MangaModel } from '../../models/MangaModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; export class MALAPIManga extends APIModel { plugin: MediaDbPlugin; @@ -73,7 +73,7 @@ export class MALAPIManga extends APIModel { lastWatched: '', personalRating: 0, }, - } as MangaModel), + }), ); } @@ -123,6 +123,6 @@ export class MALAPIManga extends APIModel { lastWatched: '', personalRating: 0, }, - } as MangaModel); + }); } } diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index 14e42c6..4c2922b 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -1,10 +1,10 @@ -import { APIModel } from '../APIModel'; import { Notice } from 'obsidian'; -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'; export class MobyGamesAPI extends APIModel { plugin: MediaDbPlugin; @@ -23,9 +23,7 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by Title`); if (!this.plugin.settings.MobyGamesKey) { - console.error(new Error(`MDB | API key for ${this.apiName} missing.`)); - new Notice(`MediaDB | API key for ${this.apiName} missing.`); - return []; + throw new Error(`MDB | API key for ${this.apiName} missing.`); } const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`; @@ -68,7 +66,6 @@ export class MobyGamesAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); if (!this.plugin.settings.MobyGamesKey) { - new Notice(`MediaDB | API key for ${this.apiName} missing.`); throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -79,7 +76,6 @@ export class MobyGamesAPI extends APIModel { console.debug(fetchData); if (fetchData.status !== 200) { - new Notice(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); } @@ -109,6 +105,6 @@ export class MobyGamesAPI extends APIModel { personalRating: 0, }, - } as GameModel); + }); } } diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index e0fd62b..87349a1 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -1,10 +1,10 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MusicReleaseModel } from '../../models/MusicReleaseModel'; -import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; import { MediaType } from '../../utils/MediaType'; +import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; export class MusicBrainzAPI extends APIModel { plugin: MediaDbPlugin; @@ -55,7 +55,7 @@ export class MusicBrainzAPI extends APIModel { artists: result['artist-credit'].map((a: any) => a.name), subType: result['primary-type'], - } as MusicReleaseModel), + }), ); } @@ -97,6 +97,6 @@ export class MusicBrainzAPI extends APIModel { userData: { personalRating: 0, }, - } as MusicReleaseModel); + }); } } diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 451961a..0dd54cc 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -1,11 +1,11 @@ -import { APIModel } from '../APIModel'; import { Notice } from 'obsidian'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import MediaDbPlugin from '../../main'; -import { SeriesModel } from '../../models/SeriesModel'; +import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MovieModel } from '../../models/MovieModel'; +import { SeriesModel } from '../../models/SeriesModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; export class OMDbAPI extends APIModel { plugin: MediaDbPlugin; @@ -30,9 +30,7 @@ export class OMDbAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by Title`); if (!this.plugin.settings.OMDbKey) { - console.error(new Error(`MDB | API key for ${this.apiName} missing.`)); - new Notice(`MediaDB | API key for ${this.apiName} missing.`); - return []; + throw new Error(`MDB | API key for ${this.apiName} missing.`); } const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`; @@ -76,7 +74,7 @@ export class OMDbAPI extends APIModel { year: result.Year, dataSource: this.apiName, id: result.imdbID, - } as MovieModel), + }), ); } else if (type === 'series') { ret.push( @@ -87,7 +85,7 @@ export class OMDbAPI extends APIModel { year: result.Year, dataSource: this.apiName, id: result.imdbID, - } as SeriesModel), + }), ); } else if (type === 'game') { ret.push( @@ -98,7 +96,7 @@ export class OMDbAPI extends APIModel { year: result.Year, dataSource: this.apiName, id: result.imdbID, - } as GameModel), + }), ); } } @@ -110,7 +108,6 @@ export class OMDbAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); if (!this.plugin.settings.OMDbKey) { - new Notice(`MediaDB | API key for ${this.apiName} missing.`); throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -118,11 +115,9 @@ export class OMDbAPI extends APIModel { const fetchData = await fetch(searchUrl); if (fetchData.status === 401) { - new Notice(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); } if (fetchData.status !== 200) { - new Notice(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); } @@ -130,7 +125,6 @@ export class OMDbAPI extends APIModel { // console.debug(result); if (result.Response === 'False') { - new Notice(`MDB | Received error from ${this.apiName}: ${result.Error}`); throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); } @@ -168,7 +162,7 @@ export class OMDbAPI extends APIModel { lastWatched: '', personalRating: 0, }, - } as MovieModel); + }); } else if (type === 'series') { return new SeriesModel({ type: type, @@ -200,7 +194,7 @@ export class OMDbAPI extends APIModel { lastWatched: '', personalRating: 0, }, - } as SeriesModel); + }); } else if (type === 'game') { return new GameModel({ type: type, @@ -224,9 +218,9 @@ export class OMDbAPI extends APIModel { played: false, personalRating: 0, }, - } as GameModel); + }); } - return; + throw new Error(`MDB | Unknown media type for id ${id}`); } -} \ No newline at end of file +} diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts index 43a8014..4835afe 100644 --- a/src/api/apis/OpenLibraryAPI.ts +++ b/src/api/apis/OpenLibraryAPI.ts @@ -1,8 +1,8 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; import { BookModel } from 'src/models/BookModel'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; export class OpenLibraryAPI extends APIModel { plugin: MediaDbPlugin; @@ -42,7 +42,7 @@ export class OpenLibraryAPI extends APIModel { dataSource: this.apiName, id: result.key, author: result.author_name ?? 'unknown', - } as BookModel), + }), ); } @@ -87,6 +87,6 @@ export class OpenLibraryAPI extends APIModel { lastRead: '', personalRating: 0, }, - } as BookModel); + }); } } diff --git a/src/api/apis/SteamAPI.ts b/src/api/apis/SteamAPI.ts index 7edf017..2d5ef1b 100644 --- a/src/api/apis/SteamAPI.ts +++ b/src/api/apis/SteamAPI.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'; export class SteamAPI extends APIModel { plugin: MediaDbPlugin; @@ -49,7 +49,7 @@ export class SteamAPI extends APIModel { year: '', dataSource: this.apiName, id: result.appid, - } as GameModel), + }), ); } @@ -94,8 +94,8 @@ export class SteamAPI extends APIModel { url: `https://store.steampowered.com/app/${result.steam_appid}`, id: result.steam_appid, - developers: result['developers'], - publishers: result['publishers'], + developers: result.developers, + publishers: result.publishers, genres: result.genres?.map((x: any) => x.description) ?? [], onlineRating: Number.parseFloat(result.metacritic?.score ?? 0), image: result.header_image ?? '', @@ -107,6 +107,6 @@ export class SteamAPI extends APIModel { played: false, personalRating: 0, }, - } as GameModel); + }); } } diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts index 0965ab8..110cd79 100644 --- a/src/api/apis/WikipediaAPI.ts +++ b/src/api/apis/WikipediaAPI.ts @@ -1,8 +1,8 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { WikiModel } from '../../models/WikiModel'; import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; export class WikipediaAPI extends APIModel { plugin: MediaDbPlugin; @@ -42,7 +42,7 @@ export class WikipediaAPI extends APIModel { year: '', dataSource: this.apiName, id: result.pageid, - } as WikiModel), + }), ); } @@ -73,10 +73,10 @@ export class WikipediaAPI extends APIModel { id: result.pageid, wikiUrl: result.fullurl, - lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), + lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat) ?? undefined, length: result.length, userData: {}, - } as WikiModel); + }); } } diff --git a/src/main.ts b/src/main.ts index f190df6..348c464 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,33 +1,28 @@ import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder } from 'obsidian'; -import { getDefaultSettings, MediaDbPluginSettings, MediaDbSettingTab } from './settings/Settings'; +import type { MediaType } from 'src/utils/MediaType'; import { APIManager } from './api/APIManager'; -import { MediaTypeModel } from './models/MediaTypeModel'; -import { - CreateNoteOptions, - dateTimeToString, - markdownTable, - replaceIllegalFileNameCharactersInString, - unCamelCase, - hasTemplaterPlugin, - useTemplaterPluginInFile, -} from './utils/Utils'; -import { OMDbAPI } from './api/apis/OMDbAPI'; +import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; +import { GiantBombAPI } from './api/apis/GiantBombAPI'; import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; -import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; +import { OMDbAPI } from './api/apis/OMDbAPI'; +import type { MediaTypeModel } from './models/MediaTypeModel'; +import type { MediaDbPluginSettings } from './settings/Settings'; +import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; +import type { SearchModalOptions } from './utils/ModalHelper'; +import type { CreateNoteOptions } from './utils/Utils'; +import { dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; +import { WikipediaAPI } from './api/apis/WikipediaAPI'; 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 { GiantBombAPI } from './api/apis/GiantBombAPI'; import { PropertyMapper } from './settings/PropertyMapper'; import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal'; import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping'; -import { ModalHelper, ModalResultCode, SearchModalOptions } from './utils/ModalHelper'; +import { ModalHelper, ModalResultCode } from './utils/ModalHelper'; import { DateFormatter } from './utils/DateFormatter'; -import { MediaType } from 'src/utils/MediaType'; export type Metadata = Record; @@ -38,12 +33,12 @@ export interface MediaTypeModelObj { } export default class MediaDbPlugin extends Plugin { - settings: MediaDbPluginSettings; - apiManager: APIManager; - mediaTypeManager: MediaTypeManager; - modelPropertyMapper: PropertyMapper; - modalHelper: ModalHelper; - dateFormatter: DateFormatter; + settings!: MediaDbPluginSettings; + apiManager!: APIManager; + mediaTypeManager!: MediaTypeManager; + modelPropertyMapper!: PropertyMapper; + modalHelper!: ModalHelper; + dateFormatter!: DateFormatter; frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; @@ -165,7 +160,7 @@ export default class MediaDbPlugin extends Plugin { * - maybe custom link syntax */ async createLinkWithSearchModal(): Promise { - const apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { + const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); }); @@ -173,7 +168,7 @@ export default class MediaDbPlugin extends Plugin { return; } - const selectResults: MediaTypeModel[] = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => { + const selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => { return await this.queryDetails(selectModalData.selected); }); @@ -193,7 +188,7 @@ export default class MediaDbPlugin extends Plugin { async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { let types: string[] = []; - let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { + let apiSearchResults = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { types = searchModalData.types; const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); try { @@ -214,12 +209,13 @@ export default class MediaDbPlugin extends Plugin { apiSearchResults = apiSearchResults.filter(x => types.contains(x.type)); let selectResults: MediaTypeModel[]; - let proceed: boolean; + let proceed: boolean = false; while (!proceed) { - selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - }); + selectResults = + (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { + return await this.queryDetails(selectModalData.selected); + })) ?? []; if (!selectResults) { return; } @@ -229,11 +225,11 @@ export default class MediaDbPlugin extends Plugin { }); } - await this.createMediaDbNotes(selectResults); + await this.createMediaDbNotes(selectResults!); } async createEntryWithAdvancedSearchModal(): Promise { - const apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { + const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); }); @@ -243,12 +239,13 @@ export default class MediaDbPlugin extends Plugin { } let selectResults: MediaTypeModel[]; - let proceed: boolean; + let proceed: boolean = false; while (!proceed) { - selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - }); + selectResults = + (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { + return await this.queryDetails(selectModalData.selected); + })) ?? []; if (!selectResults) { return; } @@ -258,12 +255,12 @@ export default class MediaDbPlugin extends Plugin { }); } - await this.createMediaDbNotes(selectResults); + await this.createMediaDbNotes(selectResults!); } async createEntryWithIdSearchModal(): Promise { - let idSearchResult: MediaTypeModel; - let proceed: boolean; + let idSearchResult: MediaTypeModel | undefined = undefined; + let proceed: boolean = false; while (!proceed) { idSearchResult = await this.modalHelper.openIdSearchModal({}, async idSearchModalData => { @@ -278,6 +275,9 @@ export default class MediaDbPlugin extends Plugin { }); } + if (!idSearchResult) { + return; + } await this.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); } @@ -290,11 +290,9 @@ export default class MediaDbPlugin extends Plugin { async queryDetails(models: MediaTypeModel[]): Promise { const detailModels: MediaTypeModel[] = []; for (const model of models) { - try { - detailModels.push(await this.apiManager.queryDetailedInfo(model)); - } catch (e) { - console.warn(e); - new Notice(e.toString()); + const res = await this.apiManager.queryDetailedInfo(model); + if (res) { + detailModels.push(res); } } return detailModels; @@ -319,7 +317,7 @@ export default class MediaDbPlugin extends Plugin { } } catch (e) { console.warn(e); - new Notice(e.toString()); + new Notice(`${e}`); } } @@ -383,7 +381,7 @@ export default class MediaDbPlugin extends Plugin { // Updating a previous file if (options.attachFile) { - const previousMetadata = this.app.metadataCache.getFileCache(options.attachFile).frontmatter; + const previousMetadata = this.app.metadataCache.getFileCache(options.attachFile)?.frontmatter ?? {}; // Use contents (below front matter) from previous file fileContent = await this.app.vault.read(options.attachFile); @@ -443,7 +441,7 @@ export default class MediaDbPlugin extends Plugin { return { fileMetadata: fileMetadata, fileContent: fileContent }; } - async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string): Promise<{ fileMetadata: Metadata; fileContent: string }> { + async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { if (!template) { return { fileMetadata: fileMetadata, fileContent: fileContent }; } @@ -486,7 +484,7 @@ export default class MediaDbPlugin extends Plugin { } getMetadataFromFileCache(file: TFile): Metadata { - const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file).frontmatter; + const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter; return structuredClone(metadata ?? {}); } @@ -501,6 +499,10 @@ export default class MediaDbPlugin extends Plugin { // find and possibly create the folder set in settings or passed in folder const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/'); + if (!folder || !(folder instanceof TFolder)) { + throw new Error('MDB | invalid folder'); + } + fileName = replaceIllegalFileNameCharactersInString(fileName); const filePath = `${folder.path}/${fileName}.md`; @@ -519,7 +521,7 @@ export default class MediaDbPlugin extends Plugin { const activeLeaf = this.app.workspace.getUnpinnedLeaf(); if (!activeLeaf) { console.warn('MDB | no active leaf, not opening newly created note'); - return; + return targetFile; } await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); } @@ -532,7 +534,7 @@ export default class MediaDbPlugin extends Plugin { * Tries to read the type, id and dataSource of the active note. If successful it will query the api, delete the old note and create a new one. */ async updateActiveNote(onlyMetadata: boolean = false): Promise { - const activeFile: TFile = this.app.workspace.getActiveFile(); + const activeFile = this.app.workspace.getActiveFile() ?? undefined; if (!activeFile) { throw new Error('MDB | there is no active note'); } @@ -560,9 +562,9 @@ export default class MediaDbPlugin extends Plugin { // console.debug(newMediaTypeModel); if (onlyMetadata) { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent, openNote: true }); + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); } else { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent, openNote: true }); + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true }); } } @@ -596,7 +598,7 @@ export default class MediaDbPlugin extends Plugin { try { results = await this.apiManager.query(title, [selectedAPI]); } catch (e) { - erroredFiles.push({ filePath: file.path, error: e.toString() }); + erroredFiles.push({ filePath: file.path, error: `${e}` }); continue; } if (!results || results.length === 0) { @@ -631,7 +633,7 @@ export default class MediaDbPlugin extends Plugin { } const detailedResults = await this.queryDetails(selectModalResult.data.selected); - await this.createMediaDbNotes(detailedResults, appendContent ? file : null); + await this.createMediaDbNotes(detailedResults, appendContent ? file : undefined); selectModal.close(); } @@ -661,7 +663,7 @@ export default class MediaDbPlugin extends Plugin { // migrate the settings loaded from the disk to match the structure of the default settings const newPropertyMappings: PropertyMappingModel[] = []; for (const defaultPropertyMappingModel of defaultSettings.propertyMappingModels) { - const newPropertyMappingModel: PropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type); + const newPropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type); if (newPropertyMappingModel === undefined) { // if the propertyMappingModel exists in the default settings but not the loaded settings, add it newPropertyMappings.push(defaultPropertyMappingModel); diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/src/modals/MediaDbAdvancedSearchModal.ts index 9a79ac1..d6aa564 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/src/modals/MediaDbAdvancedSearchModal.ts @@ -1,7 +1,9 @@ -import { ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import { MediaTypeModel } from '../models/MediaTypeModel'; -import MediaDbPlugin from '../main'; -import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; +import type { ButtonComponent } from 'obsidian'; +import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; +import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; export class MediaDbAdvancedSearchModal extends Modal { plugin: MediaDbPlugin; @@ -9,9 +11,9 @@ export class MediaDbAdvancedSearchModal extends Modal { query: string; isBusy: boolean; title: string; - selectedApis: { name: string; selected: boolean }[]; + selectedApis: string[]; - searchBtn: ButtonComponent; + searchBtn?: ButtonComponent; submitCallback?: (res: AdvancedSearchModalData) => void; closeCallback?: (err?: Error) => void; @@ -22,12 +24,9 @@ export class MediaDbAdvancedSearchModal extends Modal { this.plugin = plugin; this.selectedApis = []; - this.title = advancedSearchModalOptions.modalTitle; - this.query = advancedSearchModalOptions.prefilledSearchString; - - for (const api of this.plugin.apiManager.apis) { - this.selectedApis.push({ name: api.apiName, selected: advancedSearchModalOptions.preselectedAPIs.contains(api.apiName) }); - } + this.title = advancedSearchModalOptions.modalTitle ?? ''; + this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; + this.isBusy = false; } setSubmitCallback(submitCallback: (res: AdvancedSearchModalData) => void): void { @@ -44,13 +43,13 @@ export class MediaDbAdvancedSearchModal extends Modal { } } - async search(): Promise { + async search(): Promise { if (!this.query || this.query.length < 3) { new Notice('MDB | Query too short'); return; } - const apis: string[] = this.selectedApis.filter(x => x.selected).map(x => x.name); + const apis: string[] = this.selectedApis; if (apis.length === 0) { new Notice('MDB | No API selected'); @@ -59,10 +58,10 @@ export class MediaDbAdvancedSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn.setDisabled(false); - this.searchBtn.setButtonText('Searching...'); + this.searchBtn?.setDisabled(false); + this.searchBtn?.setButtonText('Searching...'); - this.submitCallback({ query: this.query, apis: apis }); + this.submitCallback?.({ query: this.query, apis: apis }); } } @@ -97,9 +96,13 @@ export class MediaDbAdvancedSearchModal extends Modal { const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(api.apiName); - apiToggleComponent.setValue(this.selectedApis.find(x => x.name === api.apiName).selected); + apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); apiToggleComponent.onChange(value => { - this.selectedApis.find(x => x.name === api.apiName).selected = value; + if (value) { + this.selectedApis.push(api.apiName); + } else { + this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); + } }); apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); } @@ -124,7 +127,7 @@ export class MediaDbAdvancedSearchModal extends Modal { } onClose(): void { - this.closeCallback(); + this.closeCallback?.(); const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbFolderImportModal.ts index 04c908f..995df1a 100644 --- a/src/modals/MediaDbFolderImportModal.ts +++ b/src/modals/MediaDbFolderImportModal.ts @@ -1,11 +1,12 @@ -import { App, ButtonComponent, DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import MediaDbPlugin from '../main'; +import type { App, ButtonComponent } from 'obsidian'; +import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; export class MediaDbFolderImportModal extends Modal { plugin: MediaDbPlugin; onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void; selectedApi: string; - searchBtn: ButtonComponent; + searchBtn?: ButtonComponent; titleFieldName: string; appendContent: boolean; @@ -14,6 +15,8 @@ export class MediaDbFolderImportModal extends Modal { this.plugin = plugin; this.onSubmit = onSubmit; this.selectedApi = plugin.apiManager.apis[0].apiName; + this.titleFieldName = ''; + this.appendContent = false; } submit(): void { diff --git a/src/modals/MediaDbIdSearchModal.ts b/src/modals/MediaDbIdSearchModal.ts index b100b46..e7ee81c 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/src/modals/MediaDbIdSearchModal.ts @@ -1,7 +1,9 @@ -import { ButtonComponent, DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; -import { MediaTypeModel } from '../models/MediaTypeModel'; -import MediaDbPlugin from '../main'; -import { ID_SEARCH_MODAL_DEFAULT_OPTIONS, IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; +import type { ButtonComponent } from 'obsidian'; +import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; +import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; export class MediaDbIdSearchModal extends Modal { plugin: MediaDbPlugin; @@ -11,7 +13,7 @@ export class MediaDbIdSearchModal extends Modal { title: string; selectedApi: string; - searchBtn: ButtonComponent; + searchBtn?: ButtonComponent; submitCallback?: (res: IdSearchModalData, err?: Error) => void; closeCallback?: (err?: Error) => void; @@ -21,8 +23,10 @@ export class MediaDbIdSearchModal extends Modal { super(plugin.app); this.plugin = plugin; - this.title = idSearchModalOptions.modalTitle; + this.title = idSearchModalOptions.modalTitle ?? ''; this.selectedApi = idSearchModalOptions.preselectedAPI || plugin.apiManager.apis[0].apiName; + this.query = ''; + this.isBusy = false; } setSubmitCallback(submitCallback: (res: IdSearchModalData, err?: Error) => void): void { @@ -39,7 +43,7 @@ export class MediaDbIdSearchModal extends Modal { } } - async search(): Promise { + async search(): Promise { if (!this.query) { new Notice('MDB | no Id entered'); return; @@ -52,10 +56,10 @@ export class MediaDbIdSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn.setDisabled(false); - this.searchBtn.setButtonText('Searching...'); + this.searchBtn?.setDisabled(false); + this.searchBtn?.setButtonText('Searching...'); - this.submitCallback({ query: this.query, api: this.selectedApi }); + this.submitCallback?.({ query: this.query, api: this.selectedApi }); } } @@ -109,7 +113,7 @@ export class MediaDbIdSearchModal extends Modal { } onClose(): void { - this.closeCallback(); + this.closeCallback?.(); const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/MediaDbPreviewModal.ts b/src/modals/MediaDbPreviewModal.ts index ef27376..aa7f707 100644 --- a/src/modals/MediaDbPreviewModal.ts +++ b/src/modals/MediaDbPreviewModal.ts @@ -1,22 +1,19 @@ -import { ButtonComponent, Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; -import MediaDbPlugin from 'src/main'; -import { MediaTypeModel } from 'src/models/MediaTypeModel'; -import { PREVIEW_MODAL_DEFAULT_OPTIONS, PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper'; -import { CreateNoteOptions } from '../utils/Utils'; +import type { ButtonComponent } from 'obsidian'; +import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import type { MediaTypeModel } from 'src/models/MediaTypeModel'; +import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper'; +import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; export class MediaDbPreviewModal extends Modal { plugin: MediaDbPlugin; - createNoteOptions: CreateNoteOptions; elements: MediaTypeModel[]; - isBusy: boolean; title: string; - cancelButton: ButtonComponent; - submitButton: ButtonComponent; markdownComponent: Component; - submitCallback: (previewModalData: PreviewModalData) => void; - closeCallback: (err?: Error) => void; + submitCallback?: (previewModalData: PreviewModalData) => void; + closeCallback?: (err?: Error) => void; constructor(plugin: MediaDbPlugin, previewModalOptions: PreviewModalOptions) { previewModalOptions = Object.assign({}, PREVIEW_MODAL_DEFAULT_OPTIONS, previewModalOptions); @@ -24,8 +21,8 @@ export class MediaDbPreviewModal extends Modal { super(plugin.app); this.plugin = plugin; - this.title = previewModalOptions.modalTitle; - this.elements = previewModalOptions.elements; + this.title = previewModalOptions.modalTitle ?? ''; + this.elements = previewModalOptions.elements ?? []; this.markdownComponent = new Component(); } @@ -70,14 +67,12 @@ export class MediaDbPreviewModal extends Modal { btn.setButtonText('Cancel'); btn.onClick(() => this.close()); btn.buttonEl.addClass('media-db-plugin-button'); - this.cancelButton = btn; }); bottomSettingRow.addButton(btn => { btn.setButtonText('Ok'); btn.setCta(); - btn.onClick(() => this.submitCallback({ confirmed: true })); + btn.onClick(() => this.submitCallback?.({ confirmed: true })); btn.buttonEl.addClass('media-db-plugin-button'); - this.submitButton = btn; }); } @@ -87,6 +82,6 @@ export class MediaDbPreviewModal extends Modal { onClose(): void { this.markdownComponent.unload(); - this.closeCallback(); + this.closeCallback?.(); } } diff --git a/src/modals/MediaDbSearchModal.ts b/src/modals/MediaDbSearchModal.ts index 5e3cb99..f7f6d8a 100644 --- a/src/modals/MediaDbSearchModal.ts +++ b/src/modals/MediaDbSearchModal.ts @@ -1,10 +1,12 @@ -import { ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import { MediaTypeModel } from '../models/MediaTypeModel'; -import MediaDbPlugin from '../main'; -import { SEARCH_MODAL_DEFAULT_OPTIONS, SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; +import type { ButtonComponent } from 'obsidian'; +import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaType } from '../utils/MediaType'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; +import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; +import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; import { unCamelCase } from '../utils/Utils'; -import { MediaType } from '../utils/MediaType'; export class MediaDbSearchModal extends Modal { plugin: MediaDbPlugin; @@ -12,9 +14,9 @@ export class MediaDbSearchModal extends Modal { query: string; isBusy: boolean; title: string; - selectedTypes: { name: MediaType; selected: boolean }[]; + selectedTypes: MediaType[]; - searchBtn: ButtonComponent; + searchBtn?: ButtonComponent; submitCallback?: (res: SearchModalData) => void; closeCallback?: (err?: Error) => void; @@ -24,13 +26,10 @@ export class MediaDbSearchModal extends Modal { super(plugin.app); this.plugin = plugin; - this.selectedTypes = []; - this.title = searchModalOptions.modalTitle; - this.query = searchModalOptions.prefilledSearchString; - - for (const mediaType of MEDIA_TYPES) { - this.selectedTypes.push({ name: mediaType, selected: searchModalOptions.preselectedTypes.contains(mediaType) }); - } + this.selectedTypes = [...(searchModalOptions.preselectedTypes ?? [])]; + this.title = searchModalOptions.modalTitle ?? ''; + this.query = searchModalOptions.prefilledSearchString ?? ''; + this.isBusy = false; } setSubmitCallback(submitCallback: (res: SearchModalData) => void): void { @@ -47,13 +46,13 @@ export class MediaDbSearchModal extends Modal { } } - async search(): Promise { + async search(): Promise { if (!this.query || this.query.length < 3) { new Notice('MDB | Query too short'); return; } - const types: MediaType[] = this.selectedTypes.filter(x => x.selected).map(x => x.name); + const types: MediaType[] = this.selectedTypes; if (types.length === 0) { new Notice('MDB | No Type selected'); @@ -62,10 +61,10 @@ export class MediaDbSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn.setDisabled(false); - this.searchBtn.setButtonText('Searching...'); + this.searchBtn?.setDisabled(false); + this.searchBtn?.setButtonText('Searching...'); - this.submitCallback({ query: this.query, types: types }); + this.submitCallback?.({ query: this.query, types: types }); } } @@ -76,7 +75,7 @@ export class MediaDbSearchModal extends Modal { const placeholder = 'Search by title'; const searchComponent = new TextComponent(contentEl); - let currentToggle: ToggleComponent = null; + let currentToggle: ToggleComponent | undefined = undefined; searchComponent.inputEl.style.width = '100%'; searchComponent.setPlaceholder(placeholder); @@ -100,7 +99,7 @@ export class MediaDbSearchModal extends Modal { const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(unCamelCase(mediaType)); - apiToggleComponent.setValue(this.selectedTypes.find(x => x.name === mediaType).selected); + apiToggleComponent.setValue(this.selectedTypes.contains(mediaType)); if (apiToggleComponent.getValue()) { currentToggle = apiToggleComponent; } @@ -108,13 +107,13 @@ export class MediaDbSearchModal extends Modal { if (value) { if (currentToggle && currentToggle !== apiToggleComponent) { currentToggle.setValue(false); - this.selectedTypes.find(x => x.name === mediaType).selected = false; + this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } currentToggle = apiToggleComponent; - this.selectedTypes.find(x => x.name === mediaType).selected = true; + this.selectedTypes.push(mediaType); } else { - currentToggle = null; - this.selectedTypes.find(x => x.name === mediaType).selected = false; + currentToggle = undefined; + this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } }); apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); @@ -140,7 +139,7 @@ export class MediaDbSearchModal extends Modal { } onClose(): void { - this.closeCallback(); + this.closeCallback?.(); const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 57c12f8..b33d886 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -1,7 +1,8 @@ -import { MediaTypeModel } from '../models/MediaTypeModel'; -import MediaDbPlugin from '../main'; +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; +import { SELECT_MODAL_OPTIONS_DEFAULT } from '../utils/ModalHelper'; import { SelectModal } from './SelectModal'; -import { SELECT_MODAL_OPTIONS_DEFAULT, SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; @@ -9,18 +10,18 @@ export class MediaDbSearchResultModal extends SelectModal { busy: boolean; sendCallback: boolean; - submitCallback: (res: SelectModalData) => void; - closeCallback: (err?: Error) => void; - skipCallback: () => void; + submitCallback?: (res: SelectModalData) => void; + closeCallback?: (err?: Error) => void; + skipCallback?: () => void; constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) { selectModalOptions = Object.assign({}, SELECT_MODAL_OPTIONS_DEFAULT, selectModalOptions); - super(plugin.app, selectModalOptions.elements, selectModalOptions.multiSelect); + super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect); this.plugin = plugin; - this.title = selectModalOptions.modalTitle; + this.title = selectModalOptions.modalTitle ?? ''; this.description = 'Select one or multiple search results.'; - this.addSkipButton = selectModalOptions.skipButton; + this.addSkipButton = selectModalOptions.skipButton ?? false; this.busy = false; @@ -50,17 +51,17 @@ export class MediaDbSearchResultModal extends SelectModal { submit(): void { if (!this.busy) { this.busy = true; - this.submitButton.setButtonText('Creating entry...'); - this.submitCallback({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) }); + this.submitButton?.setButtonText('Creating entry...'); + this.submitCallback?.({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) }); } } skip(): void { - this.skipButton.setButtonText('Skipping...'); - this.skipCallback(); + this.skipButton?.setButtonText('Skipping...'); + this.skipCallback?.(); } onClose(): void { - this.closeCallback(); + this.closeCallback?.(); } } diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts index dd89d52..596cb15 100644 --- a/src/modals/SelectModal.ts +++ b/src/modals/SelectModal.ts @@ -1,6 +1,7 @@ -import { App, ButtonComponent, Modal, Setting } from 'obsidian'; -import { SelectModalElement } from './SelectModalElement'; +import type { App, ButtonComponent } from 'obsidian'; +import { Modal, Setting } from 'obsidian'; import { mod } from '../utils/Utils'; +import { SelectModalElement } from './SelectModalElement'; export abstract class SelectModal extends Modal { allowMultiSelect: boolean; @@ -142,7 +143,7 @@ export abstract class SelectModal extends Modal { } // nothing is highlighted - this.selectModalElements.last().setHighlighted(true); + this.selectModalElements.last()?.setHighlighted(true); } highlightDown(): void { @@ -154,20 +155,20 @@ export abstract class SelectModal extends Modal { } // nothing is highlighted - this.selectModalElements.first().setHighlighted(true); + this.selectModalElements.first()?.setHighlighted(true); } private getNextSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { let nextId = selectModalElement.id + 1; nextId = mod(nextId, this.selectModalElements.length); - return this.selectModalElements.filter(x => x.id === nextId).first(); + return this.selectModalElements.find(x => x.id === nextId)!; } private getPreviousSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { let nextId = selectModalElement.id - 1; nextId = mod(nextId, this.selectModalElements.length); - return this.selectModalElements.filter(x => x.id === nextId).first(); + return this.selectModalElements.find(x => x.id === nextId)!; } } diff --git a/src/modals/SelectModalElement.ts b/src/modals/SelectModalElement.ts index 9b661b7..70c09f1 100644 --- a/src/modals/SelectModalElement.ts +++ b/src/modals/SelectModalElement.ts @@ -1,4 +1,4 @@ -import { SelectModal } from './SelectModal'; +import type { SelectModal } from './SelectModal'; export class SelectModalElement { selectModal: SelectModal; @@ -35,6 +35,8 @@ export class SelectModalElement { this.element.on('mouseleave', '#' + this.getHTMLId(), () => { this.setHighlighted(false); }); + + this.highlighted = false; } getHTMLId(): string { diff --git a/src/models/BoardGameModel.ts b/src/models/BoardGameModel.ts index 70b86e5..7f45cab 100644 --- a/src/models/BoardGameModel.ts +++ b/src/models/BoardGameModel.ts @@ -1,6 +1,8 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type BoardGameData = ModelToData; export class BoardGameModel extends MediaTypeModel { genres: string[]; @@ -19,23 +21,23 @@ export class BoardGameModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: BoardGameData) { super(); - this.genres = undefined; - this.onlineRating = undefined; - this.minPlayers = undefined; - this.maxPlayers = undefined; - this.playtime = undefined; - this.publishers = undefined; - this.complexityRating = undefined; - this.image = undefined; + this.genres = []; + this.onlineRating = 0; + this.complexityRating = 0; + this.minPlayers = 0; + this.maxPlayers = 0; + this.playtime = ''; + this.publishers = []; + this.image = ''; - this.released = undefined; + this.released = false; this.userData = { - played: undefined, - personalRating: undefined, + played: false, + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/BookModel.ts b/src/models/BookModel.ts index 183d402..67f5412 100644 --- a/src/models/BookModel.ts +++ b/src/models/BookModel.ts @@ -1,6 +1,8 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type BookData = ModelToData; export class BookModel extends MediaTypeModel { author: string; @@ -8,7 +10,6 @@ export class BookModel extends MediaTypeModel { pages: number; image: string; onlineRating: number; - english_title: string; isbn: number; isbn13: number; @@ -20,22 +21,23 @@ export class BookModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: BookData) { super(); - this.author = undefined; - this.pages = undefined; - this.image = undefined; - this.onlineRating = undefined; - this.isbn = undefined; - this.isbn13 = undefined; + this.author = ''; + this.plot = ''; + this.pages = 0; + this.image = ''; + this.onlineRating = 0; + this.isbn = 0; + this.isbn13 = 0; - this.released = undefined; + this.released = false; this.userData = { - read: undefined, - lastRead: undefined, - personalRating: undefined, + read: false, + lastRead: '', + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/GameModel.ts b/src/models/GameModel.ts index cdddff1..f1df03e 100644 --- a/src/models/GameModel.ts +++ b/src/models/GameModel.ts @@ -1,6 +1,8 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type GameData = ModelToData; export class GameModel extends MediaTypeModel { developers: string[]; @@ -17,19 +19,21 @@ export class GameModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: GameData) { super(); - this.developers = undefined; - this.publishers = undefined; - this.genres = undefined; - this.onlineRating = undefined; - this.image = undefined; - this.released = undefined; - this.releaseDate = undefined; + this.developers = []; + this.publishers = []; + this.genres = []; + this.onlineRating = 0; + this.image = ''; + + this.released = false; + this.releaseDate = ''; + this.userData = { - played: undefined, - personalRating: undefined, + played: false, + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/MangaModel.ts b/src/models/MangaModel.ts index 244a7af..70548ef 100644 --- a/src/models/MangaModel.ts +++ b/src/models/MangaModel.ts @@ -1,19 +1,12 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type MangaData = ModelToData; export class MangaModel extends MediaTypeModel { - type: string; - subType: string; - title: string; plot: string; - englishTitle: string; alternateTitles: string[]; - year: string; - dataSource: string; - url: string; - id: string; - genres: string[]; authors: string[]; chapters: number; @@ -32,27 +25,27 @@ export class MangaModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: MangaData) { super(); - this.plot = undefined; - this.genres = undefined; - this.authors = undefined; - this.alternateTitles = undefined; - this.chapters = undefined; - this.volumes = undefined; - this.onlineRating = undefined; - this.image = undefined; + this.plot = ''; + this.alternateTitles = []; + this.genres = []; + this.authors = []; + this.chapters = 0; + this.volumes = 0; + this.onlineRating = 0; + this.image = ''; - this.released = undefined; - this.status = undefined; - this.publishedFrom = undefined; - this.publishedTo = undefined; + this.released = false; + this.status = ''; + this.publishedFrom = ''; + this.publishedTo = ''; this.userData = { - watched: undefined, - lastWatched: undefined, - personalRating: undefined, + watched: false, + lastWatched: '', + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index c59e87f..51c6378 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -1,4 +1,4 @@ -import { MediaType } from '../utils/MediaType'; +import type { MediaType } from '../utils/MediaType'; export abstract class MediaTypeModel { type: string; @@ -13,14 +13,14 @@ export abstract class MediaTypeModel { userData: object; protected constructor() { - this.type = undefined; - this.subType = undefined; - this.title = undefined; - this.englishTitle = undefined; - this.year = undefined; - this.dataSource = undefined; - this.url = undefined; - this.id = undefined; + this.type = ''; + this.subType = ''; + this.title = ''; + this.englishTitle = ''; + this.year = ''; + this.dataSource = ''; + this.url = ''; + this.id = ''; this.userData = {}; } diff --git a/src/models/MovieModel.ts b/src/models/MovieModel.ts index a4db8a7..c0ab842 100644 --- a/src/models/MovieModel.ts +++ b/src/models/MovieModel.ts @@ -1,6 +1,8 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type MovieData = ModelToData; export class MovieModel extends MediaTypeModel { plot: string; @@ -23,27 +25,27 @@ export class MovieModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: MovieData) { super(); - this.plot = undefined; - this.genres = undefined; - this.director = undefined; - this.writer = undefined; - this.studio = undefined; - this.duration = undefined; - this.onlineRating = undefined; - this.actors = undefined; - this.image = undefined; + this.plot = ''; + this.genres = []; + this.director = []; + this.writer = []; + this.studio = []; + this.duration = ''; + this.onlineRating = 0; + this.actors = []; + this.image = ''; - this.released = undefined; - this.streamingServices = undefined; - this.premiere = undefined; + this.released = false; + this.streamingServices = []; + this.premiere = ''; this.userData = { - watched: undefined, - lastWatched: undefined, - personalRating: undefined, + watched: false, + lastWatched: '', + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/MusicReleaseModel.ts b/src/models/MusicReleaseModel.ts index fc14032..c3e4840 100644 --- a/src/models/MusicReleaseModel.ts +++ b/src/models/MusicReleaseModel.ts @@ -1,35 +1,28 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type MusicReleaseData = ModelToData; export class MusicReleaseModel extends MediaTypeModel { - type: string; - subType: string; - title: string; - englishTitle: string; - year: string; - dataSource: string; - url: string; - id: string; - image: string; - genres: string[]; artists: string[]; + image: string; rating: number; userData: { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: MusicReleaseData) { super(); - this.genres = undefined; - this.artists = undefined; - this.image = undefined; - this.rating = undefined; + this.genres = []; + this.artists = []; + this.image = ''; + this.rating = 0; this.userData = { - personalRating: undefined, + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/SeriesModel.ts b/src/models/SeriesModel.ts index edc222e..2f3fba3 100644 --- a/src/models/SeriesModel.ts +++ b/src/models/SeriesModel.ts @@ -1,17 +1,10 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type SeriesData = ModelToData; export class SeriesModel extends MediaTypeModel { - type: string; - subType: string; - title: string; - englishTitle: string; - year: string; - dataSource: string; - url: string; - id: string; - plot: string; genres: string[]; writer: string[]; @@ -34,29 +27,29 @@ export class SeriesModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: any = {}) { + constructor(obj: SeriesData) { super(); - this.plot = undefined; - this.genres = undefined; - this.writer = undefined; - this.studio = undefined; - this.episodes = undefined; - this.duration = undefined; - this.onlineRating = undefined; - this.actors = undefined; - this.image = undefined; + this.plot = ''; + this.genres = []; + this.writer = []; + this.studio = []; + this.episodes = 0; + this.duration = ''; + this.onlineRating = 0; + this.actors = []; + this.image = ''; - this.released = undefined; - this.streamingServices = undefined; - this.airing = undefined; - this.airedFrom = undefined; - this.airedTo = undefined; + this.released = false; + this.streamingServices = []; + this.airing = false; + this.airedFrom = ''; + this.airedTo = ''; this.userData = { - watched: undefined, - lastWatched: undefined, - personalRating: undefined, + watched: false, + lastWatched: '', + personalRating: 0, }; migrateObject(this, obj, this); diff --git a/src/models/WikiModel.ts b/src/models/WikiModel.ts index f325008..91ef9be 100644 --- a/src/models/WikiModel.ts +++ b/src/models/WikiModel.ts @@ -1,17 +1,10 @@ -import { MediaTypeModel } from './MediaTypeModel'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaType } from '../utils/MediaType'; +import { mediaDbTag, migrateObject, type ModelToData } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type WikiData = ModelToData; export class WikiModel extends MediaTypeModel { - type: string; - subType: string; - title: string; - englishTitle: string; - year: string; - dataSource: string; - url: string; - id: string; - wikiUrl: string; lastUpdated: string; length: number; @@ -19,13 +12,13 @@ export class WikiModel extends MediaTypeModel { userData: Record; - constructor(obj: any = {}) { + constructor(obj: WikiData) { super(); - this.wikiUrl = undefined; - this.lastUpdated = undefined; - this.length = undefined; - this.article = undefined; + this.wikiUrl = ''; + this.lastUpdated = ''; + this.length = 0; + this.article = ''; this.userData = {}; migrateObject(this, obj, this); diff --git a/src/settings/Icon.svelte b/src/settings/Icon.svelte index 47edb27..a73d4a4 100644 --- a/src/settings/Icon.svelte +++ b/src/settings/Icon.svelte @@ -4,13 +4,16 @@ import { setIcon } from 'obsidian'; import { onMount } from 'svelte'; - export let iconName: string = ''; - export let iconSize: number = 20; + interface Props { + iconName?: string; + } - let iconEl: HTMLElement; + let { iconName = '' }: Props = $props(); + + let iconEl: HTMLElement | undefined = $state(); onMount(() => { - setIcon(iconEl, iconName, iconSize); + setIcon(iconEl!, iconName); }); diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 90f930f..b5644dc 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,6 +1,6 @@ -import { PropertyMappingOption } from './PropertyMapping'; +import type MediaDbPlugin from '../main'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import MediaDbPlugin from '../main'; +import { PropertyMappingOption } from './PropertyMapping'; export class PropertyMapper { plugin: MediaDbPlugin; @@ -66,7 +66,7 @@ export class PropertyMapper { return obj; } - const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties; + const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type)?.properties ?? []; const originalObj: Record = {}; diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index f122c83..bad0369 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -1,5 +1,5 @@ +import type { MediaType } from '../utils/MediaType'; import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; -import { MediaType } from '../utils/MediaType'; export enum PropertyMappingOption { Default = 'default', diff --git a/src/settings/PropertyMappingModelComponent.svelte b/src/settings/PropertyMappingModelComponent.svelte index 05f6b36..0018d75 100644 --- a/src/settings/PropertyMappingModelComponent.svelte +++ b/src/settings/PropertyMappingModelComponent.svelte @@ -1,18 +1,22 @@
@@ -51,7 +55,7 @@ {/if}