From 498e2611ae0bda3c8ef39d7c3a3bbea360ec7652 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 6 May 2026 16:26:59 +0200 Subject: [PATCH 01/11] add obsidian eslint plugin, solve major issues --- .github/workflows/release.yml | 22 +- README.md | 34 +- __mocks__/obsidian.ts | 32 - automation/build/buildBanner.ts | 24 - automation/config.json | 5 - automation/fetchSchemas.ts | 20 - automation/release.ts | 171 ---- automation/stats.ts | 160 ---- automation/tsconfig.json | 19 - automation/utils/shellUtils.ts | 168 ---- automation/utils/utils.ts | 6 - automation/utils/versionUtils.ts | 108 --- bun.lock | 295 +++++-- eslint.config.mjs | 27 +- manifest.json | 2 +- package.json | 30 +- packages/obsidian/src/api/APIManager.ts | 113 +++ .../obsidian/src}/api/APIModel.ts | 12 +- .../obsidian/src/api/apis/BoardGameGeekAPI.ts | 217 +++++ .../obsidian/src/api/apis/ComicVineAPI.ts | 170 ++++ .../obsidian/src/api/apis/GiantBombAPI.ts | 169 ++++ packages/obsidian/src/api/apis/IGDBAPI.ts | 264 +++++++ packages/obsidian/src/api/apis/MALAPI.ts | 260 ++++++ .../obsidian/src}/api/apis/MALAPIManga.ts | 104 ++- .../obsidian/src/api/apis/MusicBrainzAPI.ts | 327 ++++++++ packages/obsidian/src/api/apis/OMDbAPI.ts | 388 +++++++++ .../obsidian/src/api/apis/OpenLibraryAPI.ts | 214 +++++ packages/obsidian/src/api/apis/RAWGAPI.ts | 161 ++++ .../obsidian/src}/api/apis/SteamAPI.ts | 138 +++- .../obsidian/src/api/apis/TMDBMovieAPI.ts | 265 +++++++ .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 438 +++++++++++ .../obsidian/src/api/apis/TMDBSeriesAPI.ts | 255 ++++++ packages/obsidian/src/api/apis/VNDBAPI.ts | 334 ++++++++ .../obsidian/src/api/apis/WikipediaAPI.ts | 170 ++++ packages/obsidian/src/main.ts | 209 +++++ .../src}/modals/ConfirmOverwriteModal.ts | 0 .../src}/modals/MediaDbAdvancedSearchModal.ts | 16 +- .../src}/modals/MediaDbBulkImportModal.ts | 26 +- .../src}/modals/MediaDbIdSearchModal.ts | 14 +- .../src}/modals/MediaDbPreviewModal.ts | 13 +- .../src}/modals/MediaDbSearchModal.ts | 22 +- .../src}/modals/MediaDbSearchResultModal.ts | 12 +- .../src}/modals/MediaDbSeasonSelectModal.ts | 6 +- .../obsidian/src}/modals/SelectModal.ts | 6 +- .../src}/modals/SelectModalElement.ts | 2 +- .../obsidian/src}/models/BoardGameModel.ts | 8 +- .../obsidian/src}/models/BookModel.ts | 8 +- .../obsidian/src}/models/ComicMangaModel.ts | 8 +- .../obsidian/src}/models/GameModel.ts | 8 +- .../obsidian/src}/models/MediaTypeModel.ts | 2 +- .../obsidian/src}/models/MovieModel.ts | 8 +- .../obsidian/src}/models/MusicReleaseModel.ts | 8 +- .../obsidian/src}/models/SeasonModel.ts | 8 +- .../obsidian/src}/models/SeriesModel.ts | 8 +- .../obsidian/src}/models/WikiModel.ts | 8 +- .../obsidian/src}/settings/Icon.tsx | 0 .../obsidian/src}/settings/PropertyMapper.ts | 11 +- .../obsidian/src}/settings/PropertyMapping.ts | 7 +- .../PropertyMappingModelComponent.tsx | 0 .../PropertyMappingModelsComponent.tsx | 0 .../obsidian/src}/settings/Settings.ts | 77 +- .../src}/settings/suggesters/FileSuggest.ts | 0 .../src}/settings/suggesters/FolderSuggest.ts | 0 {src => packages/obsidian/src}/styles.css | 4 + packages/obsidian/src/utils/AppError.ts | 24 + .../obsidian/src}/utils/BulkImportHelper.ts | 62 +- .../obsidian/src}/utils/DateFormatter.ts | 20 +- packages/obsidian/src/utils/ErrorReporter.ts | 23 + .../obsidian/src}/utils/IconList.ts | 0 .../utils/IllegalFilenameCharactersList.ts | 0 packages/obsidian/src/utils/Logger.ts | 38 + .../obsidian/src/utils/MediaDbEntryHelper.ts | 289 +++++++ .../obsidian/src/utils/MediaDbFileHelper.ts | 325 ++++++++ .../obsidian/src}/utils/MediaType.ts | 0 .../obsidian/src}/utils/MediaTypeManager.ts | 43 +- packages/obsidian/src/utils/ModalHelper.ts | 282 +++++++ {src => packages/obsidian/src}/utils/Utils.ts | 11 +- packages/obsidian/src/utils/result.ts | 52 ++ .../schemas/src}/GiantBomb.json | 0 .../schemas/src}/GiantBomb.ts | 0 .../schemas/src}/MALAPI.ts | 0 .../schemas/src}/OpenLibrary.json | 0 .../schemas/src}/OpenLibrary.ts | 0 .../schemas => packages/schemas/src}/TMDB.ts | 0 packages/schemas/src/fetchSchemas.sh | 15 + src/api/APIManager.ts | 82 -- src/api/apis/BoardGameGeekAPI.ts | 147 ---- src/api/apis/ComicVineAPI.ts | 117 --- src/api/apis/GiantBombAPI.ts | 167 ---- src/api/apis/IGDBAPI.ts | 159 ---- src/api/apis/MALAPI.ts | 229 ------ src/api/apis/MobyGamesAPI.ts | 121 --- src/api/apis/MusicBrainzAPI.ts | 261 ------ src/api/apis/OMDbAPI.ts | 289 ------- src/api/apis/OpenLibraryAPI.ts | 161 ---- src/api/apis/RAWGAPI.ts | 105 --- src/api/apis/TMDBMovieAPI.ts | 187 ----- src/api/apis/TMDBSeasonAPI.ts | 302 ------- src/api/apis/TMDBSeriesAPI.ts | 179 ----- src/api/apis/VNDBAPI.ts | 264 ------- src/api/apis/WikipediaAPI.ts | 112 --- src/main.ts | 743 ------------------ src/utils/ModalHelper.ts | 537 ------------- tsconfig.json | 9 +- vite.config.ts => vite.config.mts | 34 +- 105 files changed, 5702 insertions(+), 5308 deletions(-) delete mode 100644 __mocks__/obsidian.ts delete mode 100644 automation/build/buildBanner.ts delete mode 100644 automation/config.json delete mode 100644 automation/fetchSchemas.ts delete mode 100644 automation/release.ts delete mode 100644 automation/stats.ts delete mode 100644 automation/tsconfig.json delete mode 100644 automation/utils/shellUtils.ts delete mode 100644 automation/utils/utils.ts delete mode 100644 automation/utils/versionUtils.ts create mode 100644 packages/obsidian/src/api/APIManager.ts rename {src => packages/obsidian/src}/api/APIModel.ts (55%) create mode 100644 packages/obsidian/src/api/apis/BoardGameGeekAPI.ts create mode 100644 packages/obsidian/src/api/apis/ComicVineAPI.ts create mode 100644 packages/obsidian/src/api/apis/GiantBombAPI.ts create mode 100644 packages/obsidian/src/api/apis/IGDBAPI.ts create mode 100644 packages/obsidian/src/api/apis/MALAPI.ts rename {src => packages/obsidian/src}/api/apis/MALAPIManga.ts (52%) create mode 100644 packages/obsidian/src/api/apis/MusicBrainzAPI.ts create mode 100644 packages/obsidian/src/api/apis/OMDbAPI.ts create mode 100644 packages/obsidian/src/api/apis/OpenLibraryAPI.ts create mode 100644 packages/obsidian/src/api/apis/RAWGAPI.ts rename {src => packages/obsidian/src}/api/apis/SteamAPI.ts (50%) create mode 100644 packages/obsidian/src/api/apis/TMDBMovieAPI.ts create mode 100644 packages/obsidian/src/api/apis/TMDBSeasonAPI.ts create mode 100644 packages/obsidian/src/api/apis/TMDBSeriesAPI.ts create mode 100644 packages/obsidian/src/api/apis/VNDBAPI.ts create mode 100644 packages/obsidian/src/api/apis/WikipediaAPI.ts create mode 100644 packages/obsidian/src/main.ts rename {src => packages/obsidian/src}/modals/ConfirmOverwriteModal.ts (100%) rename {src => packages/obsidian/src}/modals/MediaDbAdvancedSearchModal.ts (86%) rename {src => packages/obsidian/src}/modals/MediaDbBulkImportModal.ts (79%) rename {src => packages/obsidian/src}/modals/MediaDbIdSearchModal.ts (82%) rename {src => packages/obsidian/src}/modals/MediaDbPreviewModal.ts (80%) rename {src => packages/obsidian/src}/modals/MediaDbSearchModal.ts (82%) rename {src => packages/obsidian/src}/modals/MediaDbSearchResultModal.ts (80%) rename {src => packages/obsidian/src}/modals/MediaDbSeasonSelectModal.ts (88%) rename {src => packages/obsidian/src}/modals/SelectModal.ts (95%) rename {src => packages/obsidian/src}/modals/SelectModalElement.ts (96%) rename {src => packages/obsidian/src}/models/BoardGameModel.ts (78%) rename {src => packages/obsidian/src}/models/BookModel.ts (77%) rename {src => packages/obsidian/src}/models/ComicMangaModel.ts (82%) rename {src => packages/obsidian/src}/models/GameModel.ts (76%) rename {src => packages/obsidian/src}/models/MediaTypeModel.ts (92%) rename {src => packages/obsidian/src}/models/MovieModel.ts (82%) rename {src => packages/obsidian/src}/models/MusicReleaseModel.ts (81%) rename {src => packages/obsidian/src}/models/SeasonModel.ts (82%) rename {src => packages/obsidian/src}/models/SeriesModel.ts (82%) rename {src => packages/obsidian/src}/models/WikiModel.ts (75%) rename {src => packages/obsidian/src}/settings/Icon.tsx (100%) rename {src => packages/obsidian/src}/settings/PropertyMapper.ts (87%) rename {src => packages/obsidian/src}/settings/PropertyMapping.ts (96%) rename {src => packages/obsidian/src}/settings/PropertyMappingModelComponent.tsx (100%) rename {src => packages/obsidian/src}/settings/PropertyMappingModelsComponent.tsx (100%) rename {src => packages/obsidian/src}/settings/Settings.ts (91%) rename {src => packages/obsidian/src}/settings/suggesters/FileSuggest.ts (100%) rename {src => packages/obsidian/src}/settings/suggesters/FolderSuggest.ts (100%) rename {src => packages/obsidian/src}/styles.css (99%) create mode 100644 packages/obsidian/src/utils/AppError.ts rename {src => packages/obsidian/src}/utils/BulkImportHelper.ts (67%) rename {src => packages/obsidian/src}/utils/DateFormatter.ts (80%) create mode 100644 packages/obsidian/src/utils/ErrorReporter.ts rename {src => packages/obsidian/src}/utils/IconList.ts (100%) rename {src => packages/obsidian/src}/utils/IllegalFilenameCharactersList.ts (100%) create mode 100644 packages/obsidian/src/utils/Logger.ts create mode 100644 packages/obsidian/src/utils/MediaDbEntryHelper.ts create mode 100644 packages/obsidian/src/utils/MediaDbFileHelper.ts rename {src => packages/obsidian/src}/utils/MediaType.ts (100%) rename {src => packages/obsidian/src}/utils/MediaTypeManager.ts (82%) create mode 100644 packages/obsidian/src/utils/ModalHelper.ts rename {src => packages/obsidian/src}/utils/Utils.ts (96%) create mode 100644 packages/obsidian/src/utils/result.ts rename {src/api/schemas => packages/schemas/src}/GiantBomb.json (100%) rename {src/api/schemas => packages/schemas/src}/GiantBomb.ts (100%) rename {src/api/schemas => packages/schemas/src}/MALAPI.ts (100%) rename {src/api/schemas => packages/schemas/src}/OpenLibrary.json (100%) rename {src/api/schemas => packages/schemas/src}/OpenLibrary.ts (100%) rename {src/api/schemas => packages/schemas/src}/TMDB.ts (100%) create mode 100644 packages/schemas/src/fetchSchemas.sh delete mode 100644 src/api/APIManager.ts delete mode 100644 src/api/apis/BoardGameGeekAPI.ts delete mode 100644 src/api/apis/ComicVineAPI.ts delete mode 100644 src/api/apis/GiantBombAPI.ts delete mode 100644 src/api/apis/IGDBAPI.ts delete mode 100644 src/api/apis/MALAPI.ts delete mode 100644 src/api/apis/MobyGamesAPI.ts delete mode 100644 src/api/apis/MusicBrainzAPI.ts delete mode 100644 src/api/apis/OMDbAPI.ts delete mode 100644 src/api/apis/OpenLibraryAPI.ts delete mode 100644 src/api/apis/RAWGAPI.ts delete mode 100644 src/api/apis/TMDBMovieAPI.ts delete mode 100644 src/api/apis/TMDBSeasonAPI.ts delete mode 100644 src/api/apis/TMDBSeriesAPI.ts delete mode 100644 src/api/apis/VNDBAPI.ts delete mode 100644 src/api/apis/WikipediaAPI.ts delete mode 100644 src/main.ts delete mode 100644 src/utils/ModalHelper.ts rename vite.config.ts => vite.config.mts (70%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2594994..e7202e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,9 +13,14 @@ jobs: build: runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine prerelease status id: status @@ -27,23 +32,32 @@ jobs: fi - name: Install Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Build id: build run: | - bun install + bun install --frozen-lockfile bun run build mkdir ${{ env.PLUGIN_NAME }} cp dist/main.js dist/manifest.json dist/styles.css ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }} ls + - name: Attest build provenance + uses: actions/attest@v4 + with: + subject-path: | + dist/main.js + dist/manifest.json + dist/styles.css + ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip + - name: Release id: release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: prerelease: ${{ steps.status.outputs.prerelease }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 7cd31f3..5a3e090 100644 --- a/README.md +++ b/README.md @@ -116,20 +116,18 @@ Now you select the result you want, and the plugin will cast its magic, creating ### 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 movies, 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 | -| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Read Access Token` (**not** `API Key`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes | -| [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 | -| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | -| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | -| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | 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 movies, 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 | +| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Read Access Token` (**not** `API Key`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes | +| [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 | +| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | +| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | +| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | No | #### Notes @@ -137,6 +135,8 @@ Now you select the result you want, and the plugin will cast its magic, creating - 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" +- Support for the [Moby Games](https://www.mobygames.com) API has been removed from the plugins as the API is no longer free to use. +- Support for the [Giant Bomb](https://www.giantbomb.com) API has been removed from the plugin temporarily as their API is [currently non functional](https://giantbomb.com/api). #### Search by ID @@ -174,12 +174,6 @@ Now you select the result you want, and the plugin will cast its magic, creating - This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) ` - For a specific edition of "Fantastic Mr. Fox" the "/books/" URL looks like this `https://openlibrary.org/books/OL3567303M/` so the ID is `/books/OL3567303M` - This URL is located in the editions section` -- [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` - [Comic Vine](https://www.comicvine.gamespot.com) - you can find this ID in the URL - e.g. for "Boule & Bill" the URL looks like this `https://comicvine.gamespot.com/boule-bill/4050-70187/` so the ID is `4050-70187` diff --git a/__mocks__/obsidian.ts b/__mocks__/obsidian.ts deleted file mode 100644 index 5ae0205..0000000 --- a/__mocks__/obsidian.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { RequestUrlParam, RequestUrlResponse } from 'obsidian'; - -export function requestUrl(request: RequestUrlParam): Promise { - return fetch(request.url, { - method: request.method, - headers: request.headers, - body: request.body, - }).then(async response => { - if (response.status >= 400 && request.throw) { - throw new Error(`Request failed, ${response.status}`); - } - - // Turn response headers into Record object - const headers: Record = {}; - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - const arraybuffer = await response.arrayBuffer(); - const text = arraybuffer ? new TextDecoder().decode(arraybuffer) : ''; - const json = text ? JSON.parse(text) : {}; - - let response_body: RequestUrlResponse = { - status: response.status, - headers: headers, - arrayBuffer: arraybuffer, - json: json, - text: text, - }; - return response_body; - }); -} diff --git a/automation/build/buildBanner.ts b/automation/build/buildBanner.ts deleted file mode 100644 index 849aec3..0000000 --- a/automation/build/buildBanner.ts +++ /dev/null @@ -1,24 +0,0 @@ -import manifest from '../../manifest.json' with { type: 'json' }; - -export function getBuildBanner(buildType: string, getVersion: (version: string) => string) { - return `/* -------------------------------------------- -${manifest.name} - ${buildType} -------------------------------------------- -By: ${manifest.author} (${manifest.authorUrl}) -Time: ${new Date().toUTCString()} -Version: ${getVersion(manifest.version)} -------------------------------------------- -THIS IS A GENERATED/BUNDLED FILE -if you want to view the source, please visit the github repository of this plugin -------------------------------------------- -Copyright (c) ${new Date().getFullYear()} ${manifest.author} -------------------------------------------- -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . -*/ -`; -} diff --git a/automation/config.json b/automation/config.json deleted file mode 100644 index c9a8947..0000000 --- a/automation/config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "devBranch": "master", - "releaseBranch": "release", - "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin" -} diff --git a/automation/fetchSchemas.ts b/automation/fetchSchemas.ts deleted file mode 100644 index c5c2102..0000000 --- a/automation/fetchSchemas.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { $ } from './utils/shellUtils'; - -async function fetchSchema() { - // https://docs.api.jikan.moe/ - await $('bun openapi-typescript https://raw.githubusercontent.com/jikan-me/jikan-rest/master/storage/api-docs/api-docs.json -o ./src/api/schemas/MALAPI.ts'); - - // https://www.giantbomb.com/forums/api-developers-3017/giant-bomb-openapi-specification-1901269/ - await $('bun openapi-typescript ./src/api/schemas/GiantBomb.json -o ./src/api/schemas/GiantBomb.ts'); - - // https://www.omdbapi.com/swagger.json - // await $('bun openapi-typescript ./src/api/schemas/OMDb.json -o ./src/api/schemas/OMDb.ts'); - - // https://github.com/internetarchive/openlibrary-api/blob/main/swagger.yaml - await $('bun openapi-typescript ./src/api/schemas/OpenLibrary.json -o ./src/api/schemas/OpenLibrary.ts'); - - // https://developer.themoviedb.org/openapi - await $('bun openapi-typescript https://developer.themoviedb.org/openapi/tmdb-api.json -o ./src/api/schemas/TMDB.ts'); -} - -await fetchSchema(); diff --git a/automation/release.ts b/automation/release.ts deleted file mode 100644 index 9c60136..0000000 --- a/automation/release.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { UserError } from './utils/utils'; -import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from './utils/versionUtils'; -import config from './config.json'; -import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from './utils/shellUtils'; - -async function runPreconditions(): Promise { - // run preconditions - await $seq( - [`bun run format`, `bun run test`], - (cmd: string) => { - throw new UserError(`precondition "${cmd}" failed`); - }, - () => {}, - undefined, - Verboseness.VERBOSE, - ); - - // add changed files - await $seq( - [`git add .`], - () => { - throw new UserError('failed to add preconditions changes to git'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - - // check if there were any changes - let changesToCommit = false; - await $seq( - [`git diff --quiet`, `git diff --cached --quiet`], - () => { - changesToCommit = true; - }, - () => {}, - undefined, - Verboseness.QUITET, - ); - - // if there were any changes, commit them - if (changesToCommit) { - await $seq( - [`git commit -m "[auto] run release preconditions"`], - () => { - throw new UserError('failed to add preconditions changes to git'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - } -} - -async function run() { - console.log('looking for untracked changes ...'); - - // check for any uncommited files and exit if there are any - await $seq( - [`git add .`, `git diff --quiet`, `git diff --cached --quiet`, `git checkout ${config.devBranch}`], - () => { - throw new UserError('there are still untracked changes'); - }, - () => {}, - undefined, - Verboseness.QUITET, - ); - - console.log('\nrunning preconditions ...\n'); - - await runPreconditions(); - - console.log('\nbumping versions ...\n'); - - const manifestFile = Bun.file('./manifest.json'); - const manifest = await manifestFile.json(); - - const versionString: string = manifest.version; - const currentVersion: Version = parseVersion(versionString); - const currentVersionString = stringifyVersion(currentVersion); - - const versionIncrementOptions = getIncrementOptions(currentVersion); - - const selectedIndex = await $choice( - `Current version "${currentVersionString}". Select new version`, - versionIncrementOptions.map(x => stringifyVersion(x)), - ); - const newVersion = versionIncrementOptions[selectedIndex]; - const newVersionString = stringifyVersion(newVersion); - - console.log(''); - - await $confirm(`Version will be updated "${currentVersionString}" -> "${newVersionString}". Are you sure`, () => { - throw new UserError('user canceled script'); - }); - - if (!(newVersion instanceof CanaryVersion)) { - manifest.version = newVersionString; - } - - await Bun.write(manifestFile, JSON.stringify(manifest, null, '\t')); - - const betaManifest = structuredClone(manifest); - betaManifest.version = newVersionString; - - const betaManifestFile = Bun.file('./manifest-beta.json'); - await Bun.write(betaManifestFile, JSON.stringify(betaManifest, null, '\t')); - - if (!(newVersion instanceof CanaryVersion)) { - const versionsFile = Bun.file('./versions.json'); - const versionsJson = await versionsFile.json(); - - versionsJson[newVersionString] = manifest.minAppVersion; - - await Bun.write(versionsFile, JSON.stringify(versionsJson, null, '\t')); - - const packageFile = Bun.file('./package.json'); - const packageJson = await packageFile.json(); - - packageJson.version = newVersionString; - - await Bun.write(packageFile, JSON.stringify(packageJson, null, '\t')); - } - - await $seq( - [`bun run format`, `git add .`, `git commit -m "[auto] bump version to \`${newVersionString}\`"`], - () => { - throw new UserError('failed to add preconditions changes to git'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - - console.log('\ncreating release tag ...\n'); - - await $seq( - [ - `git checkout ${config.releaseBranch}`, - `git merge ${config.devBranch} --commit -m "[auto] merge \`${newVersionString}\` release commit"`, - `git push origin ${config.releaseBranch}`, - `git tag -a ${newVersionString} -m "release version ${newVersionString}"`, - `git push origin ${newVersionString}`, - `git checkout ${config.devBranch}`, - `git merge ${config.releaseBranch}`, - `git push origin ${config.devBranch}`, - ], - () => { - throw new UserError('failed to merge or create tag'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - - console.log(''); - - console.log(`${CMD_FMT.BgGreen}done${CMD_FMT.Reset}`); - console.log(`${config.github}`); - console.log(`${config.github}/releases/tag/${newVersionString}`); -} - -try { - await run(); -} catch (e) { - if (e instanceof UserError) { - console.error(e.message); - } else { - console.error(e); - } -} diff --git a/automation/stats.ts b/automation/stats.ts deleted file mode 100644 index 2a34c03..0000000 --- a/automation/stats.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as fs from 'fs'; - -interface Stat { - fileType: string; - count: number; - lines: number; -} - -abstract class StatsBase { - parent: StatsBase | undefined; - path: string; - name: string; - stats: Stat[]; - - constructor(parent: StatsBase | undefined, path: string, name: string, stats: Stat[]) { - this.parent = parent; - - this.path = path; - this.name = name; - this.stats = stats; - } - - abstract addChild(child: StatsBase): void; - - abstract mergeStats(stats: Stat[]): void; - - abstract print(depth: number, lastChildArr: boolean[]): void; - - abstract sort(): void; - - getPrefix(depth: number, lastChildArr: boolean[]): string { - let prefix = ''; - for (let i = 0; i < depth; i++) { - prefix += lastChildArr[i] ? ' ' : '│ '; - } - - if (lastChildArr.at(-1)) { - prefix += '└─ '; - } else { - prefix += '├─ '; - } - - return prefix; - } -} - -class FolderStats extends StatsBase { - children: StatsBase[]; - - constructor(parent: StatsBase | undefined, path: string, name: string) { - super(parent, path, name, []); - - this.children = []; - } - - addChild(child: StatsBase) { - this.children.push(child); - this.mergeStats(child.stats); - } - - mergeStats(stats: Stat[]): void { - // console.log(this, stats); - for (const stat of stats) { - const existingStat = this.stats.find(s => s.fileType === stat.fileType); - if (existingStat) { - existingStat.count += stat.count; - existingStat.lines += stat.lines; - } else { - this.stats.push(structuredClone(stat)); - } - } - - this.parent?.mergeStats(stats); - } - - print(depth: number, lastChildArr: boolean[]): void { - console.log( - `${this.getPrefix(depth, lastChildArr)}${this.name} | ${this.stats.reduce((acc, s) => acc + s.count, 0)} files | ${this.stats.reduce((acc, s) => acc + s.lines, 0)} lines`, - ); - for (let i = 0; i < this.children.length; i++) { - const child = this.children[i]; - child.print(depth + 1, [...lastChildArr, i === this.children.length - 1]); - } - } - - sort(): void { - this.children.sort((a, b) => { - if (a instanceof FolderStats && b instanceof FileStats) { - return 1; - } else if (a instanceof FileStats && b instanceof FolderStats) { - return -1; - } else { - return a.name.localeCompare(b.name); - } - }); - this.children.forEach(c => c.sort()); - } -} - -class FileStats extends StatsBase { - constructor(parent: StatsBase, path: string, name: string, stats: Stat[]) { - super(parent, path, name, stats); - } - - addChild(_child: StatsBase): void { - throw new Error('Cannot add child to file'); - } - - mergeStats(_stats: Stat[]): void { - throw new Error('Cannot merge stats to file'); - } - - print(depth: number, lastChildArr: boolean[]): void { - console.log(`${this.getPrefix(depth, lastChildArr)}${this.name} | ${this.stats[0].lines} lines`); - } - - sort(): void {} -} - -function collectStats() { - const root = new FolderStats(undefined, './src', 'src'); - const ignore = ['node_modules', 'extraTypes', 'bun.lockb']; - - const todo: FolderStats[] = [root]; - - while (todo.length > 0) { - const current = todo.pop()!; - const children = fs.readdirSync(current.path, { withFileTypes: true }); - - for (const child of children) { - if (ignore.includes(child.name)) { - continue; - } - - if (child.isDirectory()) { - const folder = new FolderStats(current, `${current.path}/${child.name}`, child.name); - current.addChild(folder); - - todo.push(folder); - } else { - const content = fs.readFileSync(`${current.path}/${child.name}`, 'utf-8'); - - const file = new FileStats(current, `${current.path}/${child.name}`, child.name, [ - { - fileType: child.name.split('.').splice(1).join('.'), - count: 1, - lines: content.split('\n').length, - }, - ]); - current.addChild(file); - } - } - } - - root.sort(); - - root.print(0, [true]); -} - -collectStats(); diff --git a/automation/tsconfig.json b/automation/tsconfig.json deleted file mode 100644 index 8e1333b..0000000 --- a/automation/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "module": "ESNext", - "target": "ESNext", - "allowJs": true, - "noImplicitAny": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "moduleResolution": "bundler", - "importHelpers": true, - "isolatedModules": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "Es2022"], - "types": ["bun-types"], - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true - }, - "include": ["**/*.ts"] -} diff --git a/automation/utils/shellUtils.ts b/automation/utils/shellUtils.ts deleted file mode 100644 index d1c58e0..0000000 --- a/automation/utils/shellUtils.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Subprocess } from 'bun'; -import stringArgv from 'string-argv'; - -export enum Verboseness { - QUITET, - NORMAL, - VERBOSE, -} - -function exec(c: string, cwd?: string): Subprocess<'ignore', 'pipe', 'inherit'> { - return Bun.spawn(stringArgv(c), { cwd: cwd }); -} - -export async function $(cmd: string, cwd?: string | undefined, verboseness: Verboseness = Verboseness.NORMAL): Promise<{ stdout: string; stderr: string; exit: number }> { - if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) { - if (cwd !== undefined) { - console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} in ${cwd} - ${cmd}\n`); - } else { - console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} - ${cmd}\n`); - } - } - - const proc = exec(cmd, cwd); - const stdout = await new Response(proc.stdout).text(); - const stderr = await new Response(proc.stderr).text(); - - if (verboseness === Verboseness.VERBOSE) { - if (stdout !== '') { - console.log( - stdout - .split('\n') - .map(x => `${CMD_FMT.FgGray}>${CMD_FMT.Reset} ${x}\n`) - .join(''), - ); - } - - if (stderr !== '') { - console.log( - stderr - .split('\n') - .map(x => `${CMD_FMT.FgRed}>${CMD_FMT.Reset} ${x}\n`) - .join(''), - ); - } - } - - const exit = await proc.exited; - - if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) { - if (exit === 0) { - console.log(`${CMD_FMT.FgGreen}success${CMD_FMT.Reset} - ${cmd}\n`); - } else { - console.log(`${CMD_FMT.FgRed}fail${CMD_FMT.Reset} - ${cmd} - code ${exit}\n`); - } - } - - return { - stdout, - stderr, - exit, - }; -} - -export async function $seq( - cmds: string[], - onError: (cmd: string, index: number) => void, - onSuccess: () => void, - cwd?: string | undefined, - verboseness: Verboseness = Verboseness.NORMAL, -): Promise { - const results = []; - for (let i = 0; i < cmds.length; i += 1) { - const cmd = cmds[i]; - const result = await $(cmd, cwd, verboseness); - - if (result.exit !== 0) { - onError(cmd, i); - return; - } - - results.push(result); - } - onSuccess(); -} - -export async function $input(message: string): Promise { - console.write(`${message} `); - const stdin = Bun.stdin.stream(); - const reader = stdin.getReader(); - const chunk = await reader.read(); - reader.releaseLock(); - const text = Buffer.from(chunk.value ?? '').toString(); - return text.trim(); -} - -export async function $choice(message: string, options: string[]): Promise { - console.log(`${message} `); - - let optionNumbers = new Map(); - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - console.log(`[${i}] ${option}`); - - optionNumbers.set(i.toString(), i); - } - - let ret: undefined | number = undefined; - - while (ret === undefined) { - const selectedStr = await $input(`Select [${[...optionNumbers.keys()].join('/')}]:`); - - ret = optionNumbers.get(selectedStr); - - if (ret === undefined) { - console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`); - } - } - - return ret; -} - -export async function $confirm(message: string, onReject: () => void): Promise { - while (true) { - const answer = await $input(`${message} [Y/N]?`); - - if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { - return; - } - - if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') { - onReject(); - return; - } - - console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`); - } -} - -export const CMD_FMT = { - Reset: '\x1b[0m', - Bright: '\x1b[1m', - Dim: '\x1b[2m', - Underscore: '\x1b[4m', - Blink: '\x1b[5m', - Reverse: '\x1b[7m', - Hidden: '\x1b[8m', - - FgBlack: '\x1b[30m', - FgRed: '\x1b[31m', - FgGreen: '\x1b[32m', - FgYellow: '\x1b[33m', - FgBlue: '\x1b[34m', - FgMagenta: '\x1b[35m', - FgCyan: '\x1b[36m', - FgWhite: '\x1b[37m', - FgGray: '\x1b[90m', - - BgBlack: '\x1b[40m', - BgRed: '\x1b[41m', - BgGreen: '\x1b[42m', - BgYellow: '\x1b[43m', - BgBlue: '\x1b[44m', - BgMagenta: '\x1b[45m', - BgCyan: '\x1b[46m', - BgWhite: '\x1b[47m', - BgGray: '\x1b[100m', -}; diff --git a/automation/utils/utils.ts b/automation/utils/utils.ts deleted file mode 100644 index f5d72ee..0000000 --- a/automation/utils/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class UserError extends Error {} - -export interface ProjectConfig { - corePackages: string[]; - packages: string[]; -} diff --git a/automation/utils/versionUtils.ts b/automation/utils/versionUtils.ts deleted file mode 100644 index 8ad3fa2..0000000 --- a/automation/utils/versionUtils.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Parser } from '@lemons_dev/parsinom/lib/Parser'; -import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils'; -import { P } from '@lemons_dev/parsinom/lib/ParsiNOM'; -import Moment from 'moment'; -import { UserError } from './utils'; - -export class Version { - major: number; - minor: number; - patch: number; - - constructor(major: number, minor: number, patch: number) { - this.major = major; - this.minor = minor; - this.patch = patch; - } - - toString(): string { - return `${this.major}.${this.minor}.${this.patch}`; - } -} - -export class CanaryVersion extends Version { - canary: string; - - constructor(major: number, minor: number, patch: number, canary: string) { - super(major, minor, patch); - this.canary = canary; - } - - toString(): string { - return `${super.toString()}-canary.${this.canary}`; - } -} - -const numberParser: Parser = P_UTILS.digits() - .map(x => Number.parseInt(x)) - .chain(x => { - if (Number.isNaN(x)) { - return P.fail('a number'); - } else { - return P.succeed(x); - } - }); - -const canaryParser: Parser = P.sequenceMap( - (_, c1, c2, c3) => { - return c1 + c2 + c3; - }, - P.string('-canary.'), - P_UTILS.digit() - .repeat(8, 8) - .map(x => x.join('')), - P.string('T'), - P_UTILS.digit() - .repeat(6, 6) - .map(x => x.join('')), -); - -export const versionParser: Parser = P.or( - P.sequenceMap( - (major, _1, minor, _2, patch) => { - return new Version(major, minor, patch); - }, - numberParser, - P.string('.'), - numberParser, - P.string('.'), - numberParser, - P_UTILS.eof(), - ), - P.sequenceMap( - (major, _1, minor, _2, patch, canary) => { - return new CanaryVersion(major, minor, patch, canary); - }, - numberParser, - P.string('.'), - numberParser, - P.string('.'), - numberParser, - canaryParser, - P_UTILS.eof(), - ), -); - -export function parseVersion(str: string): Version { - const parserRes = versionParser.tryParse(str); - if (parserRes.success) { - return parserRes.value; - } else { - throw new UserError(`failed to parse manifest version "${str}"`); - } -} - -export function stringifyVersion(version: Version): string { - return version.toString(); -} - -export function getIncrementOptions(version: Version): [Version, Version, Version, CanaryVersion] { - const moment = Moment(); - const canary = moment.utcOffset(0).format('YYYYMMDDTHHmmss'); - return [ - new Version(version.major + 1, 0, 0), - new Version(version.major, version.minor + 1, 0), - new Version(version.major, version.minor, version.patch + 1), - new CanaryVersion(version.major, version.minor, version.patch, canary), - ]; -} diff --git a/bun.lock b/bun.lock index 2460bba..9f91cfa 100644 --- a/bun.lock +++ b/bun.lock @@ -5,33 +5,34 @@ "": { "name": "obsidian-media-db-plugin", "devDependencies": { - "@happy-dom/global-registrator": "^20.8.9", - "@lemons_dev/parsinom": "^0.1.0", - "@types/bun": "^1.3.11", + "@happy-dom/global-registrator": "^20.9.0", + "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", + "@types/bun": "^1.3.13", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-obsidianmd": "^0.2.9", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", - "prettier": "^3.8.1", + "prettier": "^3.8.3", "solid-js": "^1.9.12", - "string-argv": "^0.3.2", "tslib": "^2.8.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.0", - "vite": "^8.0.5", + "vite": "^8.0.10", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", - "vite-plugin-static-copy": "^4.0.1", + "vite-plugin-static-copy": "^4.1.0", }, }, }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], @@ -55,7 +56,7 @@ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], @@ -69,9 +70,9 @@ "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], - "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -89,18 +90,24 @@ "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@eslint/json": ["@eslint/json@0.14.0", "", { "dependencies": { "@eslint/core": "^0.17.0", "@eslint/plugin-kit": "^0.4.1", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.9.0" } }, "sha512-lBW6/m5BIFl3pMuWPNN0lIOYw9LMCmPfix53ExS3FBi4E+NELEljQ3xH6aAV9IYiQRfn9YIIgzzMrD0vIcD7tw=="], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + "@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="], + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -113,55 +120,61 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.1.0", "", {}, "sha512-Jek2Drc1n6lRbr3QCB9JzK4rId5/U3rMmywiE/Quqmvq+f4unVqLeHe6XVTm6DeKn/T4Udgx/pC4Okt8FHX2sw=="], + "@lemons_dev/lemons-obsidian-plugin-automation": ["@lemons_dev/lemons-obsidian-plugin-automation@0.1.3", "", { "dependencies": { "@lemons_dev/parsinom": "^0.2.1", "moment": "^2.30.1", "string-argv": "^0.3.2" }, "peerDependencies": { "vite": "^8.0.9" }, "bin": { "lemons-automation": "bin/lemons-automation.js" } }, "sha512-Nv1eONy1Q9citS+nsMMasgL9ZfhKb57uinJmHRF+F1H+9HSiRkB5wlx9J/S1ou9cJL8IAhFvv72KGzE9HTIWtg=="], + + "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.2.1", "", {}, "sha512-ew+G3gm5aWJBnIhrysxI7k09FCAtAoj48wbTzr16NoX9ndhB8UnhmqjTxYVv46DsG88HQo+DTWPIFQW5qTAwwg=="], "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], + "@microsoft/eslint-plugin-sdl": ["@microsoft/eslint-plugin-sdl@1.1.0", "", { "dependencies": { "eslint-plugin-n": "17.10.3", "eslint-plugin-react": "7.37.3", "eslint-plugin-security": "1.4.0" }, "peerDependencies": { "eslint": "^9" } }, "sha512-dxdNHOemLnBhfY3eByrujX9KyLigcNtW8sU+axzWv5nLGcsSBeKW2YYyTpfPo1hV8YPOmIGnfA4fZHyKVtWqBQ=="], - "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="], "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="], "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], - "@redocly/openapi-core": ["@redocly/openapi-core@1.34.11", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-V09ayfnb5GyysmvARbt+voFZAjGcf7hSYxOYxSkCc4fbH/DTfq5YWoec8cflvmHHqyIFbqvmGKmYFzqhr9zxDg=="], + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -171,17 +184,19 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], @@ -189,25 +204,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -215,7 +230,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -229,12 +244,16 @@ "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], @@ -247,19 +266,19 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -267,7 +286,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001786", "", {}, "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -311,16 +330,22 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="], + "electron-to-chromium": ["electron-to-chromium@1.5.351", "", {}, "sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA=="], + + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -335,14 +360,34 @@ "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="], + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + "eslint-plugin-depend": ["eslint-plugin-depend@1.3.1", "", { "dependencies": { "empathic": "^2.0.0", "module-replacements": "^2.8.0", "semver": "^7.6.3" } }, "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw=="], + + "eslint-plugin-es-x": ["eslint-plugin-es-x@7.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", "@eslint-community/regexpp": "^4.11.0", "eslint-compat-utils": "^0.5.1" }, "peerDependencies": { "eslint": ">=8" } }, "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ=="], + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + "eslint-plugin-json-schema-validator": ["eslint-plugin-json-schema-validator@5.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", "ajv": "^8.0.0", "debug": "^4.3.1", "eslint-compat-utils": "^0.5.0", "json-schema-migrate": "^2.0.0", "jsonc-eslint-parser": "^2.0.0", "minimatch": "^8.0.0", "synckit": "^0.9.0", "toml-eslint-parser": "^0.9.0", "tunnel-agent": "^0.6.0", "yaml-eslint-parser": "^1.0.0" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-ZmVyxRIjm58oqe2kTuy90PpmZPrrKvOjRPXKzq8WCgRgAkidCgm5X8domL2KSfadZ3QFAmifMgGTcVNhZ5ez2g=="], + + "eslint-plugin-n": ["eslint-plugin-n@17.10.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "enhanced-resolve": "^5.17.0", "eslint-plugin-es-x": "^7.5.0", "get-tsconfig": "^4.7.0", "globals": "^15.8.0", "ignore": "^5.2.4", "minimatch": "^9.0.5", "semver": "^7.5.3" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw=="], + + "eslint-plugin-no-relative-import-paths": ["eslint-plugin-no-relative-import-paths@1.6.1", "", {}, "sha512-YZNeOnsOrJcwhFw0X29MXjIzu2P/f5X2BZDPWw1R3VUYBRFxNIh77lyoL/XrMU9ewZNQPcEvAgL/cBOT1P330A=="], + + "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], + + "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="], + "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], + "eslint-plugin-react": ["eslint-plugin-react@7.37.3", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA=="], + + "eslint-plugin-security": ["eslint-plugin-security@2.1.1", "", { "dependencies": { "safe-regex": "^2.1.1" } }, "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w=="], + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -363,6 +408,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -395,6 +442,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -403,7 +452,9 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -417,7 +468,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -445,7 +496,7 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], @@ -493,6 +544,8 @@ "iso-639-2": ["iso-639-2@3.0.2", "", {}, "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw=="], + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -503,12 +556,18 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema-migrate": ["json-schema-migrate@2.0.0", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "jsonc-eslint-parser": ["jsonc-eslint-parser@2.4.2", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", "espree": "^9.0.0", "semver": "^7.3.5" } }, "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -541,6 +600,8 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -551,20 +612,24 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "module-replacements": ["module-replacements@2.11.0", "", {}, "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA=="], + + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], @@ -617,18 +682,24 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -637,19 +708,27 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], + + "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -663,7 +742,7 @@ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], @@ -679,6 +758,10 @@ "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], @@ -695,16 +778,24 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "synckit": ["synckit@0.9.3", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toml-eslint-parser": ["toml-eslint-parser@0.9.3", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0" } }, "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -717,13 +808,13 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], + "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -731,13 +822,13 @@ "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], - "vite": ["vite@8.0.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="], + "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="], "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "vite-plugin-static-copy": ["vite-plugin-static-copy@4.0.1", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-r3kQUrrimduikhyRm58ayemoxsgB8lZdn/JULLL4wpXHAZlYejtyZx7E/id7dwRtIOSYWu/tWvFjdEOTzso2MA=="], + "vite-plugin-static-copy": ["vite-plugin-static-copy@4.1.0", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], @@ -761,8 +852,12 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], + "yaml-eslint-parser": ["yaml-eslint-parser@1.3.2", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0", "yaml": "^2.0.0" } }, "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -771,6 +866,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@microsoft/eslint-plugin-sdl/eslint-plugin-security": ["eslint-plugin-security@1.4.0", "", { "dependencies": { "safe-regex": "^1.1.0" } }, "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA=="], + "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], @@ -791,20 +888,66 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "eslint-compat-utils/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-depend/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-json-schema-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "eslint-plugin-json-schema-validator/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "eslint-plugin-n/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + + "eslint-plugin-obsidianmd/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + + "json-schema-migrate/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "jsonc-eslint-parser/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], + "toml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "yaml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@microsoft/eslint-plugin-sdl/eslint-plugin-security/safe-regex": ["safe-regex@1.1.0", "", { "dependencies": { "ret": "~0.1.10" } }, "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg=="], + + "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "eslint-plugin-json-schema-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/eslint.config.mjs b/eslint.config.mjs index c634b74..5f85572 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,17 +1,25 @@ // @ts-check import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import only_warn from 'eslint-plugin-only-warn'; -import * as plugin_import from 'eslint-plugin-import'; +import no_relative_import_paths from 'eslint-plugin-no-relative-import-paths'; +import obsidianmd from 'eslint-plugin-obsidianmd'; -export default tseslint.config( +export default defineConfig( { - ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css', 'src/api/schemas/'], + ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css'], }, { - files: ['src/**/*.ts'], - extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked], + files: ['packages/obsidian/src/**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ...obsidianmd.configs.recommended, + ], languageOptions: { parser: tseslint.parser, parserOptions: { @@ -21,7 +29,8 @@ export default tseslint.config( plugins: { // @ts-ignore 'only-warn': only_warn, - import: plugin_import, + 'no-relative-import-paths': no_relative_import_paths, + obsidianmd: obsidianmd, }, rules: { '@typescript-eslint/no-explicit-any': ['warn'], @@ -44,11 +53,15 @@ export default tseslint.config( '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], '@typescript-eslint/restrict-template-expressions': 'off', + 'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: false }], + '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/explicit-function-return-type': ['warn'], - '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-deprecated': 'off', + + 'obsidianmd/ui/sentence-case': 'off', }, }, ); diff --git a/manifest.json b/manifest.json index a8d101f..5e91e7f 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "obsidian-media-db-plugin", "name": "Media DB", "version": "0.8.0", - "minAppVersion": "1.5.0", + "minAppVersion": "1.12.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev", diff --git a/package.json b/package.json index 815c67a..5661e62 100644 --- a/package.json +++ b/package.json @@ -6,41 +6,41 @@ "scripts": { "dev": "vite build --watch --mode development", "build": "bun run tsc && vite build --mode production", - "tsc": "tsc -noEmit -skipLibCheck", + "typecheck": "tsc -noEmit -skipLibCheck", "test": "bun test", "test:log": "LOG_TESTS=true bun test", "format": "prettier --write .", "format:check": "prettier --check .", - "lint": "eslint --max-warnings=0 --no-warn-ignored src/**", - "lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**", - "check": "bun run format:check && bun run tsc && bun run lint", - "check:fix": "bun run format && bun run tsc && bun run lint:fix", - "release": "bun run automation/release.ts", - "stats": "bun run automation/stats.ts" + "lint": "eslint --max-warnings=0 --no-warn-ignored packages/obsidian/src/**", + "lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored packages/obsidian/src/**", + "check": "bun run format:check && bun run typecheck && bun run lint", + "check:fix": "bun run format && bun run typecheck && bun run lint:fix", + "release": "lemons-automation release" }, "keywords": [], "author": "Moritz Jung", "license": "GPL-3.0", "devDependencies": { - "@happy-dom/global-registrator": "^20.8.9", - "@lemons_dev/parsinom": "^0.1.0", - "@types/bun": "^1.3.11", + "@happy-dom/global-registrator": "^20.9.0", + "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", + "@types/bun": "^1.3.13", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-obsidianmd": "^0.2.9", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", - "prettier": "^3.8.1", + "prettier": "^3.8.3", "solid-js": "^1.9.12", - "string-argv": "^0.3.2", "tslib": "^2.8.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.0", - "vite": "^8.0.5", + "vite": "^8.0.10", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", - "vite-plugin-static-copy": "^4.0.1" + "vite-plugin-static-copy": "^4.1.0" } } diff --git a/packages/obsidian/src/api/APIManager.ts b/packages/obsidian/src/api/APIManager.ts new file mode 100644 index 0000000..de9ba41 --- /dev/null +++ b/packages/obsidian/src/api/APIManager.ts @@ -0,0 +1,113 @@ +import type { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; + +export interface ApiQueryOk { + items: MediaTypeModel[]; + warnings: AppError[]; +} + +export class APIManager { + apis: APIModel[]; + + constructor() { + this.apis = []; + } + + /** + * Queries the basic info for one query string and multiple APIs. + * + * @param query + * @param apisToQuery + */ + async query(query: string, apisToQuery: string[]): Promise> { + Logger.debug(`MDB | api manager queried with "${query}"`); + + const apis = this.apis.filter(api => apisToQuery.includes(api.apiName)); + const results = await Promise.all(apis.map(api => api.searchByTitle(query))); + + const items: MediaTypeModel[] = []; + const warnings: AppError[] = []; + for (const result of results) { + if (result.ok) { + items.push(...result.value); + } else { + warnings.push(result.error); + } + } + + if (items.length === 0 && warnings.length > 0) { + // If all APIs failed, surface an error (using the first as representative) + return err( + toAppError(warnings[0], { + kind: AppErrorKind.Api, + message: 'Failed to query APIs', + userMessage: 'Failed to query APIs', + context: { query, apisToQuery }, + }), + ); + } + + for (const warning of warnings) { + Logger.warn(warning); + } + + return ok({ items, warnings }); + } + + /** + * Queries detailed information for a MediaTypeModel. + * + * @param item + */ + async queryDetailedInfo(item: MediaTypeModel): Promise> { + return await this.queryDetailedInfoById(item.id, item.dataSource); + } + + /** + * Queries detailed info for an id from an API. + * + * @param id + * @param apiName + */ + async queryDetailedInfoById(id: string, apiName: string): Promise> { + for (const api of this.apis) { + if (api.apiName === apiName) { + const result = await api.getById(id); + + if (!result.ok) { + Logger.warn(result.error); + } + + return result.ok ? ok(result.value) : err(result.error); + } + } + + return err( + toAppError(new Error(`API not found: ${apiName}`), { + kind: AppErrorKind.Validation, + message: `API not found: ${apiName}`, + userMessage: `API not found: ${apiName}`, + context: { apiName, id }, + }), + ); + } + + getApiByName(name: string): APIModel | undefined { + for (const api of this.apis) { + if (api.apiName === name) { + return api; + } + } + + return undefined; + } + + registerAPI(api: APIModel): void { + this.apis.push(api); + } +} diff --git a/src/api/APIModel.ts b/packages/obsidian/src/api/APIModel.ts similarity index 55% rename from src/api/APIModel.ts rename to packages/obsidian/src/api/APIModel.ts index 0919db9..7527e99 100644 --- a/src/api/APIModel.ts +++ b/packages/obsidian/src/api/APIModel.ts @@ -1,6 +1,8 @@ -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { MediaType } from '../utils/MediaType'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; export abstract class APIModel { apiName!: string; @@ -14,9 +16,9 @@ export abstract class APIModel { * * @param title the title to query for */ - abstract searchByTitle(title: string): Promise; + abstract searchByTitle(title: string): Promise>; - abstract getById(id: string): Promise; + abstract getById(id: string): Promise>; abstract getDisabledMediaTypes(): MediaType[]; diff --git a/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts new file mode 100644 index 0000000..889b779 --- /dev/null +++ b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts @@ -0,0 +1,217 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +// sadly no open api schema available + +export class BoardGameGeekAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'BoardGameGeekAPI'; + this.apiDescription = 'A free API for BoardGameGeek things.'; + this.apiUrl = 'https://boardgamegeek.com/xmlapi/'; + this.types = [MediaType.BoardGame]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + headers: { + Authorization: `Bearer ${key}`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + if (fetchData.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const data = fetchData.text; + const response = new window.DOMParser().parseFromString(data, 'text/xml'); + + // console.debug(response); + + 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 ?? undefined; + const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; + + ret.push( + new BoardGameModel({ + dataSource: this.apiName, + id, + title, + englishTitle: title, + year, + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + headers: { + Authorization: `Bearer ${key}`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + if (fetchData.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); + } + + const data = fetchData.text; + const response = new window.DOMParser().parseFromString(data, 'text/xml'); + // console.debug(response); + + const boardgame = response.querySelector('boardgame'); + if (!boardgame) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received invalid data from ${this.apiName}.`, + userMessage: `Received invalid data from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + 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) + .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) + .filter(n => n !== null); + + return ok( + new BoardGameModel({ + title: title ?? undefined, + englishTitle: title ?? undefined, + year: year === '0' ? '' : year, + dataSource: this.apiName, + url: `https://boardgamegeek.com/boardgame/${id}`, + id: id, + + genres: genres, + onlineRating: onlineRating, + complexityRating: complexityRating, + minPlayers: minPlayers, + maxPlayers: maxPlayers, + playtime: playtime, + publishers: publishers, + image: image, + + released: true, + + userData: { + played: false, + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/ComicVineAPI.ts b/packages/obsidian/src/api/apis/ComicVineAPI.ts new file mode 100644 index 0000000..0babc39 --- /dev/null +++ b/packages/obsidian/src/api/apis/ComicVineAPI.ts @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ + +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +// sadly no open api schema available + +export class ComicVineAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'ComicVineAPI'; + this.apiDescription = 'A free API for comic books.'; + this.apiUrl = 'https://comicvine.gamespot.com/api'; + this.types = [MediaType.ComicManga]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const searchUrl = `${this.apiUrl}/search/?api_key=${key}&format=json&resources=volume&query=${encodeURIComponent(title)}`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + // console.debug(fetchData); + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const data = await fetchData.json; + // console.debug(data); + const ret: MediaTypeModel[] = []; + for (const result of data.results) { + ret.push( + new ComicMangaModel({ + title: result.name, + englishTitle: result.name, + year: result.start_year, + dataSource: this.apiName, + id: `4050-${result.id}`, + publishers: result.publisher?.name, + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + + const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${key}&format=json`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + Logger.debug(fetchData); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); + } + + const data = await fetchData.json; + const result = data.results; + + const authors = result.people as + | { + name: string; + }[] + | undefined; + + return ok( + new ComicMangaModel({ + type: MediaType.ComicManga, + title: result.name, + englishTitle: result.name, + alternateTitles: result.aliases, + plot: result.deck, + year: result.start_year, + dataSource: this.apiName, + url: result.site_detail_url, + id: `4050-${result.id}`, + + authors: authors?.map(x => x.name), + chapters: result.count_of_issues, + image: result.image?.original_url, + + released: true, + publishers: result.publisher?.name, + publishedFrom: result.start_year, + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.ComicVineAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/GiantBombAPI.ts b/packages/obsidian/src/api/apis/GiantBombAPI.ts new file mode 100644 index 0000000..2346fbe --- /dev/null +++ b/packages/obsidian/src/api/apis/GiantBombAPI.ts @@ -0,0 +1,169 @@ +// Temporarily removed until they get their API working again. + +// import createClient from 'openapi-fetch'; +// import type MediaDbPlugin from '../../main'; +// import { GameModel } from '../../models/GameModel'; +// import type { MediaTypeModel } from '../../models/MediaTypeModel'; +// import { MediaType } from '../../utils/MediaType'; +// import { obsidianFetch } from '../../utils/Utils'; +// import { APIModel } from '../APIModel'; +// import type { paths } from '../schemas/GiantBomb'; + +// 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`); +// const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.GiantBombKeyId); + +// if (!key) { +// throw Error(`MDB | API key for ${this.apiName} missing.`); +// } + +// const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); +// const response = await client.GET('/games', { +// params: { +// query: { +// api_key: key, +// filter: `name:${title}`, +// format: 'json', +// limit: 20, +// }, +// }, +// fetch: obsidianFetch, +// }); + +// if (response.response.status === 401) { +// throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); +// } +// if (response.response.status === 429) { +// throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); +// } +// if (response.response.status !== 200) { +// throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); +// } + +// const data = response.data?.results; + +// const ret: MediaTypeModel[] = []; +// for (const result of data ?? []) { +// const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; + +// ret.push( +// new GameModel({ +// title: result.name, +// englishTitle: result.name, +// year: year, +// dataSource: this.apiName, +// id: result.guid?.toString(), +// }), +// ); +// } + +// return ret; +// } + +// async getById(id: string): Promise { +// console.log(`MDB | api "${this.apiName}" queried by ID`); +// const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.GiantBombKeyId); + +// if (!key) { +// throw Error(`MDB | API key for ${this.apiName} missing.`); +// } + +// const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); +// const response = await client.GET('/game/{guid}', { +// params: { +// path: { +// guid: id, +// }, +// query: { +// api_key: key, +// format: 'json', +// }, +// }, +// fetch: obsidianFetch, +// }); + +// if (response.response.status === 401) { +// throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); +// } +// if (response.response.status === 429) { +// throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); +// } +// if (response.response.status !== 200) { +// throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); +// } + +// const result = response.data?.results; + +// if (!result) { +// throw Error(`MDB | No results found for ID ${id} in ${this.apiName}.`); +// } + +// console.log(result); + +// // sadly the only OpenAPI definition I could find doesn't have the right types +// const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; +// const developers = result.developers as +// | { +// name: string; +// }[] +// | undefined; +// const publishers = result.publishers as +// | { +// name: string; +// }[] +// | undefined; +// const genres = result.genres as +// | { +// name: string; +// }[] +// | undefined; +// const image = result.image as +// | { +// small_url: string; +// medium_url: string; +// super_url: string; +// } +// | undefined; + +// return new GameModel({ +// type: MediaType.Game, +// title: result.name, +// englishTitle: result.name, +// year: year, +// dataSource: this.apiName, +// url: result.site_detail_url, +// id: result.guid?.toString(), +// developers: developers?.map(x => x.name), +// publishers: publishers?.map(x => x.name), +// genres: genres?.map(x => x.name), +// onlineRating: 0, +// image: image?.super_url, + +// released: true, +// releaseDate: result.original_release_date, + +// userData: { +// played: false, + +// personalRating: 0, +// }, +// }); +// } +// getDisabledMediaTypes(): MediaType[] { +// return this.plugin.settings.GiantBombAPI_disabledMediaTypes; +// } +// } diff --git a/packages/obsidian/src/api/apis/IGDBAPI.ts b/packages/obsidian/src/api/apis/IGDBAPI.ts new file mode 100644 index 0000000..acfbf61 --- /dev/null +++ b/packages/obsidian/src/api/apis/IGDBAPI.ts @@ -0,0 +1,264 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface IGDBCover { + url: string; +} + +interface IGDBGenre { + name: string; +} + +interface IGDBCompany { + name: string; +} + +interface IGDBInvolvedCompany { + company: IGDBCompany; + developer: boolean; + publisher: boolean; +} + +interface IGDBGame { + id: number; + name: string; + cover?: IGDBCover; + first_release_date?: number; + summary?: string; + total_rating?: number; + url?: string; + genres?: IGDBGenre[]; + involved_companies?: IGDBInvolvedCompany[]; +} + +interface TwitchAuthResponse { + access_token: string; + expires_in: number; +} + +export class IGDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + private accessToken: string = ''; + private tokenExpiry: number = 0; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'IGDBAPI'; + this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).'; + this.apiUrl = 'https://api.igdb.com/v4'; + this.types = [MediaType.Game]; + } + + private async getAuthToken(): Promise> { + const currentTime = Date.now(); + if (this.accessToken && currentTime < this.tokenExpiry) return ok(this.accessToken); + + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret); + + if (!clientId || !clientSecret) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Client ID or Client Secret for ${this.apiName} missing.`, + userMessage: `Client ID or Client Secret for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + Logger.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); + const responseResult = await fromPromise( + requestUrl({ + url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, + method: 'POST', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying Twitch auth for ${this.apiName}`, + userMessage: `Network error querying Twitch auth for ${this.apiName}`, + context: { apiName: this.apiName }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Auth failed for ${this.apiName}. Check Credentials.`, + userMessage: `Auth failed for ${this.apiName}. Check Credentials.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as TwitchAuthResponse; + this.accessToken = data.access_token; + this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; + return ok(this.accessToken); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + if (!clientId) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Client ID for ${this.apiName} missing.`, + userMessage: `Client ID for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + const tokenResult = await this.getAuthToken(); + if (!tokenResult.ok) { + return err(tokenResult.error); + } + const token = tokenResult.value; + const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, + body: queryBody, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as IGDBGame[]; + return ok( + data.map(result => { + const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; + return new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: year, + dataSource: this.apiName, + id: result.id.toString(), + image: image, + }); + }), + ); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + if (!clientId) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Client ID for ${this.apiName} missing.`, + userMessage: `Client ID for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + const tokenResult = await this.getAuthToken(); + if (!tokenResult.ok) { + return err(tokenResult.error); + } + const token = tokenResult.value; + const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, + body: queryBody, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status, id }, + }); + } + + const data = response.json as IGDBGame[]; + if (!data || data.length === 0) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No result found for ID ${id}`, + userMessage: `No result found for ID ${id}`, + context: { apiName: this.apiName, id }, + }); + } + const result = data[0]; + + const developers: string[] = []; + const publishers: string[] = []; + result.involved_companies?.forEach(c => { + if (c.developer) developers.push(c.company.name); + if (c.publisher) publishers.push(c.company.name); + }); + const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; + + return ok( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', + dataSource: this.apiName, + url: result.url, + id: result.id.toString(), + developers: developers, + publishers: publishers, + genres: result.genres?.map(g => g.name) ?? [], + onlineRating: result.total_rating, + image: image, + released: true, + releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', + userData: { played: false, personalRating: 0 }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? []; + } +} diff --git a/packages/obsidian/src/api/apis/MALAPI.ts b/packages/obsidian/src/api/apis/MALAPI.ts new file mode 100644 index 0000000..11d1c02 --- /dev/null +++ b/packages/obsidian/src/api/apis/MALAPI.ts @@ -0,0 +1,260 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; +import { isTruthy, obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/MALAPI'; + +export class MALAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MALAPI'; + this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; + this.apiUrl = 'https://jikan.moe/'; + this.types = [MediaType.Movie, MediaType.Series]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + this.typeMappings.set('special', 'special'); + this.typeMappings.set('tv', 'series'); + this.typeMappings.set('ova', 'ova'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/anime', { + params: { + query: { + q: title, + limit: 20, + sfw: this.plugin.settings.sfwFilter ? true : false, + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data?.data; + + const ret: MediaTypeModel[] = []; + + for (const result of data ?? []) { + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString() ?? ''; + const id = result.mal_id?.toString(); + + if (type === undefined) { + ret.push( + new MovieModel({ + subType: '', + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } + if (type === 'movie' || type === 'special') { + ret.push( + new MovieModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } else if (type === 'series' || type === 'ova') { + ret.push( + new SeriesModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/anime/{id}/full', { + params: { + path: { + id: id as unknown as number, // This is fine + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const result = response.data?.data; + + if (result === undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data found for ID ${id} in ${this.apiName}.`, + userMessage: `No data found for ID ${id} in ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString(); + const new_id = result.mal_id?.toString(); + + if (type === undefined) { + return ok( + new MovieModel({ + subType: undefined, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + duration: result.duration, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + if (type === 'movie' || type === 'special') { + return ok( + new MovieModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + duration: result.duration, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } else if (type === 'series' || type === 'ova') { + return ok( + new SeriesModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + episodes: result.episodes, + duration: result.duration, + onlineRating: result.score, + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat), + airing: result.airing, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + return err({ + kind: AppErrorKind.Unexpected, + message: `MDB | Unknown media type for id ${id}`, + userMessage: `Unknown media type for id ${id}`, + context: { apiName: this.apiName, id }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MALAPI_disabledMediaTypes; + } +} diff --git a/src/api/apis/MALAPIManga.ts b/packages/obsidian/src/api/apis/MALAPIManga.ts similarity index 52% rename from src/api/apis/MALAPIManga.ts rename to packages/obsidian/src/api/apis/MALAPIManga.ts index 2d39a4f..424449b 100644 --- a/src/api/apis/MALAPIManga.ts +++ b/packages/obsidian/src/api/apis/MALAPIManga.ts @@ -1,11 +1,16 @@ import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import { ComicMangaModel } from '../../models/ComicMangaModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { isTruthy, obsidianFetch } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/MALAPI'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; +import { isTruthy, obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/MALAPI'; export class MALAPIManga extends APIModel { plugin: MediaDbPlugin; @@ -30,8 +35,8 @@ export class MALAPIManga extends APIModel { this.typeMappings.set('novel', 'novel'); } - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -47,7 +52,12 @@ export class MALAPIManga extends APIModel { }); if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); } const data = response.data?.data; @@ -93,11 +103,11 @@ export class MALAPIManga extends APIModel { ); } - return ret; + return ok(ret); } - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -111,13 +121,23 @@ export class MALAPIManga extends APIModel { }); if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); } const result = response.data?.data; if (!result) { - throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data found for ID ${id} in ${this.apiName}.`, + userMessage: `No data found for ID ${id} in ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); } const resType = result.type?.toLowerCase(); @@ -125,35 +145,37 @@ export class MALAPIManga extends APIModel { const year = result.published?.prop?.from?.year?.toString() ?? ''; const new_id = result.mal_id?.toString(); - return new ComicMangaModel({ - subType: type, - title: result.title, - plot: result.synopsis ?? undefined, - englishTitle: result.title_english ?? result.title, - alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, + return ok( + new ComicMangaModel({ + subType: type, + title: result.title, + plot: result.synopsis ?? undefined, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, - genres: result.genres?.map(x => x.name).filter(isTruthy), - authors: result.authors?.map(x => x.name).filter(isTruthy), - chapters: result.chapters, - volumes: result.volumes, - onlineRating: result.score, - image: result.images?.jpg?.image_url, + genres: result.genres?.map(x => x.name).filter(isTruthy), + authors: result.authors?.map(x => x.name).filter(isTruthy), + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score, + image: result.images?.jpg?.image_url, - released: true, - publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), - publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), - status: result.status, + released: true, + publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), + publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), + status: result.status, - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); } getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.MALAPIManga_disabledMediaTypes; diff --git a/packages/obsidian/src/api/apis/MusicBrainzAPI.ts b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts new file mode 100644 index 0000000..a61d7d0 --- /dev/null +++ b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts @@ -0,0 +1,327 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from 'packages/obsidian/src/utils/Utils'; + +// sadly no open api schema available + +interface Tag { + name: string; + count: number; +} +interface Genre { + name: string; + count: number; + id: string; + disambiguation: string; +} +interface Release { + id: string; + 'status-id': string; + title: string; + status: string; +} + +interface ArtistCredit { + name: string; + artist: { + tags: Tag[]; + type: string; + id: string; + name: string; + 'short-name': string; + country: string; + }; +} + +interface SearchResponse { + id: string; + 'type-id': string; + score: number; + 'primary-type-id': string; + 'artists-credit-id': string; + count: number; + title: string; + 'first-release-date': string; + 'primary-type': string; + 'artist-credit': ArtistCredit[]; + releases: Release[]; + tags: Tag[]; +} + +interface IdResponse { + id: string; + tags: Tag[]; + 'primary-type-id': string; + 'artist-credit': ArtistCredit[]; + title: string; + genres: Genre[]; + 'first-release-date': string; + releases: Release[]; + 'primary-type': string; + rating: { + value: number; + 'votes-count': number; + }; +} + +interface MediaResponse { + media: { + 'track-count': number; + tracks: { + 'artist-credit': ArtistCredit[]; + length: number | null; + number: string; + position: number; + title: string; + recording: { + length: number; + title: string; + }; + }[]; + }[]; + 'text-representation': { + language: string; + script: string; + }; +} + +export class MusicBrainzAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MusicBrainz API'; + this.apiDescription = 'Free API for music albums.'; + this.apiUrl = 'https://musicbrainz.org/'; + this.types = [MediaType.MusicRelease]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; + + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + // console.debug(fetchData); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const data = (await fetchData.json) as { + 'release-groups': SearchResponse[]; + }; + // console.debug(data); + const ret: MediaTypeModel[] = []; + + for (const result of data['release-groups']) { + ret.push( + new MusicReleaseModel({ + type: 'musicRelease', + title: result.title, + englishTitle: result.title, + year: new Date(result['first-release-date']).getFullYear().toString(), + releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: 'https://musicbrainz.org/release-group/' + result.id, + id: result.id, + image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', + + artists: result['artist-credit'].map(a => a.name), + subType: result['primary-type'], + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + // Fetch release group + const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`; + const groupResponseResult = await fromPromise( + requestUrl({ + url: groupUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!groupResponseResult.ok) { + return err(groupResponseResult.error); + } + const groupResponse = groupResponseResult.value; + + if (groupResponse.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${groupResponse.status} from ${this.apiName}.`, + userMessage: `Received status code ${groupResponse.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: groupResponse.status, id }, + }); + } + + const result = (await groupResponse.json) as IdResponse; + + // Get ID of the first release + const firstRelease = result.releases?.[0]; + if (!firstRelease) { + return err({ + kind: AppErrorKind.Api, + message: 'MDB | No releases found in release group.', + userMessage: 'No releases found in release group.', + context: { apiName: this.apiName, id }, + }); + } + + // Fetch recordings for the first release + const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; + Logger.log(`MDB | Fetching release recordings from: ${releaseUrl}`); + + const releaseResponseResult = await fromPromise( + requestUrl({ + url: releaseUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id, releaseId: firstRelease.id }, + }), + ); + if (!releaseResponseResult.ok) { + return err(releaseResponseResult.error); + } + const releaseResponse = releaseResponseResult.value; + + if (releaseResponse.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`, + userMessage: `Received status code ${releaseResponse.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: releaseResponse.status, id, releaseId: firstRelease.id }, + }); + } + + const releaseData = (await releaseResponse.json) as MediaResponse; + const tracks = extractTracksFromMedia(releaseData.media); + + // Calculate total album length for the first release + const totalrawLength = + releaseData.media[0]?.tracks.reduce((sum, track) => { + const len = track.length ?? track.recording?.length; + return typeof len === 'number' && !isNaN(len) ? sum + len : sum; + }, 0) ?? 0; + const albumLengthCalc = millisecondsToMinutes(totalrawLength); + + Logger.debug(releaseData); + + return ok( + new MusicReleaseModel({ + type: 'musicRelease', + title: result.title, + englishTitle: result.title, + year: new Date(result['first-release-date']).getFullYear().toString(), + releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: 'https://musicbrainz.org/release-group/' + result.id, + id: result.id, + image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', + + artists: result['artist-credit'].map(a => a.name), + language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown', + genres: result.genres.map(g => g.name), + subType: result['primary-type'], + albumDuration: albumLengthCalc, + trackCount: releaseData.media[0]?.['track-count'] ?? 0, + tracks: tracks, + rating: result.rating.value * 2, + + userData: { + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; + } +} + +function extractTracksFromMedia(media: MediaResponse['media']): { + number: number; + title: string; + duration: string; + featuredArtists: string[]; +}[] { + if (!media || media.length === 0 || !media[0].tracks) return []; + + return media[0].tracks.map((track, index) => { + const title = track.title ?? track.recording?.title ?? 'Unknown Title'; + const rawLength = track.length ?? track.recording?.length; + const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; + const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; + + return { + number: index + 1, + title, + duration, + featuredArtists, + }; + }); +} + +function millisecondsToMinutes(milliseconds: number): string { + const minutes = Math.floor(milliseconds / 60000); + const seconds = Math.floor((milliseconds % 60000) / 1000); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} diff --git a/packages/obsidian/src/api/apis/OMDbAPI.ts b/packages/obsidian/src/api/apis/OMDbAPI.ts new file mode 100644 index 0000000..2439378 --- /dev/null +++ b/packages/obsidian/src/api/apis/OMDbAPI.ts @@ -0,0 +1,388 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface ErrorResponse { + Response: 'False'; + Error: string; +} + +type SearchResponse = + | { + Response: 'True'; + totalResults: string; + Search: { + Title: string; + Year: string; + Poster: string; + imdbID: string; + Type: string; + }[]; + } + | ErrorResponse; + +type IdResponse = + | { + Response: 'True'; + Title: string; + Year: string; + Rated: string; + Released: string; + Runtime: string; + Genre: string; + Director: string; + Writer: string; + Actors: string; + Plot: string; + Language: string; + Country: string; + Awards: string; + Poster: string; + Metascore: string; + imdbRating: string; + imdbVotes: string; + imdbID: string; + Type: string; + DVD: string; + BoxOffice: string; + Production: string; + Website: string; + } + | ErrorResponse; + +export class OMDbAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'DD MMM YYYY'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'OMDbAPI'; + this.apiDescription = 'A free API for Movies, Series and Games.'; + this.apiUrl = 'https://www.omdbapi.com/'; + this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + this.typeMappings.set('series', 'series'); + this.typeMappings.set('game', 'game'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${key}`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as SearchResponse | undefined; + + if (!data) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName }, + }); + } + + if (data.Response === 'False') { + if (data.Error === 'Movie not found!') { + return ok([]); + } + + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received error from ${this.apiName}: ${data.Error}`, + userMessage: `${data.Error}`, + context: { apiName: this.apiName }, + }); + } + if (!data.Search) { + return ok([]); + } + + // console.debug(data.Search); + + const ret: MediaTypeModel[] = []; + + for (const result of data.Search) { + const type = this.typeMappings.get(result.Type.toLowerCase()); + if (type === undefined) { + continue; + } + if (type === 'movie') { + ret.push( + new MovieModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } else if (type === 'series') { + ret.push( + new SeriesModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } else if (type === 'game') { + ret.push( + new GameModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${key}`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status, id }, + }); + } + + const result = response.json as IdResponse | undefined; + + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + if (result.Response === 'False') { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received error from ${this.apiName}: ${result.Error}`, + userMessage: `${result.Error}`, + context: { apiName: this.apiName, id }, + }); + } + + const type = this.typeMappings.get(result.Type.toLowerCase()); + if (type === undefined) { + return err({ + kind: AppErrorKind.Validation, + message: `${result.Type.toLowerCase()} is an unsupported type.`, + userMessage: `${result.Type.toLowerCase()} is an unsupported type.`, + context: { apiName: this.apiName, id, type: result.Type }, + }); + } + + if (type === 'movie') { + return ok( + new MovieModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + plot: result.Plot, + genres: result.Genre?.split(', '), + director: result.Director?.split(', '), + writer: result.Writer?.split(', '), + duration: result.Runtime, + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', '), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + country: result.Country?.split(', '), + boxOffice: result.BoxOffice, + ageRating: result.Rated, + premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } else if (type === 'series') { + return ok( + new SeriesModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + plot: result.Plot, + genres: result.Genre?.split(', '), + writer: result.Writer?.split(', '), + studio: [], + episodes: 0, + duration: result.Runtime, + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', '), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + country: result.Country?.split(', '), + ageRating: result.Rated, + airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } else if (type === 'game') { + return ok( + new GameModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + genres: result.Genre?.split(', '), + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + played: false, + personalRating: 0, + }, + }), + ); + } + + return err({ + kind: AppErrorKind.Unexpected, + message: `MDB | Unknown media type for id ${id}`, + userMessage: `Unknown media type for id ${id}`, + context: { apiName: this.apiName, id }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.OMDbAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts new file mode 100644 index 0000000..6a52bdb --- /dev/null +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -0,0 +1,214 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { BookModel } from 'packages/obsidian/src/models/BookModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/OpenLibrary'; + +interface SearchResponse { + editions: { + docs: { + key?: string; + title?: string; + cover_i?: number; + isbn?: string[]; + }[]; + }; + cover_i?: number; + has_fulltext?: boolean; + edition_count?: number; + title?: string; + author_name?: string[]; + first_publish_year?: number; + key: string; + description?: string; + + number_of_pages_median?: number; + isbn?: string[]; + ratings_average?: number; +} + +export class OpenLibraryAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'OpenLibraryAPI'; + this.apiDescription = 'A free API for books'; + this.apiUrl = 'https://openlibrary.org/'; + this.types = [MediaType.Book]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + + const responseResult = await fromPromise( + client.GET('/search.json', { + params: { + query: { + q: title, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data as { + docs: SearchResponse[]; + }; + + // console.debug(data); + + const ret: MediaTypeModel[] = []; + + for (const result of data.docs) { + ret.push( + new BookModel({ + title: result.title, + englishTitle: result.title, + year: result.first_publish_year?.toString() ?? 'unknown', + dataSource: this.apiName, + id: result.key, + author: result.author_name?.join(', '), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + + const responseResult = await fromPromise( + client.GET('/search.json', { + params: { + query: { + q: `${id}`, + fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const data = response.data as { + docs: SearchResponse[]; + q?: string; + }; + + const result = data.docs?.[0]; + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data found for ID ${id} in ${this.apiName}.`, + userMessage: `No data found for ID ${id}.`, + context: { apiName: this.apiName, id }, + }); + } + + let key = result.key; + let title = result.title; + let cover_i = result.cover_i; + let isbnArr = result.isbn; + + // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present + const q = data.q ?? ''; + if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { + const edition = result.editions.docs[0]; + key = edition.key ?? key; + title = edition.title ?? title; + cover_i = edition.cover_i ?? cover_i; + isbnArr = edition.isbn ?? isbnArr; + } + + const pages = Number(result.number_of_pages_median); + const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); + const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); + + return ok( + new BookModel({ + title: title, + year: result.first_publish_year?.toString() ?? 'unknown', + dataSource: this.apiName, + url: `https://openlibrary.org` + key, + id: key, + isbn: Number.isNaN(isbn) ? undefined : isbn, + isbn13: Number.isNaN(isbn13) ? undefined : isbn13, + englishTitle: title, + + author: result.author_name?.join(', '), + plot: result.description ?? undefined, + pages: Number.isNaN(pages) ? undefined : pages, + onlineRating: result.ratings_average, + image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, + + released: true, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/RAWGAPI.ts b/packages/obsidian/src/api/apis/RAWGAPI.ts new file mode 100644 index 0000000..21d603b --- /dev/null +++ b/packages/obsidian/src/api/apis/RAWGAPI.ts @@ -0,0 +1,161 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface RAWGGame { + id: number; + name: string; + released?: string; + background_image?: string; + name_original?: string; + website?: string; + slug?: string; + metacritic?: number; + developers?: { name: string }[]; + publishers?: { name: string }[]; + genres?: { name: string }[]; +} + +interface RAWGSearchResponse { + results: RAWGGame[]; +} + +export class RAWGAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'RAWGAPI'; + this.apiDescription = 'A large open video game database.'; + this.apiUrl = 'https://api.rawg.io/api'; + this.types = [MediaType.Game]; + } + + async searchByTitle(title: string): Promise> { + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Error ${response.status} from ${this.apiName}.`, + userMessage: `Error ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as RAWGSearchResponse; + return ok( + data.results.map( + result => + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: result.released ? new Date(result.released).getFullYear().toString() : '', + dataSource: this.apiName, + id: result.id.toString(), + image: result.background_image, + }), + ), + ); + } + + async getById(id: string): Promise> { + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games/${id}?key=${key}`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Error ${response.status} from ${this.apiName}.`, + userMessage: `Error ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status, id }, + }); + } + + const result = response.json as RAWGGame; + return ok( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name_original ?? result.name, + year: result.released ? new Date(result.released).getFullYear().toString() : '', + dataSource: this.apiName, + url: result.website ?? `https://rawg.io/games/${result.slug}`, + id: result.id.toString(), + developers: result.developers?.map(d => d.name) ?? [], + publishers: result.publishers?.map(p => p.name) ?? [], + genres: result.genres?.map(g => g.name) ?? [], + onlineRating: result.metacritic, + image: result.background_image, + released: result.released != null, + releaseDate: result.released, + userData: { played: false, personalRating: 0 }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? []; + } +} diff --git a/src/api/apis/SteamAPI.ts b/packages/obsidian/src/api/apis/SteamAPI.ts similarity index 50% rename from src/api/apis/SteamAPI.ts rename to packages/obsidian/src/api/apis/SteamAPI.ts index b8e72b2..8a47efa 100644 --- a/src/api/apis/SteamAPI.ts +++ b/packages/obsidian/src/api/apis/SteamAPI.ts @@ -1,10 +1,15 @@ 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 { imageUrlExists } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { imageUrlExists } from 'packages/obsidian/src/utils/Utils'; interface SearchResponse { appid: string; @@ -143,16 +148,34 @@ export class SteamAPI extends APIModel { this.typeMappings.set('game', 'game'); } - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); } const data = (await fetchData.json) as SearchResponse[]; @@ -174,19 +197,37 @@ export class SteamAPI extends APIModel { ); } - return ret; + return ok(ret); } - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; - const fetchData = await requestUrl({ - url: searchUrl, - }); + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); } // console.debug(await fetchData.json); @@ -200,14 +241,27 @@ export class SteamAPI extends APIModel { } } if (!result) { - throw Error(`MDB | API returned invalid data.`); + return err({ + kind: AppErrorKind.Api, + message: 'MDB | API returned invalid data.', + userMessage: 'Steam returned invalid data.', + context: { apiName: this.apiName, id }, + }); } // console.debug(result); // Check if a poster version of the image exists, else use the header image const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`; - const exists = await imageUrlExists(imageUrl); + const existsResult = await fromPromise(imageUrlExists(imageUrl), cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Failed to validate image URL for ${this.apiName}`, + userMessage: `Failed to validate image URL for ${this.apiName}`, + context: { apiName: this.apiName, id, imageUrl }, + }), + ); + const exists = existsResult.ok ? existsResult.value : false; let finalimageurl; if (exists) { finalimageurl = imageUrl; @@ -215,29 +269,31 @@ export class SteamAPI extends APIModel { finalimageurl = result.header_image ?? ''; } - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: new Date(result.release_date.date).getFullYear().toString(), - dataSource: this.apiName, - url: `https://store.steampowered.com/app/${result.steam_appid}`, - id: result.steam_appid.toString(), + return ok( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: new Date(result.release_date.date).getFullYear().toString(), + dataSource: this.apiName, + url: `https://store.steampowered.com/app/${result.steam_appid}`, + id: result.steam_appid.toString(), - developers: result.developers, - publishers: result.publishers, - genres: result.genres?.map(x => x.description), - onlineRating: result.metacritic?.score, - image: finalimageurl, + developers: result.developers, + publishers: result.publishers, + genres: result.genres?.map(x => x.description), + onlineRating: result.metacritic?.score, + image: finalimageurl, - released: !result.release_date?.coming_soon, - releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), + released: !result.release_date?.coming_soon, + releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), - userData: { - played: false, - personalRating: 0, - }, - }); + userData: { + played: false, + personalRating: 0, + }, + }), + ); } getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.SteamAPI_disabledMediaTypes; diff --git a/packages/obsidian/src/api/apis/TMDBMovieAPI.ts b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts new file mode 100644 index 0000000..9f2dbf1 --- /dev/null +++ b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts @@ -0,0 +1,265 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/TMDB'; + +interface TMDBCreditMember { + name?: string | null; + job?: string | null; +} + +interface TMDBCreditsResponse { + credits?: { + cast?: TMDBCreditMember[]; + crew?: TMDBCreditMember[]; + }; +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.length > 0; +} + +function getTopCastNames(credits: TMDBCreditsResponse['credits'], size: number): string[] { + return (credits?.cast ?? []) + .map(c => c.name) + .filter(isNonEmptyString) + .slice(0, size); +} + +function getCrewNamesByJob(credits: TMDBCreditsResponse['credits'], job: string): string[] { + return (credits?.crew ?? []) + .filter(c => c.job === job) + .map(c => c.name) + .filter(isNonEmptyString); +} + +export class TMDBMovieAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBMovieAPI'; + this.apiDescription = 'A community built Movie DB.'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Movie]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/search/movie', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data; + + if (!data) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName }, + }); + } + + if (data.total_results === 0 || !data.results) { + return ok([]); + } + + // console.debug(data.results); + + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + new MovieModel({ + type: 'movie', + title: result.original_title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id.toString(), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/movie/{movie_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { movie_id: parseInt(id) }, + query: { + append_to_response: 'credits', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const result = response.data; + + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + // console.debug(result); + const credits = (result as TMDBCreditsResponse).credits; + + return ok( + new MovieModel({ + type: 'movie', + title: result.title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/movie/${result.id}`, + id: result.id.toString(), + + plot: result.overview ?? '', + genres: result.genres?.map(g => g.name).filter(isNonEmptyString) ?? [], + writer: getCrewNamesByJob(credits, 'Screenplay'), + director: getCrewNamesByJob(credits, 'Director'), + studio: result.production_companies?.map(s => s.name).filter(isNonEmptyString) ?? [], + + duration: result.runtime?.toString() ?? 'unknown', + onlineRating: result.vote_average, + actors: getTopCastNames(credits, 5), + image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, + + released: ['Released'].includes(result.status!), + streamingServices: [], + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts new file mode 100644 index 0000000..867879e --- /dev/null +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -0,0 +1,438 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/TMDB'; + +interface NamedEntity { + name?: string | null; +} + +interface CastMember { + name?: string | null; +} + +interface CreditsLike { + cast?: CastMember[] | null; +} + +function extractNames(items: (NamedEntity | null | undefined)[] | null | undefined): string[] { + if (!Array.isArray(items)) { + return []; + } + + return items.map(item => item?.name?.trim() ?? '').filter(name => name.length > 0); +} + +function getTopActorNames(credits: CreditsLike | null | undefined, limit: number = 5): string[] { + if (!credits || !Array.isArray(credits.cast)) { + return []; + } + + return credits.cast + .map(member => { + const name = member?.name; + return typeof name === 'string' ? name : ''; + }) + .filter(name => name.length > 0) + .slice(0, limit); +} + +export class TMDBSeasonAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBSeasonAPI'; + this.apiDescription = 'A community built Series DB (seasons).'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Season]; + this.typeMappings = new Map(); + this.typeMappings.set('tv', 'season'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const searchResponseResult = await fromPromise( + client.GET('/3/search/tv', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!searchResponseResult.ok) { + return err(searchResponseResult.error); + } + const searchResponse = searchResponseResult.value; + + if (searchResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + + if (searchResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${searchResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: searchResponse.response.status }, + }); + } + + const searchData = searchResponse.data; + + if (!searchData?.results || searchData.total_results === 0) { + return ok([]); + } + + const topResults = searchData.results.slice(0, 20); + + const items = await Promise.all( + topResults.map(async result => { + let totalSeasons = 0; + if (typeof result.id === 'number') { + try { + const detailsResponse = await client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: result.id }, + }, + fetch: obsidianFetch, + }); + + if (detailsResponse.response.status === 200 && Array.isArray(detailsResponse.data?.seasons)) { + totalSeasons = detailsResponse.data.seasons.length; + } + } catch { + // Ignore detail errors and use 0 as fallback. + } + } + + return new SeasonModel({ + title: `${result.name ?? result.original_name ?? ''}`, + englishTitle: result.name ?? result.original_name ?? '', + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id?.toString() ?? '', + seasonTitle: result.name ?? result.original_name ?? '', + seasonNumber: totalSeasons, + }); + }), + ); + + return ok(items); + } + + // Fetch all seasons for a given series + async getSeasonsForSeries(tvId: string): Promise> { + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, tvId }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const seriesResponseResult = await fromPromise( + client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: Number.parseInt(tvId, 10) }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, tvId }, + }), + ); + + if (!seriesResponseResult.ok) { + return err(seriesResponseResult.error); + } + const seriesResponse = seriesResponseResult.value; + + if (seriesResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, tvId }, + }); + } + + if (seriesResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: seriesResponse.response.status, tvId }, + }); + } + + const seriesData = seriesResponse.data; + const seriesName = seriesData?.name ?? ''; + + const ret: SeasonModel[] = []; + + if (Array.isArray(seriesData?.seasons)) { + for (const season of seriesData.seasons) { + const seasonNumber = season.season_number ?? 0; + const titleText = `${seriesName} - Season ${seasonNumber}`; + + ret.push( + new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: `${tvId}/season/${seasonNumber}`, + seasonTitle: season.name ?? titleText, + seasonNumber: seasonNumber, + }), + ); + } + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + + // Expect season ids like "12345/season/2" + const m = /^(\d+)\/season\/(\d+)$/.exec(id); + if (!m) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Invalid season id "${id}". Expected format "/season/".`, + userMessage: `Invalid season id "${id}".`, + context: { apiName: this.apiName, id }, + }); + } + + const tvId = Number.parseInt(m[1], 10); + const seasonNumber = Number.parseInt(m[2], 10); + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + + // Fetch season details + const seasonResponseResult = await fromPromise( + client.GET('/3/tv/{series_id}/season/{season_number}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { + series_id: tvId, + season_number: seasonNumber, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!seasonResponseResult.ok) { + return err(seasonResponseResult.error); + } + const seasonResponse = seasonResponseResult.value; + + if (seasonResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, id }, + }); + } + + if (seasonResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${seasonResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: seasonResponse.response.status, id }, + }); + } + + const seasonData = seasonResponse.data; + if (!seasonData) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + // Fetch parent series to build consistent titles and inherit fields + const seriesResponseResult = await fromPromise( + client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: tvId }, + query: { + append_to_response: 'credits', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!seriesResponseResult.ok) { + return err(seriesResponseResult.error); + } + const seriesResponse = seriesResponseResult.value; + + if (seriesResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, id }, + }); + } + + if (seriesResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: seriesResponse.response.status, id }, + }); + } + + const seriesData = seriesResponse.data; + + if (!seriesData) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + const seriesName = seriesData?.name ?? ''; + const airDate = seasonData.air_date ?? ''; + const titleText = `${seriesName} - Season ${seasonData.season_number}`; + + // Get airedTo as the air_date of the last episode, if available + let airedTo = 'unknown'; + if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) { + const lastEp = seasonData.episodes[seasonData.episodes.length - 1]; + if (lastEp?.air_date) airedTo = lastEp.air_date; + } + const formattedAiredTo = airedTo === 'unknown' ? 'unknown' : (this.plugin.dateFormatter.format(airedTo, this.apiDateFormat) ?? airedTo); + + return ok( + new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/tv/${tvId.toString()}/season/${seasonData.season_number}`, + id: `${tvId.toString()}/season/${seasonData.season_number}`, + seasonTitle: seasonData.name ?? titleText, + seasonNumber: seasonData.season_number ?? seasonNumber, + episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, + airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', + airedTo: formattedAiredTo, + plot: seasonData.overview ?? '', + image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', + genres: extractNames(seriesData.genres), + writer: extractNames(seriesData.created_by), + studio: extractNames(seriesData.production_companies), + duration: seriesData.episode_run_time?.[0]?.toString() ?? '', + onlineRating: seasonData.vote_average ?? 0, + actors: getTopActorNames((seriesData as { credits?: CreditsLike }).credits), + released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), + streamingServices: [], + airing: ['Returning Series'].includes(seriesData.status ?? ''), + userData: { watched: false, lastWatched: '', personalRating: 0 }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return []; + } +} diff --git a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts new file mode 100644 index 0000000..40f68d7 --- /dev/null +++ b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts @@ -0,0 +1,255 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/TMDB'; + +interface TMDBCreditMember { + name?: string | null; +} + +interface TMDBCreditsResponse { + credits?: { + cast?: TMDBCreditMember[]; + }; +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.length > 0; +} + +function getTopCastNames(credits: TMDBCreditsResponse['credits'], size: number): string[] { + return (credits?.cast ?? []) + .map(c => c.name) + .filter(isNonEmptyString) + .slice(0, size); +} + +export class TMDBSeriesAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBSeriesAPI'; + this.apiDescription = 'A community built Series DB.'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Series]; + this.typeMappings = new Map(); + this.typeMappings.set('tv', 'series'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/search/tv', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data; + + if (!data) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName }, + }); + } + + if (data.total_results === 0 || !data.results) { + return ok([]); + } + + // console.debug(data.results); + + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + new SeriesModel({ + type: 'series', + title: result.original_name, + englishTitle: result.name, + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id.toString(), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: parseInt(id) }, + query: { + append_to_response: 'credits', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, id }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const result = response.data; + + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + // console.debug(result); + const credits = (result as TMDBCreditsResponse).credits; + + return ok( + new SeriesModel({ + type: 'series', + title: result.original_name, + englishTitle: result.name, + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/tv/${result.id}`, + id: result.id.toString(), + + plot: result.overview ?? '', + genres: result.genres?.map(g => g.name).filter(isNonEmptyString) ?? [], + writer: result.created_by?.map(c => c.name).filter(isNonEmptyString) ?? [], + studio: result.production_companies?.map(s => s.name).filter(isNonEmptyString) ?? [], + episodes: result.number_of_episodes, + duration: result.episode_run_time?.[0]?.toString() ?? 'unknown', + onlineRating: result.vote_average, + actors: getTopCastNames(credits, 5), + image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null, + + released: ['Returning Series', 'Cancelled', 'Ended'].includes(result.status!), + streamingServices: [], + airing: ['Returning Series'].includes(result.status!), + airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown', + airedTo: ['Returning Series'].includes(result.status!) ? 'unknown' : (this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown'), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/VNDBAPI.ts b/packages/obsidian/src/api/apis/VNDBAPI.ts new file mode 100644 index 0000000..eae27de --- /dev/null +++ b/packages/obsidian/src/api/apis/VNDBAPI.ts @@ -0,0 +1,334 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +enum VNDevStatus { + Finished, + InDevelopment, + Cancelled, +} + +enum TagSpoiler { + None, + Minor, + Major, +} + +enum TagCategory { + Content = 'cont', + Sexual = 'ero', + Technical = 'tech', +} + +/** + * A partial `POST /vn` response payload; desired fields should be listed in the request body. + */ +interface VNJSONResponse { + more: boolean; + results: [ + { + id: string; + title: string; + titles: [ + { + title: string; + lang: string; + }, + ]; + devstatus: VNDevStatus; + released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents + image: { + url: string; + sexual: number; + } | null; + rating: number | null; + tags: [ + { + id: string; + name: string; + category: TagCategory; + rating: number; + spoiler: TagSpoiler; + }, + ]; + developers: [ + { + id: string; + name: string; + }, + ]; + }, + ]; +} + +/** + * A partial `POST /release` response payload; desired fields should be listed in the request body. + */ +interface ReleaseJSONResponse { + more: boolean; + results: [ + { + id: string; + producers: [ + { + id: string; + name: string; + developer: boolean; + publisher: boolean; + }, + ]; + }, + ]; +} + +export class VNDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'VNDB API'; + this.apiDescription = 'A free API for visual novels.'; + this.apiUrl = 'https://api.vndb.org/kana'; + this.types = [MediaType.Game]; + } + + /** + * Make a `POST` request to the VNDB API. + * @param endpoint The API endpoint to query. E.g. "/vn". + * @param body A JSON object defining the query, following the VNDB API structure. + * @returns A JSON object representing the query response. + * @see {@link https://api.vndb.org/kana#api-structure} + */ + private async postQuery(endpoint: string, body: string): Promise> { + const fetchDataResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}${endpoint}`, + method: 'POST', + contentType: 'application/json', + body: body, + throw: false, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, endpoint }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + if (fetchData.status !== 200) { + switch (fetchData.status) { + case 400: + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Invalid request body or query [${fetchData.text}].`, + userMessage: 'Invalid VNDB request.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 404: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | Invalid API path or HTTP method.', + userMessage: 'VNDB endpoint not found.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 429: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | VNDB throttled the request.', + userMessage: 'VNDB throttled the request. Please try again later.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 500: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | VNDB server error.', + userMessage: 'VNDB server error.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 502: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | VNDB server is down.', + userMessage: 'VNDB server is down.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + default: + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + } + } + + return ok(fetchData.json); + } + + /** + * Make a `POST` request to the `/vn` endpoint. + * Queries visual novel entries. + * @see {@link https://api.vndb.org/kana#post-vn} + */ + private async postVNQuery(body: string): Promise> { + const result = await this.postQuery('/vn', body); + return result.ok ? ok(result.value as VNJSONResponse) : err(result.error); + } + + /** + * Make a `POST` request to the `/release` endpoint. + * Queries release entries. + * @see {@link https://api.vndb.org/kana#post-release} + */ + private async postReleaseQuery(body: string): Promise> { + const result = await this.postQuery('/release', body); + return result.ok ? ok(result.value as ReleaseJSONResponse) : err(result.error); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + /* SFW Filter: has ANY official&&complete&&standalone&&SFW release + OR has NO official&&standalone&&NSFW release + OR has the `In-game Sexual Content Toggle` (g2708) tag */ + // prettier-ignore + const vnDataResult = await this.postVNQuery(`{ + "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : + `, ["or" + , ["release", "=", ["and" + , ["official", "=", "1"] + , ["rtype", "=", "complete"] + , ["patch", "!=", "1"] + , ["has_ero", "!=", "1"] + ]] + , ["release", "!=", ["and" + , ["official", "=", "1"] + , ["patch", "!=", "1"] + , ["has_ero", "=", "1"] + ]] + , ["tag", "=", "g2708"] + ]`} + , ["search", "=", "${title}"] + ], + "fields": "title, titles{title, lang}, released", + "sort": "searchrank", + "results": 20 + }`); + if (!vnDataResult.ok) { + return err(vnDataResult.error); + } + const vnData = vnDataResult.value; + + const ret: MediaTypeModel[] = []; + for (const vn of vnData.results) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', + dataSource: this.apiName, + id: vn.id, + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const vnDataResult = await this.postVNQuery(`{ + "filters": ["id", "=", "${id}"], + "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" + }`); + if (!vnDataResult.ok) { + return err(vnDataResult.error); + } + const vnData = vnDataResult.value; + + if (vnData.results.length !== 1) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Expected 1 result from query, got ${vnData.results.length}.`, + userMessage: 'Unexpected VNDB response.', + context: { apiName: this.apiName, id }, + }); + } + const vn = vnData.results[0]; + const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; + vn.released ??= 'Unknown'; + + const releaseDataResult = await this.postReleaseQuery(`{ + "filters": ["and" + , ["vn", "=" + , ["id", "=", "${id}"] + ] + , ["official", "=", 1] + ], + "fields": "producers.name, producers.publisher, producers.developer", + "results": 100 + }`); + if (!releaseDataResult.ok) { + return err(releaseDataResult.error); + } + const releaseData = releaseDataResult.value; + + return ok( + new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: releasedIsDate ? new Date(vn.released).getFullYear().toString() : vn.released, + dataSource: this.apiName, + url: `https://vndb.org/${vn.id}`, + id: vn.id, + + developers: vn.developers.map(d => d.name), + publishers: releaseData.results + .flatMap(r => r.producers) + .filter(p => p.publisher) + .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list + .map(p => p.name) + .unique(), + genres: vn.tags + .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) + .sort((t1, t2) => t2.rating - t1.rating) + .map(t => t.name), + onlineRating: vn.rating ?? NaN, + // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively + image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, + + released: vn.devstatus === VNDevStatus.Finished, + releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, + + userData: { + played: false, + personalRating: 0, + }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.VNDBAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/WikipediaAPI.ts b/packages/obsidian/src/api/apis/WikipediaAPI.ts new file mode 100644 index 0000000..2765786 --- /dev/null +++ b/packages/obsidian/src/api/apis/WikipediaAPI.ts @@ -0,0 +1,170 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { WikiModel } from 'packages/obsidian/src/models/WikiModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface SearchResponse { + query: { + search: { + title: string; + pageid: number; + }[]; + }; +} + +interface IdResponse { + query: { + pages: Record; + }; +} + +interface WikipediaPage { + pageid: number; + title: string; + contentmodel: string; + pagelanguage: string; + pagelanguagehtmlcode: string; + pagelanguagedir: string; + touched: string; // ISO date string + lastrevid: number; + length: number; + fullurl: string; + editurl: string; + canonicalurl: string; +} +export class WikipediaAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'Wikipedia API'; + this.apiDescription = 'The API behind Wikipedia'; + this.apiUrl = 'https://www.wikipedia.com'; + this.types = [MediaType.Wiki]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; + const fetchData = await requestUrl({ + url: searchUrl, + method: 'GET', + throw: false, + }); + // console.debug(fetchData); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const response = fetchData as { status: number; json(): Promise }; + const dataResult = await fromPromise(response.json(), cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Failed to parse response from ${this.apiName}`, + userMessage: `Failed to parse response from ${this.apiName}`, + context: { apiName: this.apiName }, + }), + ); + if (!dataResult.ok) { + return err(dataResult.error); + } + const data = dataResult.value as SearchResponse; + Logger.debug(data); + const ret: MediaTypeModel[] = []; + + for (const result of data.query.search) { + ret.push( + new WikiModel({ + type: 'wiki', + title: result.title, + englishTitle: result.title, + year: '', + dataSource: this.apiName, + id: result.pageid.toString(), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; + const fetchData = await requestUrl({ + url: searchUrl, + method: 'GET', + throw: false, + }); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); + } + + const response = fetchData as { status: number; json(): Promise }; + const dataResult = await fromPromise(response.json(), cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Failed to parse response from ${this.apiName}`, + userMessage: `Failed to parse response from ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!dataResult.ok) { + return err(dataResult.error); + } + const data = dataResult.value as IdResponse; + // console.debug(data); + const result = Object.values(data?.query?.pages)[0]; + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + return ok( + new WikiModel({ + title: result.title, + englishTitle: result.title, + dataSource: this.apiName, + url: result.fullurl, + id: result.pageid.toString(), + + wikiUrl: result.fullurl, + lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), + length: result.length, + + userData: {}, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.WikipediaAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts new file mode 100644 index 0000000..3bddc83 --- /dev/null +++ b/packages/obsidian/src/main.ts @@ -0,0 +1,209 @@ +import 'packages/obsidian/src/styles.css'; +import { Plugin, TFolder } from 'obsidian'; +import { APIManager } from 'packages/obsidian/src/api/APIManager'; +import { BoardGameGeekAPI } from 'packages/obsidian/src/api/apis/BoardGameGeekAPI'; +import { ComicVineAPI } from 'packages/obsidian/src/api/apis/ComicVineAPI'; +import { IGDBAPI } from 'packages/obsidian/src/api/apis/IGDBAPI'; +import { MALAPI } from 'packages/obsidian/src/api/apis/MALAPI'; +import { MALAPIManga } from 'packages/obsidian/src/api/apis/MALAPIManga'; +import { MusicBrainzAPI } from 'packages/obsidian/src/api/apis/MusicBrainzAPI'; +import { OMDbAPI } from 'packages/obsidian/src/api/apis/OMDbAPI'; +import { OpenLibraryAPI } from 'packages/obsidian/src/api/apis/OpenLibraryAPI'; +import { RAWGAPI } from 'packages/obsidian/src/api/apis/RAWGAPI'; +import { SteamAPI } from 'packages/obsidian/src/api/apis/SteamAPI'; +import { TMDBMovieAPI } from 'packages/obsidian/src/api/apis/TMDBMovieAPI'; +import { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; +import { TMDBSeriesAPI } from 'packages/obsidian/src/api/apis/TMDBSeriesAPI'; +import { VNDBAPI } from 'packages/obsidian/src/api/apis/VNDBAPI'; +import { WikipediaAPI } from 'packages/obsidian/src/api/apis/WikipediaAPI'; +import { PropertyMapper } from 'packages/obsidian/src/settings/PropertyMapper'; +import { PropertyMappingModel } from 'packages/obsidian/src/settings/PropertyMapping'; +import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings'; +import { MediaDbSettingTab } from 'packages/obsidian/src/settings/Settings'; +import { getDefaultSettings } from 'packages/obsidian/src/settings/Settings'; +import { BulkImportHelper } from 'packages/obsidian/src/utils/BulkImportHelper'; +import { DateFormatter } from 'packages/obsidian/src/utils/DateFormatter'; +import { ErrorReporter } from 'packages/obsidian/src/utils/ErrorReporter'; +import { MediaDbEntryHelper } from 'packages/obsidian/src/utils/MediaDbEntryHelper'; +import { MediaDbFileHelper } from 'packages/obsidian/src/utils/MediaDbFileHelper'; +import { MediaTypeManager } from 'packages/obsidian/src/utils/MediaTypeManager'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; +import { ModalHelper } from 'packages/obsidian/src/utils/ModalHelper'; +import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; + +export default class MediaDbPlugin extends Plugin { + settings!: MediaDbPluginSettings; + apiManager!: APIManager; + mediaTypeManager!: MediaTypeManager; + modelPropertyMapper!: PropertyMapper; + modalHelper!: ModalHelper; + fileHelper!: MediaDbFileHelper; + entryHelper!: MediaDbEntryHelper; + bulkImportHelper!: BulkImportHelper; + dateFormatter!: DateFormatter; + errorReporter!: ErrorReporter; + + async onload(): Promise { + this.mediaTypeManager = new MediaTypeManager(); + this.modelPropertyMapper = new PropertyMapper(this); + this.errorReporter = new ErrorReporter(); + this.modalHelper = new ModalHelper(this); + this.fileHelper = new MediaDbFileHelper(this); + this.entryHelper = new MediaDbEntryHelper(this); + this.bulkImportHelper = new BulkImportHelper(this); + this.dateFormatter = new DateFormatter(); + + await this.loadSettings(); + this.registerDefaultApis(); + this.addSettingTab(new MediaDbSettingTab(this.app, this)); + this.registerRibbonAndFileMenu(); + this.registerCommands(); + } + + onunload(): void {} + + private registerDefaultApis(): void { + this.apiManager = new APIManager(); + this.apiManager.registerAPI(new OMDbAPI(this)); + this.apiManager.registerAPI(new MALAPI(this)); + this.apiManager.registerAPI(new MALAPIManga(this)); + this.apiManager.registerAPI(new WikipediaAPI(this)); + this.apiManager.registerAPI(new MusicBrainzAPI(this)); + this.apiManager.registerAPI(new SteamAPI(this)); + this.apiManager.registerAPI(new TMDBSeriesAPI(this)); + this.apiManager.registerAPI(new TMDBSeasonAPI(this)); + this.apiManager.registerAPI(new TMDBMovieAPI(this)); + this.apiManager.registerAPI(new BoardGameGeekAPI(this)); + this.apiManager.registerAPI(new OpenLibraryAPI(this)); + this.apiManager.registerAPI(new ComicVineAPI(this)); + this.apiManager.registerAPI(new IGDBAPI(this)); + this.apiManager.registerAPI(new RAWGAPI(this)); + this.apiManager.registerAPI(new VNDBAPI(this)); + } + + private registerRibbonAndFileMenu(): void { + const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', () => this.entryHelper.createEntryWithAdvancedSearchModal()); + ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); + + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file) => { + if (file instanceof TFolder) { + menu.addItem(item => { + item.setTitle('Import folder as Media DB entries') + .setIcon('database') + .onClick(() => this.bulkImportHelper.import(file)); + }); + } + }), + ); + } + + private registerCommands(): void { + this.addCommand({ + id: 'open-media-db-search-modal', + name: 'Create entry', + callback: () => this.entryHelper.createEntryWithSearchModal(), + }); + + for (const mediaType of MEDIA_TYPES) { + this.addCommand({ + id: `open-media-db-search-modal-with-${mediaType}`, + name: `Create entry (${unCamelCase(mediaType)})`, + callback: () => this.entryHelper.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), + }); + } + + this.addCommand({ + id: 'open-media-db-advanced-search-modal', + name: 'Create entry (advanced search)', + callback: () => this.entryHelper.createEntryWithAdvancedSearchModal(), + }); + + this.addCommand({ + id: 'open-media-db-id-search-modal', + name: 'Create entry by id', + callback: () => this.entryHelper.createEntryWithIdSearchModal(), + }); + + this.addCommand({ + id: 'update-media-db-note', + name: 'Update open note (this will recreate the note)', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.fileHelper.updateActiveNote(false); + } + return true; + }, + }); + + this.addCommand({ + id: 'update-media-db-note-metadata', + name: 'Update metadata', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.fileHelper.updateActiveNote(true); + } + return true; + }, + }); + + this.addCommand({ + id: 'add-media-db-link', + name: 'Insert link', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.entryHelper.createLinkWithSearchModal(); + } + return true; + }, + }); + } + + async loadSettings(): Promise { + const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; + const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); + const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); + + // delete old api keys + // @ts-ignore + delete loadedSettings.BoardgameGeekKey; + // @ts-ignore + delete loadedSettings.ComicVineKey; + // @ts-ignore + delete loadedSettings.GiantBombKey; + // @ts-ignore + delete loadedSettings.MobyGamesKey; + // @ts-ignore + delete loadedSettings.OMDbKey; + // @ts-ignore + delete loadedSettings.TMDBKey; + + const migratedModels = PropertyMappingModel.migrateModels( + loadedSettings.propertyMappingModels || [], + defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), + ); + + loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); + + this.settings = loadedSettings; + + await this.saveSettings(); + } + + async saveSettings(): Promise { + this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); + this.dateFormatter.setFormat(this.settings.customDateFormat); + + await this.saveData(this.settings); + } +} diff --git a/src/modals/ConfirmOverwriteModal.ts b/packages/obsidian/src/modals/ConfirmOverwriteModal.ts similarity index 100% rename from src/modals/ConfirmOverwriteModal.ts rename to packages/obsidian/src/modals/ConfirmOverwriteModal.ts diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts similarity index 86% rename from src/modals/MediaDbAdvancedSearchModal.ts rename to packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index e0e8020..04f0598 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -1,8 +1,8 @@ import type { ButtonComponent } from 'obsidian'; import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; -import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbAdvancedSearchModal extends Modal { plugin: MediaDbPlugin; @@ -71,7 +71,7 @@ export class MediaDbAdvancedSearchModal extends Modal { const placeholder = 'Search by title'; const searchComponent = new TextComponent(contentEl); - searchComponent.inputEl.style.width = '100%'; + searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); @@ -85,13 +85,13 @@ export class MediaDbAdvancedSearchModal extends Modal { // const apiToggleComponents: Component[] = []; for (const api of this.plugin.apiManager.apis) { - const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); - const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: api.apiName, cls: 'media-db-plugin-list-text' }); + const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + apiToggleTextWrapper.createSpan({ text: api.apiName, cls: 'media-db-plugin-list-text' }); apiToggleTextWrapper.createEl('small', { text: api.apiDescription, cls: 'media-db-plugin-list-text' }); - const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' }); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(api.apiName); diff --git a/src/modals/MediaDbBulkImportModal.ts b/packages/obsidian/src/modals/MediaDbBulkImportModal.ts similarity index 79% rename from src/modals/MediaDbBulkImportModal.ts rename to packages/obsidian/src/modals/MediaDbBulkImportModal.ts index 0763d32..0b5c3d4 100644 --- a/src/modals/MediaDbBulkImportModal.ts +++ b/packages/obsidian/src/modals/MediaDbBulkImportModal.ts @@ -1,8 +1,8 @@ import type { ButtonComponent } from 'obsidian'; import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type { APIModel } from '../api/APIModel'; -import type MediaDbPlugin from '../main'; -import { BulkImportLookupMethod } from '../utils/BulkImportHelper'; +import type { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { BulkImportLookupMethod } from 'packages/obsidian/src/utils/BulkImportHelper'; export class MediaDbBulkImportModal extends Modal { plugin: MediaDbPlugin; @@ -47,14 +47,14 @@ export class MediaDbBulkImportModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' }); - const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - appendContentToggleTextWrapper.createEl('span', { + const appendContentToggleElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + appendContentToggleTextWrapper.createSpan({ text: 'If this is enabled, the plugin will override metadata fields with the same name.', cls: 'media-db-plugin-list-text', }); - const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' }); const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper); appendContentToggle.setValue(false); @@ -81,9 +81,9 @@ export class MediaDbBulkImportModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' }); + const fieldNameWrapperEl = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const fieldNameLabelWrapperEl = fieldNameWrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + fieldNameLabelWrapperEl.createSpan({ text: 'Using the property named', cls: 'media-db-plugin-list-text' }); const fieldNameComponent = new TextComponent(fieldNameWrapperEl); fieldNameComponent.setPlaceholder('title / id'); @@ -115,9 +115,9 @@ export class MediaDbBulkImportModal extends Modal { } createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void { - const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' }); + const wrapperEl = parentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const labelWrapperEl = wrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + labelWrapperEl.createSpan({ text: label, cls: 'media-db-plugin-list-text' }); const dropDownComponent = new DropdownComponent(wrapperEl); dropDownComponent.onChange(onChange); diff --git a/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts similarity index 82% rename from src/modals/MediaDbIdSearchModal.ts rename to packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 7027bb5..1f65d30 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -1,8 +1,8 @@ import type { ButtonComponent } from 'obsidian'; import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; -import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { IdSearchModalData, IdSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbIdSearchModal extends Modal { plugin: MediaDbPlugin; @@ -69,7 +69,7 @@ export class MediaDbIdSearchModal extends Modal { const placeholder = 'Search by id'; const searchComponent = new TextComponent(contentEl); - searchComponent.inputEl.style.width = '100%'; + searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); searchComponent.onChange(value => (this.query = value)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); @@ -79,9 +79,9 @@ export class MediaDbIdSearchModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiSelectorTExtWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' }); + const apiSelectorWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const apiSelectorTExtWrapper = apiSelectorWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + apiSelectorTExtWrapper.createSpan({ text: 'API to search', cls: 'media-db-plugin-list-text' }); const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper); apiSelectorComponent.onChange((value: string) => { diff --git a/src/modals/MediaDbPreviewModal.ts b/packages/obsidian/src/modals/MediaDbPreviewModal.ts similarity index 80% rename from src/modals/MediaDbPreviewModal.ts rename to packages/obsidian/src/modals/MediaDbPreviewModal.ts index 1efd66a..6bf3848 100644 --- a/src/modals/MediaDbPreviewModal.ts +++ b/packages/obsidian/src/modals/MediaDbPreviewModal.ts @@ -1,8 +1,9 @@ import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper'; -import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { PreviewModalData, PreviewModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { PREVIEW_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbPreviewModal extends Modal { plugin: MediaDbPlugin; @@ -48,14 +49,14 @@ export class MediaDbPreviewModal extends Modal { previewWrapper.createEl('h3', { text: result.englishTitle }); const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' }); - let fileContent = this.plugin.generateMediaDbNoteFrontmatterPreview(result); + let fileContent = this.plugin.fileHelper.generateMediaDbNoteFrontmatterPreview(result); fileContent = `\`\`\`yaml\n${fileContent}\`\`\``; try { // TODO: fix this not rendering the frontmatter any more await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent); } catch (e) { - console.warn(`mdb | error during rendering of preview`, e); + Logger.warn(`mdb | error during rendering of preview`, e); } } diff --git a/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts similarity index 82% rename from src/modals/MediaDbSearchModal.ts rename to packages/obsidian/src/modals/MediaDbSearchModal.ts index 800eace..e62828d 100644 --- a/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -1,11 +1,11 @@ import type { ButtonComponent } from 'obsidian'; import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -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 type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; +import type { SearchModalData, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; +import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; export class MediaDbSearchModal extends Modal { plugin: MediaDbPlugin; @@ -76,7 +76,7 @@ export class MediaDbSearchModal extends Modal { const searchComponent = new TextComponent(contentEl); let currentToggle: ToggleComponent | undefined = undefined; - searchComponent.inputEl.style.width = '100%'; + searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); @@ -89,12 +89,12 @@ export class MediaDbSearchModal extends Modal { contentEl.createEl('h3', { text: 'APIs to search' }); for (const mediaType of MEDIA_TYPES) { - const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); - const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' }); + const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + apiToggleTextWrapper.createSpan({ text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' }); - const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' }); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(unCamelCase(mediaType)); diff --git a/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts similarity index 80% rename from src/modals/MediaDbSearchResultModal.ts rename to packages/obsidian/src/modals/MediaDbSearchResultModal.ts index 786df40..e513663 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -1,8 +1,8 @@ -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; -import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper'; -import { SelectModal } from './SelectModal'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; @@ -41,7 +41,7 @@ export class MediaDbSearchResultModal extends SelectModal { // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement): void { - el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) }); + el.createDiv({ text: this.plugin.mediaTypeManager.getFileName(item) }); el.createEl('small', { text: `${item.getSummary()}\n` }); el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); } diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts similarity index 88% rename from src/modals/MediaDbSeasonSelectModal.ts rename to packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index 0066c31..6508c63 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -1,5 +1,5 @@ -import type MediaDbPlugin from '../main'; -import { SelectModal } from './SelectModal'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; export interface SeasonSelectModalElement { season_number: number; @@ -24,7 +24,7 @@ export class MediaDbSeasonSelectModal extends SelectModal extends Modal { allowMultiSelect: boolean; @@ -48,7 +48,7 @@ export abstract class SelectModal extends Modal { this.activateHighlighted(); }); this.scope.register([], ' ', evt => { - if (this.elementWrapper && this.elementWrapper === document.activeElement) { + if (this.elementWrapper && this.elementWrapper === activeDocument.activeElement) { this.activateHighlighted(); evt.preventDefault(); } diff --git a/src/modals/SelectModalElement.ts b/packages/obsidian/src/modals/SelectModalElement.ts similarity index 96% rename from src/modals/SelectModalElement.ts rename to packages/obsidian/src/modals/SelectModalElement.ts index 70c09f1..7b78d24 100644 --- a/src/modals/SelectModalElement.ts +++ b/packages/obsidian/src/modals/SelectModalElement.ts @@ -1,4 +1,4 @@ -import type { SelectModal } from './SelectModal'; +import type { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; export class SelectModalElement { selectModal: SelectModal; diff --git a/src/models/BoardGameModel.ts b/packages/obsidian/src/models/BoardGameModel.ts similarity index 78% rename from src/models/BoardGameModel.ts rename to packages/obsidian/src/models/BoardGameModel.ts index e782137..1297b97 100644 --- a/src/models/BoardGameModel.ts +++ b/packages/obsidian/src/models/BoardGameModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type BoardGameData = ModelToData; diff --git a/src/models/BookModel.ts b/packages/obsidian/src/models/BookModel.ts similarity index 77% rename from src/models/BookModel.ts rename to packages/obsidian/src/models/BookModel.ts index e75da93..ff5ba65 100644 --- a/src/models/BookModel.ts +++ b/packages/obsidian/src/models/BookModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type BookData = ModelToData; diff --git a/src/models/ComicMangaModel.ts b/packages/obsidian/src/models/ComicMangaModel.ts similarity index 82% rename from src/models/ComicMangaModel.ts rename to packages/obsidian/src/models/ComicMangaModel.ts index ec47a95..57fc791 100644 --- a/src/models/ComicMangaModel.ts +++ b/packages/obsidian/src/models/ComicMangaModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type ComicMangaData = ModelToData; diff --git a/src/models/GameModel.ts b/packages/obsidian/src/models/GameModel.ts similarity index 76% rename from src/models/GameModel.ts rename to packages/obsidian/src/models/GameModel.ts index 5661f6e..536ee13 100644 --- a/src/models/GameModel.ts +++ b/packages/obsidian/src/models/GameModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type GameData = ModelToData; diff --git a/src/models/MediaTypeModel.ts b/packages/obsidian/src/models/MediaTypeModel.ts similarity index 92% rename from src/models/MediaTypeModel.ts rename to packages/obsidian/src/models/MediaTypeModel.ts index e4d03a0..cdadbb7 100644 --- a/src/models/MediaTypeModel.ts +++ b/packages/obsidian/src/models/MediaTypeModel.ts @@ -1,4 +1,4 @@ -import type { MediaType } from '../utils/MediaType'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; export abstract class MediaTypeModel { type: string; diff --git a/src/models/MovieModel.ts b/packages/obsidian/src/models/MovieModel.ts similarity index 82% rename from src/models/MovieModel.ts rename to packages/obsidian/src/models/MovieModel.ts index 563a0f9..a386615 100644 --- a/src/models/MovieModel.ts +++ b/packages/obsidian/src/models/MovieModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type MovieData = ModelToData; diff --git a/src/models/MusicReleaseModel.ts b/packages/obsidian/src/models/MusicReleaseModel.ts similarity index 81% rename from src/models/MusicReleaseModel.ts rename to packages/obsidian/src/models/MusicReleaseModel.ts index 38427a9..decb5d5 100644 --- a/src/models/MusicReleaseModel.ts +++ b/packages/obsidian/src/models/MusicReleaseModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type MusicReleaseData = ModelToData; diff --git a/src/models/SeasonModel.ts b/packages/obsidian/src/models/SeasonModel.ts similarity index 82% rename from src/models/SeasonModel.ts rename to packages/obsidian/src/models/SeasonModel.ts index fc4e0a3..b295d36 100644 --- a/src/models/SeasonModel.ts +++ b/packages/obsidian/src/models/SeasonModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type SeasonData = ModelToData; diff --git a/src/models/SeriesModel.ts b/packages/obsidian/src/models/SeriesModel.ts similarity index 82% rename from src/models/SeriesModel.ts rename to packages/obsidian/src/models/SeriesModel.ts index b87a32c..974d019 100644 --- a/src/models/SeriesModel.ts +++ b/packages/obsidian/src/models/SeriesModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type SeriesData = ModelToData; diff --git a/src/models/WikiModel.ts b/packages/obsidian/src/models/WikiModel.ts similarity index 75% rename from src/models/WikiModel.ts rename to packages/obsidian/src/models/WikiModel.ts index ee49c2d..8216d15 100644 --- a/src/models/WikiModel.ts +++ b/packages/obsidian/src/models/WikiModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type WikiData = ModelToData; diff --git a/src/settings/Icon.tsx b/packages/obsidian/src/settings/Icon.tsx similarity index 100% rename from src/settings/Icon.tsx rename to packages/obsidian/src/settings/Icon.tsx diff --git a/src/settings/PropertyMapper.ts b/packages/obsidian/src/settings/PropertyMapper.ts similarity index 87% rename from src/settings/PropertyMapper.ts rename to packages/obsidian/src/settings/PropertyMapper.ts index b34f245..831bb7d 100644 --- a/src/settings/PropertyMapper.ts +++ b/packages/obsidian/src/settings/PropertyMapper.ts @@ -1,7 +1,8 @@ -import type MediaDbPlugin from '../main'; -import type { MediaType } from '../utils/MediaType'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import { PropertyMappingOption } from './PropertyMapping'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; export class PropertyMapper { plugin: MediaDbPlugin; @@ -75,7 +76,7 @@ export class PropertyMapper { if (obj.type === 'manga') { obj.type = 'comicManga'; - console.debug(`MDB | updated metadata type`, obj.type); + Logger.debug(`MDB | updated metadata type`, obj.type); } if (!MEDIA_TYPES.includes(obj.type as MediaType)) { return obj; diff --git a/src/settings/PropertyMapping.ts b/packages/obsidian/src/settings/PropertyMapping.ts similarity index 96% rename from src/settings/PropertyMapping.ts rename to packages/obsidian/src/settings/PropertyMapping.ts index 82d044e..62e86c8 100644 --- a/src/settings/PropertyMapping.ts +++ b/packages/obsidian/src/settings/PropertyMapping.ts @@ -1,5 +1,6 @@ -import type { MediaType } from '../utils/MediaType'; -import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from 'packages/obsidian/src/utils/Utils'; // Plain object interfaces for serialization export interface PropertyMappingData { @@ -33,7 +34,7 @@ export class PropertyMappingModel { } validate(): { res: boolean; err?: Error } { - console.debug(`MDB | validated property mappings for ${this.type}`); + Logger.debug(`MDB | validated property mappings for ${this.type}`); // check properties for (const property of this.properties) { diff --git a/src/settings/PropertyMappingModelComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx similarity index 100% rename from src/settings/PropertyMappingModelComponent.tsx rename to packages/obsidian/src/settings/PropertyMappingModelComponent.tsx diff --git a/src/settings/PropertyMappingModelsComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx similarity index 100% rename from src/settings/PropertyMappingModelsComponent.tsx rename to packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx diff --git a/src/settings/Settings.ts b/packages/obsidian/src/settings/Settings.ts similarity index 91% rename from src/settings/Settings.ts rename to packages/obsidian/src/settings/Settings.ts index c7e694a..40ceb8d 100644 --- a/src/settings/Settings.ts +++ b/packages/obsidian/src/settings/Settings.ts @@ -1,16 +1,51 @@ import type { App } from 'obsidian'; import { Notice, PluginSettingTab, SecretComponent, SettingGroup } from 'obsidian'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping'; +import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping'; +import PropertyMappingModelsComponent from 'packages/obsidian/src/settings/PropertyMappingModelsComponent'; +import { FileSuggest } from 'packages/obsidian/src/settings/suggesters/FileSuggest'; +import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderSuggest'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; +import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; import { render } from 'solid-js/web'; -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { MediaType } from '../utils/MediaType'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import { fragWithHTML, unCamelCase } from '../utils/Utils'; -import type { PropertyMappingModelData } from './PropertyMapping'; -import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; -import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; -import { FileSuggest } from './suggesters/FileSuggest'; -import { FolderSuggest } from './suggesters/FolderSuggest'; + +function createDateFormatDescription(preview: string): DocumentFragment { + return createFragment(frag => { + const container = frag.createDiv(); + container.appendText('Your custom date format. Use '); + container.createEl('em', { text: "'YYYY-MM-DD'" }); + container.appendText(' for example.'); + container.createEl('br'); + container.appendText('For more syntax, refer to '); + container.createEl('a', { + href: 'https://momentjs.com/docs/#/displaying/format/', + text: 'format reference', + }); + container.appendText('.'); + container.createEl('br'); + container.appendText('Your current syntax looks like this: '); + container.createEl('em', { text: preview }); + }); +} + +function createPropertyMappingsDescription(): DocumentFragment { + return createFragment(frag => { + const container = frag.createDiv(); + container.createEl('p', { + text: 'Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.', + }); + container.createEl('p', { + text: 'You can choose to keep the original name, rename the property, or remove it entirely.', + }); + const paragraph = container.createEl('p'); + paragraph.createEl('strong', { + text: 'Remember to save your changes using the save button for each individual category.', + }); + }); +} // MARK: Settings export interface MediaDbPluginSettings { @@ -435,22 +470,14 @@ export class MediaDbSettingTab extends PluginSettingTab { setting => void setting .setName('Date format') - .setDesc( - fragWithHTML( - "Your custom date format. Use 'YYYY-MM-DD' for example.
" + - "For more syntax, refer to format reference.
" + - "Your current syntax looks like this: " + - this.plugin.dateFormatter.getPreview() + - '', - ), - ) + .setDesc(createDateFormatDescription(this.plugin.dateFormatter.getPreview())) .addText(cb => { cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) .onChange(data => { const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; this.plugin.settings.customDateFormat = newDateFormat; - const previewEl = document.getElementById('media-db-dateformat-preview'); + const previewEl = activeDocument.getElementById('media-db-dateformat-preview'); if (previewEl) { previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview } @@ -809,15 +836,7 @@ export class MediaDbSettingTab extends PluginSettingTab { const mappingGroup = new SettingGroup(containerEl); mappingGroup.setHeading('Property mappings'); mappingGroup.addSetting(setting => { - setting - .setName('Property mappings explanation') - .setDesc( - fragWithHTML( - '

Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.

' + - '

You can choose to keep the original name, rename the property, or remove it entirely.

' + - '

Remember to save your changes using the save button for each individual category.

', - ), - ); + setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription()); render( () => diff --git a/src/settings/suggesters/FileSuggest.ts b/packages/obsidian/src/settings/suggesters/FileSuggest.ts similarity index 100% rename from src/settings/suggesters/FileSuggest.ts rename to packages/obsidian/src/settings/suggesters/FileSuggest.ts diff --git a/src/settings/suggesters/FolderSuggest.ts b/packages/obsidian/src/settings/suggesters/FolderSuggest.ts similarity index 100% rename from src/settings/suggesters/FolderSuggest.ts rename to packages/obsidian/src/settings/suggesters/FolderSuggest.ts diff --git a/src/styles.css b/packages/obsidian/src/styles.css similarity index 99% rename from src/styles.css rename to packages/obsidian/src/styles.css index b204d1d..18a0f55 100644 --- a/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -64,6 +64,10 @@ small.media-db-plugin-list-text { margin-bottom: 10px; } +.media-db-plugin-search-input { + width: 100%; +} + .media-db-plugin-button:focus { /*outline: 1px solid white;*/ } diff --git a/packages/obsidian/src/utils/AppError.ts b/packages/obsidian/src/utils/AppError.ts new file mode 100644 index 0000000..50db684 --- /dev/null +++ b/packages/obsidian/src/utils/AppError.ts @@ -0,0 +1,24 @@ +export enum AppErrorKind { + Validation = 'Validation', + Api = 'Api', + Network = 'Network', + Vault = 'Vault', + Modal = 'Modal', + Cancelled = 'Cancelled', + Unexpected = 'Unexpected', +} + +export interface AppError { + kind: AppErrorKind; + message: string; + userMessage?: string; + cause?: unknown; + context?: Record; +} + +export const appError = (error: AppError): AppError => error; + +export const toAppError = (cause: unknown, fallback: Omit): AppError => { + const message = cause instanceof Error ? cause.message : String(cause); + return { ...fallback, message: fallback.message || message, cause }; +}; diff --git a/src/utils/BulkImportHelper.ts b/packages/obsidian/src/utils/BulkImportHelper.ts similarity index 67% rename from src/utils/BulkImportHelper.ts rename to packages/obsidian/src/utils/BulkImportHelper.ts index da4b1a4..f91ae37 100644 --- a/src/utils/BulkImportHelper.ts +++ b/packages/obsidian/src/utils/BulkImportHelper.ts @@ -1,10 +1,10 @@ import type { TFolder } from 'obsidian'; import { TFile } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import { MediaDbBulkImportModal as MediaDbBulkImportModal } from '../modals/MediaDbBulkImportModal'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { ModalResultCode } from './ModalHelper'; -import { dateTimeToString, markdownTable } from './Utils'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; +import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils'; export enum BulkImportLookupMethod { ID = 'id', @@ -50,7 +50,7 @@ export class BulkImportHelper { continue; } - const metadata = this.plugin.getMetadataFromFileCache(file); + const metadata = this.plugin.fileHelper.getMetadataFromFileCache(file); const lookupValue = metadata[fieldName]; if (!lookupValue || typeof lookupValue !== 'string') { @@ -81,60 +81,54 @@ export class BulkImportHelper { } private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { - try { - const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); - if (model) { - await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined); - return undefined; - } else { - return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; - } - } catch (e) { - return { filePath: file.path, error: `${e}` }; + const modelResult = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); + if (!modelResult.ok) { + return { filePath: file.path, error: modelResult.error.userMessage ?? modelResult.error.message }; } + + if (modelResult.value) { + await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined); + return undefined; + } + + return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; } private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { - let results: MediaTypeModel[] = []; - try { - results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); - } catch (e) { - return { filePath: file.path, error: `${e}` }; + const resultsResult = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); + if (!resultsResult.ok) { + return { filePath: file.path, error: resultsResult.error.userMessage ?? resultsResult.error.message }; } + + const results: MediaTypeModel[] = resultsResult.value.items; if (!results || results.length === 0) { return { filePath: file.path, error: `no search results` }; } - const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({ + const selectModalResult = await this.plugin.modalHelper.createSelectModalOutcome({ elements: results, skipButton: true, modalTitle: `Results for '${lookupValue}'`, }); - if (selectModalResult.code === ModalResultCode.ERROR) { - selectModal.close(); - return { filePath: file.path, error: selectModalResult.error.message }; + if (selectModalResult.status === OutcomeStatus.Error) { + return { filePath: file.path, error: selectModalResult.error.userMessage ?? selectModalResult.error.message }; } - if (selectModalResult.code === ModalResultCode.CLOSE) { - selectModal.close(); + if (selectModalResult.status === OutcomeStatus.Cancelled) { return { filePath: file.path, error: 'user canceled', canceled: true }; } - if (selectModalResult.code === ModalResultCode.SKIP) { - selectModal.close(); + if (selectModalResult.status === OutcomeStatus.Skipped) { return { filePath: file.path, error: 'user skipped' }; } if (selectModalResult.data.selected.length === 0) { - selectModal.close(); return { filePath: file.path, error: `no search results selected` }; } - const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected); - await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined); - - selectModal.close(); + const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected); + await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined); return undefined; } diff --git a/src/utils/DateFormatter.ts b/packages/obsidian/src/utils/DateFormatter.ts similarity index 80% rename from src/utils/DateFormatter.ts rename to packages/obsidian/src/utils/DateFormatter.ts index 31169e5..bc12064 100644 --- a/src/utils/DateFormatter.ts +++ b/packages/obsidian/src/utils/DateFormatter.ts @@ -1,8 +1,6 @@ -import type momentType from 'moment'; -import type { Moment } from 'moment'; -import { moment as obsidianMoment } from 'obsidian'; +import { moment } from 'obsidian'; -const moment: typeof momentType = obsidianMoment as unknown as typeof momentType; +const momentFn = 'default' in moment ? moment.default : moment; export class DateFormatter { private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ'; @@ -21,7 +19,7 @@ export class DateFormatter { } getPreview(format?: string): string { - const today = moment(); + const today = momentFn(); format ??= this.toFormat; @@ -44,32 +42,32 @@ export class DateFormatter { return null; } - let date: Moment; + let date: moment.Moment; if (!dateFormat) { date = this.parseWithoutFormat(dateString); } else { - date = moment(dateString, dateFormat, locale); + date = momentFn(dateString, dateFormat, locale); } // format date (if it is valid) return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null; } - private parseWithoutFormat(dateString: string): Moment { + private parseWithoutFormat(dateString: string): moment.Moment { // reading date formats other than C2822 or ISO with moment is deprecated // see https://momentjs.com/docs/#/parsing/string/ if (this.hasMomentFormat(dateString)) { // expect C2822 or ISO format - return moment(dateString); + return momentFn(dateString); } // fall back to native Date parsing for unknown formats - return moment(new Date(dateString)); + return momentFn(new Date(dateString)); } private hasMomentFormat(dateString: string): boolean { - const date = moment(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode + const date = momentFn(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode return date.isValid(); } } diff --git a/packages/obsidian/src/utils/ErrorReporter.ts b/packages/obsidian/src/utils/ErrorReporter.ts new file mode 100644 index 0000000..4155ae1 --- /dev/null +++ b/packages/obsidian/src/utils/ErrorReporter.ts @@ -0,0 +1,23 @@ +import { Notice } from 'obsidian'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; + +export class ErrorReporter { + notice(error: AppError): void { + const message = error.userMessage ?? error.message; + new Notice(message); + } + + log(error: AppError): void { + Logger.warn('MDB | error', error); + } + + report(error: AppError): void { + this.log(error); + + if (error.kind !== AppErrorKind.Cancelled) { + this.notice(error); + } + } +} diff --git a/src/utils/IconList.ts b/packages/obsidian/src/utils/IconList.ts similarity index 100% rename from src/utils/IconList.ts rename to packages/obsidian/src/utils/IconList.ts diff --git a/src/utils/IllegalFilenameCharactersList.ts b/packages/obsidian/src/utils/IllegalFilenameCharactersList.ts similarity index 100% rename from src/utils/IllegalFilenameCharactersList.ts rename to packages/obsidian/src/utils/IllegalFilenameCharactersList.ts diff --git a/packages/obsidian/src/utils/Logger.ts b/packages/obsidian/src/utils/Logger.ts new file mode 100644 index 0000000..ab88308 --- /dev/null +++ b/packages/obsidian/src/utils/Logger.ts @@ -0,0 +1,38 @@ +declare const __LOG_LEVEL__: number; + +enum LogLevel { + NONE = 0, + ERROR = 1, + WARN = 2, + LOG = 3, + DEBUG = 4, +} + +const logLevel: LogLevel = typeof __LOG_LEVEL__ !== 'undefined' ? __LOG_LEVEL__ : LogLevel.LOG; + +export class Logger { + static debug(...args: unknown[]): void { + if (logLevel >= LogLevel.DEBUG) { + console.debug(...args); + } + } + + static log(...args: unknown[]): void { + if (logLevel >= LogLevel.LOG) { + // eslint-disable-next-line obsidianmd/rule-custom-message -- These log statements are disabled for production builds. + console.log(...args); + } + } + + static warn(...args: unknown[]): void { + if (logLevel >= LogLevel.WARN) { + console.warn(...args); + } + } + + static error(...args: unknown[]): void { + if (logLevel >= LogLevel.ERROR) { + console.error(...args); + } + } +} diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts new file mode 100644 index 0000000..222a11c --- /dev/null +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -0,0 +1,289 @@ +import { MarkdownView, Notice } from 'obsidian'; +import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; + +export class MediaDbEntryHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + private reportAppError(error: AppError): void { + this.plugin.errorReporter.report(error); + } + + async createLinkWithSearchModal(): Promise { + const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + if (!advancedSearch) { + return; + } + + const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + if (!apiSearchResults.ok) { + this.reportAppError(apiSearchResults.error); + return; + } + + if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + return; + } + + const selectResults = await this.plugin.modalHelper.promptSelectModal({ elements: apiSearchResults.value.items, multiSelect: false }); + if (!selectResults || selectResults.selected.length < 1) { + return; + } + + const detailedResults = await this.queryDetails(selectResults.selected); + if (detailedResults.length < 1) { + return; + } + + const link = `[${detailedResults[0].title}](${detailedResults[0].url})`; + const view = this.plugin.app.workspace.getActiveViewOfType(MarkdownView); + + if (view) { + view.editor.replaceRange(link, view.editor.getCursor()); + } + } + + async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { + const searchData = await this.plugin.modalHelper.promptSearchModal(searchModalOptions ?? {}); + if (!searchData) { + return; + } + + const types = searchData.types; + const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName); + const apiSearchResults = await this.plugin.apiManager.query(searchData.query, apis); + if (!apiSearchResults.ok) { + this.reportAppError(apiSearchResults.error); + return; + } + + if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + return; + } + + const filteredSearchResults = apiSearchResults.value.items.filter(result => types.includes(result.getMediaType())); + if (filteredSearchResults.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' }); + return; + } + + const selectResultsData = + types.length === 1 && types[0] === MediaType.Season + ? await this.plugin.modalHelper.promptSelectModal({ + elements: filteredSearchResults, + description: 'Select one search result to proceed.', + submitButtonText: 'Ok', + }) + : await this.plugin.modalHelper.promptSelectModal({ elements: filteredSearchResults }); + + if (!selectResultsData) { + return; + } + + const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected); + + if (selectResults.length === 0) { + return; + } + + const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults); + if (seasonHandlingResult.handled) { + return; + } + + const confirmed = await this.plugin.modalHelper.promptPreviewModal({ elements: selectResults }); + if (!confirmed?.confirmed) { + return; + } + + await this.plugin.fileHelper.createMediaDbNotes(selectResults); + } + + async createEntryWithAdvancedSearchModal(): Promise { + const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + if (!advancedSearch) { + return; + } + + const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + if (!apiSearchResults.ok) { + this.reportAppError(apiSearchResults.error); + return; + } + + if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + return; + } + + const selectResultsData = await this.plugin.modalHelper.promptSelectModal({ elements: apiSearchResults.value.items }); + if (!selectResultsData) { + return; + } + + const selectResults = await this.queryDetails(selectResultsData.selected); + if (selectResults.length < 1) { + return; + } + + const confirmed = await this.plugin.modalHelper.promptPreviewModal({ elements: selectResults }); + if (!confirmed?.confirmed) { + return; + } + + await this.plugin.fileHelper.createMediaDbNotes(selectResults); + } + + async createEntryWithIdSearchModal(): Promise { + let idSearchResult: MediaTypeModel | undefined = undefined; + let proceed = false; + + while (!proceed) { + const idSearchData = await this.plugin.modalHelper.promptIdSearchModal({}); + if (!idSearchData) { + return; + } + + const queriedIdResult = await this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api); + if (!queriedIdResult.ok) { + this.reportAppError(queriedIdResult.error); + return; + } + + idSearchResult = queriedIdResult.value; + if (!idSearchResult) { + return; + } + + const previewData = await this.plugin.modalHelper.promptPreviewModal({ elements: [idSearchResult] }); + if (!previewData) { + return; + } + + proceed = previewData.confirmed; + } + + if (!idSearchResult) { + return; + } + + const createNoteResult = await this.plugin.fileHelper.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); + if (!createNoteResult.ok) { + this.reportAppError(createNoteResult.error); + } + } + + async queryDetails(models: MediaTypeModel[]): Promise { + const results = await Promise.all(models.map(model => this.plugin.apiManager.queryDetailedInfo(model))); + + const detailModels: MediaTypeModel[] = []; + for (const result of results) { + if (result.ok && result.value) { + detailModels.push(result.value); + } else if (!result.ok) { + this.reportAppError(result.error); + } + } + + return detailModels; + } + + private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { + if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { + const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); + return { handled: true, seasonsCreated: created }; + } + + if (types.includes('series') && selectResults.some(result => result.dataSource === 'TMDBSeriesAPI')) { + const seriesResults = selectResults.filter(result => result.dataSource === 'TMDBSeriesAPI'); + if (seriesResults.length === 1 && types.includes('season')) { + const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); + return { handled: true, seasonsCreated: created }; + } + } + + return { handled: false }; + } + + private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { + const tmdbSeasonAPI = this.plugin.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI | undefined; + if (!tmdbSeasonAPI) { + new Notice('TMDBSeasonAPI not available.'); + return false; + } + + const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); + if (!allSeasonsResult.ok) { + this.reportAppError(allSeasonsResult.error); + new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`); + return false; + } + + const allSeasons = allSeasonsResult.value; + if (allSeasons.length === 0) { + new Notice('No seasons found for this series.'); + return false; + } + + const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); + if (!selectedSeasons || selectedSeasons.length === 0) { + return false; + } + + await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); + new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); + return true; + } + + private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { + const modal = new MediaDbSeasonSelectModal( + this.plugin, + allSeasons.map(season => ({ + season_number: season.seasonNumber, + name: season.seasonTitle || season.title, + episode_count: season.episodes || 0, + air_date: season.year, + poster_path: season.image, + })), + true, + seriesTitle, + ); + + return await new Promise(resolve => { + modal.setSubmitCb(resolve); + modal.open(); + }); + } + + private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { + await Promise.all( + selectedSeasons.map(async selectedSeason => { + const seasonModel = allSeasons.find(season => season.seasonNumber === selectedSeason.season_number); + if (seasonModel) { + const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id); + if (!fullMetadataResult.ok) { + this.reportAppError(fullMetadataResult.error); + new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`); + return; + } + + await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]); + } + }), + ); + } +} diff --git a/packages/obsidian/src/utils/MediaDbFileHelper.ts b/packages/obsidian/src/utils/MediaDbFileHelper.ts new file mode 100644 index 0000000..42f1bdd --- /dev/null +++ b/packages/obsidian/src/utils/MediaDbFileHelper.ts @@ -0,0 +1,325 @@ +import type { TFile } from 'obsidian'; +import { TFolder } from 'obsidian'; +import { Notice, normalizePath, parseYaml, requestUrl, stringifyYaml } from 'obsidian'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { ConfirmOverwriteModal } from 'packages/obsidian/src/modals/ConfirmOverwriteModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; +import type { CreateNoteOptions } from 'packages/obsidian/src/utils/Utils'; +import { hasTemplaterPlugin, replaceIllegalFileNameCharactersInString, useTemplaterPluginInFile } from 'packages/obsidian/src/utils/Utils'; + +export type Metadata = Record; + +export interface MediaTypeModelObj { + id: string; + type: MediaType; + dataSource: string; +} + +export class MediaDbFileHelper { + readonly plugin: MediaDbPlugin; + private readonly frontMatterRegExpPattern = '^(---)\\n[\\s\\S]*?\\n---'; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise> { + const results = await Promise.all(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile }))); + + const failures = results.filter(result => !result.ok); + if (failures.length > 0) { + Logger.warn( + 'MDB | Some notes failed to create:', + failures.map(result => result.error), + ); + new Notice(`${models.length - failures.length} of ${models.length} notes created successfully.`); + return err(failures[0].error); + } + + return ok(undefined); + } + + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise> { + Logger.debug('MDB | creating new note'); + + options.openNote = this.plugin.settings.openNoteInNewTab; + + if (this.plugin.settings.imageDownload) { + const imageResult = await this.downloadImageForMediaModel(mediaTypeModel); + if (!imageResult.ok) { + return imageResult; + } + } + + const fileContentResult = await this.attempt(() => this.generateMediaDbNoteContents(mediaTypeModel, options), { + kind: AppErrorKind.Unexpected, + message: 'Failed to generate note contents', + userMessage: 'Failed to generate note contents', + }); + if (!fileContentResult.ok) { + return fileContentResult; + } + + const folderResult = await this.attempt(() => this.plugin.mediaTypeManager.getFolder(mediaTypeModel, this.plugin.app), { + kind: AppErrorKind.Vault, + message: 'Failed to determine note folder', + userMessage: 'Failed to determine note folder', + }); + if (!folderResult.ok) { + return folderResult; + } + + options.folder ??= folderResult.value; + + const targetFileResult = await this.createNote(this.plugin.mediaTypeManager.getFileName(mediaTypeModel), fileContentResult.value, options); + if (!targetFileResult.ok) { + return targetFileResult; + } + + if (this.plugin.settings.enableTemplaterIntegration) { + const templaterResult = await this.attempt(() => useTemplaterPluginInFile(this.plugin.app, targetFileResult.value), { + kind: AppErrorKind.Unexpected, + message: 'Failed to apply templater to the note', + userMessage: 'Failed to apply templater to the note', + }); + if (!templaterResult.ok) { + return templaterResult; + } + } + + return ok(undefined); + } + + async updateActiveNote(onlyMetadata: boolean = false): Promise { + const activeFile = this.plugin.app.workspace.getActiveFile() ?? undefined; + if (!activeFile) { + throw new Error('MDB | there is no active note'); + } + + let metadata = this.getMetadataFromFileCache(activeFile); + metadata = this.plugin.modelPropertyMapper.convertObjectBack(metadata); + + Logger.debug(`MDB | read metadata`, metadata); + + if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { + throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); + } + + const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; + Logger.debug(`MDB | validOldMetadata`, validOldMetadata); + + const oldMediaTypeModel = this.plugin.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); + Logger.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); + + const newMediaTypeModelResult = await this.plugin.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); + if (!newMediaTypeModelResult.ok) { + this.plugin.errorReporter.report(newMediaTypeModelResult.error); + return; + } + + let newMediaTypeModel = newMediaTypeModelResult.value; + if (!newMediaTypeModel) { + return; + } + + newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); + Logger.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); + + if (onlyMetadata) { + const result = await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); + if (!result.ok) { + this.plugin.errorReporter.report(result.error); + } + } else { + const result = await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true }); + if (!result.ok) { + this.plugin.errorReporter.report(result.error); + } + } + } + + generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { + const fileMetadata = this.plugin.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + return stringifyYaml(fileMetadata); + } + + async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + let template = await this.plugin.mediaTypeManager.getTemplate(mediaTypeModel, this.plugin.app); + let fileMetadata: Record; + + if (this.plugin.settings.useDefaultFrontMatter) { + fileMetadata = this.plugin.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + } else { + fileMetadata = { + id: mediaTypeModel.id, + type: mediaTypeModel.type, + dataSource: mediaTypeModel.dataSource, + }; + } + + let fileContent = ''; + template = options.attachTemplate ? template : ''; + + ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); + ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); + + if (this.plugin.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.plugin.app)) { + fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + } else { + fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + } + + return fileContent; + } + + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { + if (!fileToAttach) { + return { fileMetadata, fileContent }; + } + + const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); + fileMetadata = { ...attachFileMetadata, ...fileMetadata }; + + let attachFileContent = await this.plugin.app.vault.read(fileToAttach); + const regExp = new RegExp(this.frontMatterRegExpPattern); + attachFileContent = attachFileContent.replace(regExp, ''); + attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; + fileContent += attachFileContent; + + return { fileMetadata, fileContent }; + } + + async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { + if (!template) { + return { fileMetadata, fileContent }; + } + + const templateMetadata = this.getMetaDataFromFileContent(template); + fileMetadata = { ...templateMetadata, ...fileMetadata }; + + const regExp = new RegExp(this.frontMatterRegExpPattern); + const attachFileContent = template.replace(regExp, ''); + fileContent += attachFileContent; + + return { fileMetadata, fileContent }; + } + + getMetaDataFromFileContent(fileContent: string): Metadata { + const regExp = new RegExp(this.frontMatterRegExpPattern); + const frontMatterRegExpResult = regExp.exec(fileContent); + if (!frontMatterRegExpResult) { + return {}; + } + + let frontMatter = frontMatterRegExpResult[0]; + if (!frontMatter) { + return {}; + } + + frontMatter = frontMatter.substring(4); + frontMatter = frontMatter.substring(0, frontMatter.length - 3); + + let metadata = parseYaml(frontMatter) as Metadata; + if (!metadata) { + metadata = {}; + } + + Logger.debug(`MDB | metadata read from file content`, metadata); + + return metadata; + } + + getMetadataFromFileCache(file: TFile): Metadata { + const metadata: Metadata | undefined = this.plugin.app.metadataCache.getFileCache(file)?.frontmatter; + return structuredClone(metadata ?? {}); + } + + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise> { + const folder = options.folder ?? this.plugin.app.vault.getAbstractFileByPath('/'); + + if (!folder || !(folder instanceof TFolder)) { + return err({ kind: AppErrorKind.Validation, message: 'MDB | invalid folder', userMessage: 'MDB | invalid folder' }); + } + + fileName = replaceIllegalFileNameCharactersInString(fileName); + const filePath = `${folder.path}/${fileName}.md`; + + const file = this.plugin.app.vault.getAbstractFileByPath(filePath); + if (file) { + const shouldOverwrite = await new Promise(resolve => { + new ConfirmOverwriteModal(this.plugin.app, fileName, resolve).open(); + }); + + if (!shouldOverwrite) { + return err({ kind: AppErrorKind.Cancelled, message: 'MDB | file creation cancelled by user', userMessage: 'MDB | file creation cancelled by user' }); + } + + await this.plugin.app.fileManager.trashFile(file); + } + + const targetFile = await this.plugin.app.vault.create(filePath, fileContent); + Logger.debug(`MDB | created new file at ${filePath}`); + + if (options.openNote) { + const activeLeaf = this.plugin.app.workspace.getLeaf(false); + if (!activeLeaf) { + Logger.warn('MDB | no active leaf, not opening newly created note'); + return ok(targetFile); + } + await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); + } + + return ok(targetFile); + } + + private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise> { + if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { + const imageUrl = mediaTypeModel.image; + const imageResult = await this.attempt( + async () => { + const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg'; + const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`; + const imagePath = normalizePath(`${this.plugin.settings.imageFolder}/${imageFileName}`); + + if (!this.plugin.app.vault.getAbstractFileByPath(this.plugin.settings.imageFolder)) { + await this.plugin.app.vault.createFolder(this.plugin.settings.imageFolder); + } + + if (!this.plugin.app.vault.getAbstractFileByPath(imagePath)) { + const response = await requestUrl({ url: imageUrl, method: 'GET' }); + await this.plugin.app.vault.createBinary(imagePath, response.arrayBuffer); + } + + mediaTypeModel.image = `[[${imagePath}]]`; + }, + { + kind: AppErrorKind.Network, + message: 'MDB | Failed to download image', + userMessage: 'Failed to download image', + }, + ); + + if (!imageResult.ok) { + Logger.warn('MDB | Failed to download image:', imageResult.error); + return imageResult; + } + } + + return ok(undefined); + } + + private async attempt(operation: () => Promise | T, fallback: Omit): Promise> { + return await Promise.resolve() + .then(operation) + .then( + value => ok(value), + cause => err(toAppError(cause, fallback)), + ); + } +} diff --git a/src/utils/MediaType.ts b/packages/obsidian/src/utils/MediaType.ts similarity index 100% rename from src/utils/MediaType.ts rename to packages/obsidian/src/utils/MediaType.ts diff --git a/src/utils/MediaTypeManager.ts b/packages/obsidian/src/utils/MediaTypeManager.ts similarity index 82% rename from src/utils/MediaTypeManager.ts rename to packages/obsidian/src/utils/MediaTypeManager.ts index d968573..f3ef631 100644 --- a/src/utils/MediaTypeManager.ts +++ b/packages/obsidian/src/utils/MediaTypeManager.ts @@ -1,19 +1,20 @@ -import type { App, TFile } from 'obsidian'; +import type { App } from 'obsidian'; +import { TFile } from 'obsidian'; import { TFolder } from 'obsidian'; -import { BoardGameModel } from '../models/BoardGameModel'; -import { BookModel } from '../models/BookModel'; -import { ComicMangaModel } from '../models/ComicMangaModel'; -import { GameModel } from '../models/GameModel'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { MovieModel } from '../models/MovieModel'; -import { MusicReleaseModel } from '../models/MusicReleaseModel'; -import { SeasonModel } from '../models/SeasonModel'; -import { SeriesModel } from '../models/SeriesModel'; -import { WikiModel } from '../models/WikiModel'; -import type { MediaDbPluginSettings } from '../settings/Settings'; -import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; -import { MediaType } from './MediaType'; -import { replaceTags } from './Utils'; +import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel'; +import { BookModel } from 'packages/obsidian/src/models/BookModel'; +import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel'; +import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import { WikiModel } from 'packages/obsidian/src/models/WikiModel'; +import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings'; +import { ILLEGAL_FILENAME_CHARACTERS } from 'packages/obsidian/src/utils/IllegalFilenameCharactersList'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { replaceTags } from 'packages/obsidian/src/utils/Utils'; // All media types in alphabetical order export const MEDIA_TYPES: MediaType[] = [ @@ -110,16 +111,16 @@ export class MediaTypeManager { } } - const template = await app.vault.cachedRead(templateFile as TFile); - // console.log(template); + if (!(templateFile instanceof TFile)) { + return ''; + } + + const template = await app.vault.cachedRead(templateFile); return replaceTags(template, mediaTypeModel); } async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise { - let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); - - folderPath ??= `/`; - // console.log(folderPath); + let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()) ?? '/'; if (!(await app.vault.adapter.exists(folderPath))) { await app.vault.createFolder(folderPath); diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts new file mode 100644 index 0000000..89a1b24 --- /dev/null +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -0,0 +1,282 @@ +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { MediaDbAdvancedSearchModal } from 'packages/obsidian/src/modals/MediaDbAdvancedSearchModal'; +import { MediaDbIdSearchModal } from 'packages/obsidian/src/modals/MediaDbIdSearchModal'; +import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal'; +import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; +import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Outcome } from 'packages/obsidian/src/utils/result'; +import { cancelled, failure, OutcomeStatus, skipped, success } from 'packages/obsidian/src/utils/result'; + +export interface SearchModalData { + query: string; + types: MediaType[]; +} + +export interface AdvancedSearchModalData { + query: string; + apis: string[]; +} + +export interface IdSearchModalData { + query: string; + api: string; +} + +export interface SelectModalData { + selected: MediaTypeModel[]; +} + +export interface PreviewModalData { + confirmed: boolean; +} + +export interface SearchModalOptions { + modalTitle?: string; + preselectedTypes?: MediaType[]; + prefilledSearchString?: string; +} + +export interface AdvancedSearchModalOptions { + modalTitle?: string; + preselectedAPIs?: string[]; + prefilledSearchString?: string; +} + +export interface IdSearchModalOptions { + modalTitle?: string; + preselectedAPI?: string; + prefilledSearchString?: string; +} + +export interface SelectModalOptions { + elements?: MediaTypeModel[]; + multiSelect?: boolean; + modalTitle?: string; + skipButton?: boolean; + description?: string; + submitButtonText?: string; +} + +export interface PreviewModalOptions { + modalTitle?: string; + elements?: MediaTypeModel[]; +} + +export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { + modalTitle: 'Media DB Search', + preselectedTypes: [], + prefilledSearchString: '', +}; + +export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = { + modalTitle: 'Media DB Advanced Search', + preselectedAPIs: [], + prefilledSearchString: '', +}; + +export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = { + modalTitle: 'Media DB Id Search', + preselectedAPI: undefined, + prefilledSearchString: '', +}; + +export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = { + modalTitle: 'Media DB Search Results', + elements: [], + multiSelect: true, + skipButton: false, +}; + +export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = { + modalTitle: 'Media DB Preview', + elements: [], +}; + +export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { + elements: [], + multiSelect: true, + modalTitle: '', + skipButton: false, + description: 'Select one or multiple search results.', + submitButtonText: 'Ok', +}; + +interface ModalCoreResult { + modalResult: Outcome; + modal: TModal; +} + +export class ModalHelper { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + private async openModalCore( + createModal: () => TModal, + wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, + ): Promise> { + const modal = createModal(); + const modalResult = await new Promise>(resolve => { + wireHandlers(modal, resolve); + modal.open(); + }); + + return { modalResult, modal }; + } + + private async resolveOutcome(outcomePromise: Promise>): Promise { + const outcome = await outcomePromise; + + if (outcome.status === OutcomeStatus.Ok) { + return outcome.data; + } + + if (outcome.status === OutcomeStatus.Error) { + this.plugin.errorReporter.report(outcome.error); + } + + return undefined; + } + + async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbSearchModal(this.plugin, searchModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Search modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptSearchModal(searchModalOptions: SearchModalOptions): Promise { + return await this.resolveOutcome(this.createSearchModalOutcome(searchModalOptions)); + } + + async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise { + return await this.resolveOutcome(this.createAdvancedSearchModalOutcome(advancedSearchModalOptions)); + } + + async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Id search modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise { + return await this.resolveOutcome(this.createIdSearchModalOutcome(idSearchModalOptions)); + } + + async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setSkipCallback(() => resolve(skipped())); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Select modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) { + modal.close(); + } + + return modalResult; + } + + async promptSelectModal(selectModalOptions: SelectModalOptions): Promise { + return await this.resolveOutcome(this.createSelectModalOutcome(selectModalOptions)); + } + + async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbPreviewModal(this.plugin, previewModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Preview modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise { + return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions)); + } +} diff --git a/src/utils/Utils.ts b/packages/obsidian/src/utils/Utils.ts similarity index 96% rename from src/utils/Utils.ts rename to packages/obsidian/src/utils/Utils.ts index 9e4f286..cc5b2bd 100644 --- a/src/utils/Utils.ts +++ b/packages/obsidian/src/utils/Utils.ts @@ -1,13 +1,12 @@ import { iso6392 } from 'iso-639-2'; import type { TFile, TFolder, App } from 'obsidian'; import { requestUrl } from 'obsidian'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; export const mediaDbTag: string = 'mediaDB'; -export const mediaDbVersion: string = '0.5.2'; -export const debug: boolean = true; +export const mediaDbVersion: string = '0.8.0'; export function wrapAround(value: number, size: number): number { if (size <= 0) { @@ -153,10 +152,6 @@ export function markdownTable(content: string[][]): string { return table; } -export function fragWithHTML(html: string): DocumentFragment { - return createFragment(frag => (frag.createDiv().innerHTML = html)); -} - export function dateToString(date: Date): string { return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; } @@ -240,7 +235,7 @@ function getTemplaterPlugin(app: App): TemplaterPlugin | null { return null; } - return candidate as TemplaterPlugin; + return candidate; } export function hasTemplaterPlugin(app: App): boolean { diff --git a/packages/obsidian/src/utils/result.ts b/packages/obsidian/src/utils/result.ts new file mode 100644 index 0000000..64c71c3 --- /dev/null +++ b/packages/obsidian/src/utils/result.ts @@ -0,0 +1,52 @@ +export interface Ok { + ok: true; + value: T; +} +export interface Err { + ok: false; + error: E; +} +export type Result = Ok | Err; + +export const ok = (value: T): Ok => ({ ok: true, value }); +export const err = (error: E): Err => ({ ok: false, error }); + +export const isOk = (result: Result): result is Ok => result.ok; +export const isErr = (result: Result): result is Err => !result.ok; + +export const mapResult = (result: Result, mapper: (value: T) => U): Result => (result.ok ? ok(mapper(result.value)) : result); + +export const mapError = (result: Result, mapper: (error: E) => F): Result => (result.ok ? result : err(mapper(result.error))); + +export const andThen = (result: Result, binder: (value: T) => Result): Result => (result.ok ? binder(result.value) : result); + +export const tapError = (result: Result, sideEffect: (error: E) => void): Result => { + if (!result.ok) sideEffect(result.error); + return result; +}; + +export const fromPromise = async (promise: Promise, onError: (cause: unknown) => E): Promise> => { + try { + return ok(await promise); + } catch (cause) { + return err(onError(cause)); + } +}; + +export enum OutcomeStatus { + Ok = 'ok', + Cancelled = 'cancelled', + Skipped = 'skipped', + Error = 'error', +} + +export type Outcome = + | { status: OutcomeStatus.Ok; data: T } + | { status: OutcomeStatus.Cancelled } + | { status: OutcomeStatus.Skipped } + | { status: OutcomeStatus.Error; error: E }; + +export const cancelled = (): Outcome => ({ status: OutcomeStatus.Cancelled }); +export const skipped = (): Outcome => ({ status: OutcomeStatus.Skipped }); +export const success = (data: T): Outcome => ({ status: OutcomeStatus.Ok, data }); +export const failure = (error: E): Outcome => ({ status: OutcomeStatus.Error, error }); diff --git a/src/api/schemas/GiantBomb.json b/packages/schemas/src/GiantBomb.json similarity index 100% rename from src/api/schemas/GiantBomb.json rename to packages/schemas/src/GiantBomb.json diff --git a/src/api/schemas/GiantBomb.ts b/packages/schemas/src/GiantBomb.ts similarity index 100% rename from src/api/schemas/GiantBomb.ts rename to packages/schemas/src/GiantBomb.ts diff --git a/src/api/schemas/MALAPI.ts b/packages/schemas/src/MALAPI.ts similarity index 100% rename from src/api/schemas/MALAPI.ts rename to packages/schemas/src/MALAPI.ts diff --git a/src/api/schemas/OpenLibrary.json b/packages/schemas/src/OpenLibrary.json similarity index 100% rename from src/api/schemas/OpenLibrary.json rename to packages/schemas/src/OpenLibrary.json diff --git a/src/api/schemas/OpenLibrary.ts b/packages/schemas/src/OpenLibrary.ts similarity index 100% rename from src/api/schemas/OpenLibrary.ts rename to packages/schemas/src/OpenLibrary.ts diff --git a/src/api/schemas/TMDB.ts b/packages/schemas/src/TMDB.ts similarity index 100% rename from src/api/schemas/TMDB.ts rename to packages/schemas/src/TMDB.ts diff --git a/packages/schemas/src/fetchSchemas.sh b/packages/schemas/src/fetchSchemas.sh new file mode 100644 index 0000000..e5793ee --- /dev/null +++ b/packages/schemas/src/fetchSchemas.sh @@ -0,0 +1,15 @@ +#! /bin/env bash + +# IMPORTANT: needs to be ran from this directory, otherwise the output files will be generated in the wrong place + +# https://docs.api.jikan.moe/ +bun openapi-typescript https://raw.githubusercontent.com/jikan-me/jikan-rest/master/storage/api-docs/api-docs.json -o ./MALAPI.ts + +# https://www.giantbomb.com/forums/api-developers-3017/giant-bomb-openapi-specification-1901269/ +bun openapi-typescript ./GiantBomb.json -o ./GiantBomb.ts + +# https://github.com/internetarchive/openlibrary-api/blob/main/swagger.yaml +bun openapi-typescript ./OpenLibrary.json -o ./OpenLibrary.ts + +# https://developer.themoviedb.org/openapi +bun openapi-typescript https://developer.themoviedb.org/openapi/tmdb-api.json -o ./TMDB.ts \ No newline at end of file diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts deleted file mode 100644 index 00dca1a..0000000 --- a/src/api/APIManager.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Notice } from 'obsidian'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { APIModel } from './APIModel'; - -export class APIManager { - apis: APIModel[]; - - constructor() { - this.apis = []; - } - - /** - * Queries the basic info for one query string and multiple APIs. - * - * @param query - * @param apisToQuery - */ - async query(query: string, apisToQuery: string[]): Promise { - console.debug(`MDB | api manager queried with "${query}"`); - - const promises = this.apis - .filter(api => apisToQuery.includes(api.apiName)) - .map(async api => { - try { - return await api.searchByTitle(query); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return []; - } - }); - - return (await Promise.all(promises)).flat(); - } - - /** - * Queries detailed information for a MediaTypeModel. - * - * @param item - */ - async queryDetailedInfo(item: MediaTypeModel): Promise { - return await this.queryDetailedInfoById(item.id, item.dataSource); - } - - /** - * Queries detailed info for an id from an API. - * - * @param id - * @param apiName - */ - async queryDetailedInfoById(id: string, apiName: string): Promise { - for (const api of this.apis) { - if (api.apiName === apiName) { - try { - return await api.getById(id); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return undefined; - } - } - } - - return undefined; - } - - getApiByName(name: string): APIModel | undefined { - for (const api of this.apis) { - if (api.apiName === name) { - return api; - } - } - - return undefined; - } - - registerAPI(api: APIModel): void { - this.apis.push(api); - } -} diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts deleted file mode 100644 index bddddb7..0000000 --- a/src/api/apis/BoardGameGeekAPI.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { BoardGameModel } from '../../models/BoardGameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -export class BoardGameGeekAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'BoardGameGeekAPI'; - this.apiDescription = 'A free API for BoardGameGeek things.'; - this.apiUrl = 'https://boardgamegeek.com/xmlapi/'; - this.types = [MediaType.BoardGame]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - Authorization: `Bearer ${key}`, - }, - }); - - if (fetchData.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = fetchData.text; - const response = new window.DOMParser().parseFromString(data, 'text/xml'); - - // console.debug(response); - - 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 ?? undefined; - const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; - - ret.push( - new BoardGameModel({ - dataSource: this.apiName, - id, - title, - englishTitle: title, - year, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - Authorization: `Bearer ${key}`, - }, - }); - - if (fetchData.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = fetchData.text; - const response = new window.DOMParser().parseFromString(data, 'text/xml'); - // console.debug(response); - - 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) - .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) - .filter(n => n !== null); - - return new BoardGameModel({ - title: title ?? undefined, - englishTitle: title ?? undefined, - year: year === '0' ? '' : year, - dataSource: this.apiName, - url: `https://boardgamegeek.com/boardgame/${id}`, - id: id, - - genres: genres, - onlineRating: onlineRating, - complexityRating: complexityRating, - minPlayers: minPlayers, - maxPlayers: maxPlayers, - playtime: playtime, - publishers: publishers, - image: image, - - released: true, - - userData: { - played: false, - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts deleted file mode 100644 index da7e7ad..0000000 --- a/src/api/apis/ComicVineAPI.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ - -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { ComicMangaModel } from '../../models/ComicMangaModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -export class ComicVineAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'ComicVineAPI'; - this.apiDescription = 'A free API for comic books.'; - this.apiUrl = 'https://comicvine.gamespot.com/api'; - this.types = [MediaType.ComicManga]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/search/?api_key=${key}&format=json&resources=volume&query=${encodeURIComponent(title)}`; - 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 ret: MediaTypeModel[] = []; - for (const result of data.results) { - ret.push( - new ComicMangaModel({ - title: result.name, - englishTitle: result.name, - year: result.start_year, - dataSource: this.apiName, - id: `4050-${result.id}`, - publishers: result.publisher?.name, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${key}&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; - const result = data.results; - - const authors = result.people as - | { - name: string; - }[] - | undefined; - - return new ComicMangaModel({ - type: MediaType.ComicManga, - title: result.name, - englishTitle: result.name, - alternateTitles: result.aliases, - plot: result.deck, - year: result.start_year, - dataSource: this.apiName, - url: result.site_detail_url, - id: `4050-${result.id}`, - - authors: authors?.map(x => x.name), - chapters: result.count_of_issues, - image: result.image?.original_url, - - released: true, - publishers: result.publisher?.name, - publishedFrom: result.start_year, - status: result.status, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.ComicVineAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts deleted file mode 100644 index 6d6d1ea..0000000 --- a/src/api/apis/GiantBombAPI.ts +++ /dev/null @@ -1,167 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { obsidianFetch } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/GiantBomb'; - -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`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.GiantBombKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); - const response = await client.GET('/games', { - params: { - query: { - api_key: key, - filter: `name:${title}`, - format: 'json', - limit: 20, - }, - }, - fetch: obsidianFetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.results; - - const ret: MediaTypeModel[] = []; - for (const result of data ?? []) { - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; - - ret.push( - new GameModel({ - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - id: result.guid?.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.GiantBombKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); - const response = await client.GET('/game/{guid}', { - params: { - path: { - guid: id, - }, - query: { - api_key: key, - format: 'json', - }, - }, - fetch: obsidianFetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.results; - - if (!result) { - throw Error(`MDB | No results found for ID ${id} in ${this.apiName}.`); - } - - console.log(result); - - // sadly the only OpenAPI definition I could find doesn't have the right types - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; - const developers = result.developers as - | { - name: string; - }[] - | undefined; - const publishers = result.publishers as - | { - name: string; - }[] - | undefined; - const genres = result.genres as - | { - name: string; - }[] - | undefined; - const image = result.image as - | { - small_url: string; - medium_url: string; - super_url: string; - } - | undefined; - - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - url: result.site_detail_url, - id: result.guid?.toString(), - developers: developers?.map(x => x.name), - publishers: publishers?.map(x => x.name), - genres: genres?.map(x => x.name), - onlineRating: 0, - image: image?.super_url, - - released: true, - releaseDate: result.original_release_date, - - userData: { - played: false, - - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.GiantBombAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts deleted file mode 100644 index 14094d0..0000000 --- a/src/api/apis/IGDBAPI.ts +++ /dev/null @@ -1,159 +0,0 @@ -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'; - -interface IGDBCover { - url: string; -} - -interface IGDBGenre { - name: string; -} - -interface IGDBCompany { - name: string; -} - -interface IGDBInvolvedCompany { - company: IGDBCompany; - developer: boolean; - publisher: boolean; -} - -interface IGDBGame { - id: number; - name: string; - cover?: IGDBCover; - first_release_date?: number; - summary?: string; - total_rating?: number; - url?: string; - genres?: IGDBGenre[]; - involved_companies?: IGDBInvolvedCompany[]; -} - -interface TwitchAuthResponse { - access_token: string; - expires_in: number; -} - -export class IGDBAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - private accessToken: string = ''; - private tokenExpiry: number = 0; - - constructor(plugin: MediaDbPlugin) { - super(); - this.plugin = plugin; - this.apiName = 'IGDBAPI'; - this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).'; - this.apiUrl = 'https://api.igdb.com/v4'; - this.types = [MediaType.Game]; - } - - private async getAuthToken(): Promise { - const currentTime = Date.now(); - if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret); - - if (!clientId || !clientSecret) { - throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); - } - console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); - const response = await requestUrl({ - url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, - method: 'POST', - }); - if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); - const data = response.json as TwitchAuthResponse; - this.accessToken = data.access_token; - this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; - return this.accessToken; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); - const token = await this.getAuthToken(); - const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; - const response = await requestUrl({ - url: `${this.apiUrl}/games`, - method: 'POST', - headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, - body: queryBody, - }); - if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - - const data = response.json as IGDBGame[]; - return data.map(result => { - const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - id: result.id.toString(), - image: image, - }); - }); - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); - const token = await this.getAuthToken(); - const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; - const response = await requestUrl({ - url: `${this.apiUrl}/games`, - method: 'POST', - headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, - body: queryBody, - }); - if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - - const data = response.json as IGDBGame[]; - if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); - const result = data[0]; - - const developers: string[] = []; - const publishers: string[] = []; - result.involved_companies?.forEach(c => { - if (c.developer) developers.push(c.company.name); - if (c.publisher) publishers.push(c.company.name); - }); - const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; - - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', - dataSource: this.apiName, - url: result.url, - id: result.id.toString(), - developers: developers, - publishers: publishers, - genres: result.genres?.map(g => g.name) ?? [], - onlineRating: result.total_rating, - image: image, - released: true, - releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', - userData: { played: false, personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? []; - } -} diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts deleted file mode 100644 index 3083e23..0000000 --- a/src/api/apis/MALAPI.ts +++ /dev/null @@ -1,229 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { SeriesModel } from '../../models/SeriesModel'; -import { MediaType } from '../../utils/MediaType'; -import { isTruthy, obsidianFetch } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/MALAPI'; - -export class MALAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MALAPI'; - this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; - this.apiUrl = 'https://jikan.moe/'; - this.types = [MediaType.Movie, MediaType.Series]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - this.typeMappings.set('special', 'special'); - this.typeMappings.set('tv', 'series'); - this.typeMappings.set('ova', 'ova'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/anime', { - params: { - query: { - q: title, - limit: 20, - sfw: this.plugin.settings.sfwFilter ? true : false, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.data; - - const ret: MediaTypeModel[] = []; - - for (const result of data ?? []) { - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString() ?? ''; - const id = result.mal_id?.toString(); - - if (type === undefined) { - ret.push( - new MovieModel({ - subType: '', - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } - if (type === 'movie' || type === 'special') { - ret.push( - new MovieModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } else if (type === 'series' || type === 'ova') { - ret.push( - new SeriesModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/anime/{id}/full', { - params: { - path: { - id: id as unknown as number, // This is fine - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.data; - - if (result === undefined) { - throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); - } - - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString(); - const new_id = result.mal_id?.toString(); - - if (type === undefined) { - return new MovieModel({ - subType: undefined, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: result.duration, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - if (type === 'movie' || type === 'special') { - return new MovieModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: result.duration, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'series' || type === 'ova') { - return new SeriesModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - episodes: result.episodes, - duration: result.duration, - onlineRating: result.score, - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat), - airing: result.airing, - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - throw new Error(`MDB | Unknown media type for id ${id}`); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MALAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts deleted file mode 100644 index a8972b3..0000000 --- a/src/api/apis/MobyGamesAPI.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */ - -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'; - -// sadly no open api schema available - -// TODO: maybe we should remove this API, as it can no longer be tested without paying for an API key - -export class MobyGamesAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-DD-MM'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MobyGamesAPI'; - this.apiDescription = 'A free API for games.'; - this.apiUrl = 'https://api.mobygames.com/v1'; - this.types = [MediaType.Game]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.MobyGamesKeyId); - - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${key}`; - 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.games) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: result.title, - englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear().toString(), - dataSource: this.apiName, - id: result.game_id, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.MobyGamesKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${key}`; - 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.games[0]; - - return new GameModel({ - type: MediaType.Game, - title: result.title, - englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear().toString(), - dataSource: this.apiName, - url: `https://www.mobygames.com/game/${result.game_id}`, - id: result.game_id, - developers: [], - publishers: [], - genres: result.genres?.map((x: any) => x.genre_name) ?? [], - onlineRating: result.moby_score, - image: result.sample_cover?.image ?? '', - - released: true, - releaseDate: result.platforms[0].first_release_date ?? 'unknown', - - userData: { - played: false, - - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MobyGamesAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts deleted file mode 100644 index e1d0d7a..0000000 --- a/src/api/apis/MusicBrainzAPI.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MusicReleaseModel } from '../../models/MusicReleaseModel'; -import { MediaType } from '../../utils/MediaType'; -import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -interface Tag { - name: string; - count: number; -} -interface Genre { - name: string; - count: number; - id: string; - disambiguation: string; -} -interface Release { - id: string; - 'status-id': string; - title: string; - status: string; -} - -interface ArtistCredit { - name: string; - artist: { - tags: Tag[]; - type: string; - id: string; - name: string; - 'short-name': string; - country: string; - }; -} - -interface SearchResponse { - id: string; - 'type-id': string; - score: number; - 'primary-type-id': string; - 'artists-credit-id': string; - count: number; - title: string; - 'first-release-date': string; - 'primary-type': string; - 'artist-credit': ArtistCredit[]; - releases: Release[]; - tags: Tag[]; -} - -interface IdResponse { - id: string; - tags: Tag[]; - 'primary-type-id': string; - 'artist-credit': ArtistCredit[]; - title: string; - genres: Genre[]; - 'first-release-date': string; - releases: Release[]; - 'primary-type': string; - rating: { - value: number; - 'votes-count': number; - }; -} - -interface MediaResponse { - media: { - 'track-count': number; - tracks: { - 'artist-credit': ArtistCredit[]; - length: number | null; - number: string; - position: number; - title: string; - recording: { - length: number; - title: string; - }; - }[]; - }[]; - 'text-representation': { - language: string; - script: string; - }; -} - -export class MusicBrainzAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MusicBrainz API'; - this.apiDescription = 'Free API for music albums.'; - this.apiUrl = 'https://musicbrainz.org/'; - this.types = [MediaType.MusicRelease]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; - - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - // console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json) as { - 'release-groups': SearchResponse[]; - }; - // console.debug(data); - const ret: MediaTypeModel[] = []; - - for (const result of data['release-groups']) { - ret.push( - new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), - releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, - url: 'https://musicbrainz.org/release-group/' + result.id, - id: result.id, - image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', - - artists: result['artist-credit'].map(a => a.name), - subType: result['primary-type'], - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - // Fetch release group - const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`; - const groupResponse = await requestUrl({ - url: groupUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - if (groupResponse.status !== 200) { - throw Error(`MDB | Received status code ${groupResponse.status} from ${this.apiName}.`); - } - - const result = (await groupResponse.json) as IdResponse; - - // Get ID of the first release - const firstRelease = result.releases?.[0]; - if (!firstRelease) { - throw Error('MDB | No releases found in release group.'); - } - - // Fetch recordings for the first release - const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; - console.log(`MDB | Fetching release recordings from: ${releaseUrl}`); - - const releaseResponse = await requestUrl({ - url: releaseUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - if (releaseResponse.status !== 200) { - throw Error(`MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`); - } - - const releaseData = (await releaseResponse.json) as MediaResponse; - const tracks = extractTracksFromMedia(releaseData.media); - - // Calculate total album length for the first release - const totalrawLength = - releaseData.media[0]?.tracks.reduce((sum, track) => { - const len = track.length ?? track.recording?.length; - return typeof len === 'number' && !isNaN(len) ? sum + len : sum; - }, 0) ?? 0; - const albumLengthCalc = millisecondsToMinutes(totalrawLength); - - console.log(releaseData); - - return new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), - releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, - url: 'https://musicbrainz.org/release-group/' + result.id, - id: result.id, - image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', - - artists: result['artist-credit'].map(a => a.name), - language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown', - genres: result.genres.map(g => g.name), - subType: result['primary-type'], - albumDuration: albumLengthCalc, - trackCount: releaseData.media[0]?.['track-count'] ?? 0, - tracks: tracks, - rating: result.rating.value * 2, - - userData: { - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; - } -} - -function extractTracksFromMedia(media: MediaResponse['media']): { - number: number; - title: string; - duration: string; - featuredArtists: string[]; -}[] { - if (!media || media.length === 0 || !media[0].tracks) return []; - - return media[0].tracks.map((track, index) => { - const title = track.title ?? track.recording?.title ?? 'Unknown Title'; - const rawLength = track.length ?? track.recording?.length; - const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; - const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; - - return { - number: index + 1, - title, - duration, - featuredArtists, - }; - }); -} - -function millisecondsToMinutes(milliseconds: number): string { - const minutes = Math.floor(milliseconds / 60000); - const seconds = Math.floor((milliseconds % 60000) / 1000); - return `${minutes}:${seconds.toString().padStart(2, '0')}`; -} diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts deleted file mode 100644 index 3862080..0000000 --- a/src/api/apis/OMDbAPI.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { requestUrl } from 'obsidian'; -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'; - -interface ErrorResponse { - Response: 'False'; - Error: string; -} - -type SearchResponse = - | { - Response: 'True'; - totalResults: string; - Search: { - Title: string; - Year: string; - Poster: string; - imdbID: string; - Type: string; - }[]; - } - | ErrorResponse; - -type IdResponse = - | { - Response: 'True'; - Title: string; - Year: string; - Rated: string; - Released: string; - Runtime: string; - Genre: string; - Director: string; - Writer: string; - Actors: string; - Plot: string; - Language: string; - Country: string; - Awards: string; - Poster: string; - Metascore: string; - imdbRating: string; - imdbVotes: string; - imdbID: string; - Type: string; - DVD: string; - BoxOffice: string; - Production: string; - Website: string; - } - | ErrorResponse; - -export class OMDbAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'DD MMM YYYY'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'OMDbAPI'; - this.apiDescription = 'A free API for Movies, Series and Games.'; - this.apiUrl = 'https://www.omdbapi.com/'; - this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - this.typeMappings.set('series', 'series'); - this.typeMappings.set('game', 'game'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); - - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${key}`, - method: 'GET', - }); - - if (response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.status !== 200) { - throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - } - - const data = response.json as SearchResponse | undefined; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.Response === 'False') { - if (data.Error === 'Movie not found!') { - return []; - } - - throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`); - } - if (!data.Search) { - return []; - } - - // console.debug(data.Search); - - const ret: MediaTypeModel[] = []; - - for (const result of data.Search) { - const type = this.typeMappings.get(result.Type.toLowerCase()); - if (type === undefined) { - continue; - } - if (type === 'movie') { - ret.push( - new MovieModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } else if (type === 'series') { - ret.push( - new SeriesModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } else if (type === 'game') { - ret.push( - new GameModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${key}`, - method: 'GET', - }); - - if (response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.status !== 200) { - throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - } - - const result = response.json as IdResponse | undefined; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (result.Response === 'False') { - throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); - } - - const type = this.typeMappings.get(result.Type.toLowerCase()); - if (type === undefined) { - throw Error(`${result.Type.toLowerCase()} is an unsupported type.`); - } - - if (type === 'movie') { - return new MovieModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - plot: result.Plot, - genres: result.Genre?.split(', '), - director: result.Director?.split(', '), - writer: result.Writer?.split(', '), - duration: result.Runtime, - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - actors: result.Actors?.split(', '), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - country: result.Country?.split(', '), - boxOffice: result.BoxOffice, - ageRating: result.Rated, - premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'series') { - return new SeriesModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - plot: result.Plot, - genres: result.Genre?.split(', '), - writer: result.Writer?.split(', '), - studio: [], - episodes: 0, - duration: result.Runtime, - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - actors: result.Actors?.split(', '), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - country: result.Country?.split(', '), - ageRating: result.Rated, - airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'game') { - return new GameModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - genres: result.Genre?.split(', '), - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - played: false, - personalRating: 0, - }, - }); - } - - throw new Error(`MDB | Unknown media type for id ${id}`); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.OMDbAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts deleted file mode 100644 index f42f791..0000000 --- a/src/api/apis/OpenLibraryAPI.ts +++ /dev/null @@ -1,161 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import { BookModel } from '../../models/BookModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { obsidianFetch } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/OpenLibrary'; - -interface SearchResponse { - editions: { - docs: { - key?: string; - title?: string; - cover_i?: number; - isbn?: string[]; - }[]; - }; - cover_i?: number; - has_fulltext?: boolean; - edition_count?: number; - title?: string; - author_name?: string[]; - first_publish_year?: number; - key: string; - description?: string; - - number_of_pages_median?: number; - isbn?: string[]; - ratings_average?: number; -} - -export class OpenLibraryAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'OpenLibraryAPI'; - this.apiDescription = 'A free API for books'; - this.apiUrl = 'https://openlibrary.org/'; - this.types = [MediaType.Book]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - - const response = await client.GET('/search.json', { - params: { - query: { - q: title, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data as { - docs: SearchResponse[]; - }; - - // console.debug(data); - - const ret: MediaTypeModel[] = []; - - for (const result of data.docs) { - ret.push( - new BookModel({ - title: result.title, - englishTitle: result.title, - year: result.first_publish_year?.toString() ?? 'unknown', - dataSource: this.apiName, - id: result.key, - author: result.author_name?.join(', '), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - - const response = await client.GET('/search.json', { - params: { - query: { - q: `${id}`, - fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data as { - docs: SearchResponse[]; - q?: string; - }; - - const result = data.docs[0]; - - let key = result.key; - let title = result.title; - let cover_i = result.cover_i; - let isbnArr = result.isbn; - - // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present - const q = data.q ?? ''; - if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { - const edition = result.editions.docs[0]; - key = edition.key ?? key; - title = edition.title ?? title; - cover_i = edition.cover_i ?? cover_i; - isbnArr = edition.isbn ?? isbnArr; - } - - const pages = Number(result.number_of_pages_median); - const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); - const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); - - return new BookModel({ - title: title, - year: result.first_publish_year?.toString() ?? 'unknown', - dataSource: this.apiName, - url: `https://openlibrary.org` + key, - id: key, - isbn: Number.isNaN(isbn) ? undefined : isbn, - isbn13: Number.isNaN(isbn13) ? undefined : isbn13, - englishTitle: title, - - author: result.author_name?.join(', '), - plot: result.description ?? undefined, - pages: Number.isNaN(pages) ? undefined : pages, - onlineRating: result.ratings_average, - image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, - - released: true, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts deleted file mode 100644 index ec0a53e..0000000 --- a/src/api/apis/RAWGAPI.ts +++ /dev/null @@ -1,105 +0,0 @@ -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'; - -interface RAWGGame { - id: number; - name: string; - released?: string; - background_image?: string; - name_original?: string; - website?: string; - slug?: string; - metacritic?: number; - developers?: { name: string }[]; - publishers?: { name: string }[]; - genres?: { name: string }[]; -} - -interface RAWGSearchResponse { - results: RAWGGame[]; -} - -export class RAWGAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - this.plugin = plugin; - this.apiName = 'RAWGAPI'; - this.apiDescription = 'A large open video game database.'; - this.apiUrl = 'https://api.rawg.io/api'; - this.types = [MediaType.Game]; - } - - async searchByTitle(title: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`, - method: 'GET', - }); - if (response.status !== 200) { - throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - } - - const data = response.json as RAWGSearchResponse; - return data.results.map( - result => - new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, - id: result.id.toString(), - image: result.background_image, - }), - ); - } - - async getById(id: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${key}`, - method: 'GET', - }); - if (response.status !== 200) { - throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - } - - const result = response.json as RAWGGame; - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name_original ?? result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, - url: result.website ?? `https://rawg.io/games/${result.slug}`, - id: result.id.toString(), - developers: result.developers?.map(d => d.name) ?? [], - publishers: result.publishers?.map(p => p.name) ?? [], - genres: result.genres?.map(g => g.name) ?? [], - onlineRating: result.metacritic, - image: result.background_image, - released: result.released != null, - releaseDate: result.released, - userData: { played: false, personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? []; - } -} diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts deleted file mode 100644 index 4f4e174..0000000 --- a/src/api/apis/TMDBMovieAPI.ts +++ /dev/null @@ -1,187 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -interface TMDBCreditMember { - name?: string | null; - job?: string | null; -} - -interface TMDBCreditsResponse { - credits?: { - cast?: TMDBCreditMember[]; - crew?: TMDBCreditMember[]; - }; -} - -function isNonEmptyString(value: unknown): value is string { - return typeof value === 'string' && value.length > 0; -} - -function getTopCastNames(credits: TMDBCreditsResponse['credits'], size: number): string[] { - return (credits?.cast ?? []) - .map(c => c.name) - .filter(isNonEmptyString) - .slice(0, size); -} - -function getCrewNamesByJob(credits: TMDBCreditsResponse['credits'], job: string): string[] { - return (credits?.crew ?? []) - .filter(c => c.job === job) - .map(c => c.name) - .filter(isNonEmptyString); -} - -export class TMDBMovieAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TMDBMovieAPI'; - this.apiDescription = 'A community built Movie DB.'; - this.apiUrl = 'https://www.themoviedb.org/'; - this.types = [MediaType.Movie]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/search/movie', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.total_results === 0 || !data.results) { - return []; - } - - // console.debug(data.results); - - const ret: MediaTypeModel[] = []; - - for (const result of data.results) { - ret.push( - new MovieModel({ - type: 'movie', - title: result.original_title, - englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: result.id.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/movie/{movie_id}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { movie_id: parseInt(id) }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // console.debug(result); - const credits = (result as TMDBCreditsResponse).credits; - - return new MovieModel({ - type: 'movie', - title: result.title, - englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', - premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, - url: `https://www.themoviedb.org/movie/${result.id}`, - id: result.id.toString(), - - plot: result.overview ?? '', - genres: result.genres?.map(g => g.name).filter(isNonEmptyString) ?? [], - writer: getCrewNamesByJob(credits, 'Screenplay'), - director: getCrewNamesByJob(credits, 'Director'), - studio: result.production_companies?.map(s => s.name).filter(isNonEmptyString) ?? [], - - duration: result.runtime?.toString() ?? 'unknown', - onlineRating: result.vote_average, - actors: getTopCastNames(credits, 5), - image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, - - released: ['Released'].includes(result.status!), - streamingServices: [], - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts deleted file mode 100644 index d4f9da6..0000000 --- a/src/api/apis/TMDBSeasonAPI.ts +++ /dev/null @@ -1,302 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { SeasonModel } from '../../models/SeasonModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -interface NamedEntity { - name?: string | null; -} - -interface CastMember { - name?: string | null; -} - -interface CreditsLike { - cast?: CastMember[] | null; -} - -function extractNames(items: (NamedEntity | null | undefined)[] | null | undefined): string[] { - if (!Array.isArray(items)) { - return []; - } - - return items.map(item => item?.name?.trim() ?? '').filter(name => name.length > 0); -} - -function getTopActorNames(credits: CreditsLike | null | undefined, limit: number = 5): string[] { - if (!credits || !Array.isArray(credits.cast)) { - return []; - } - - return credits.cast - .map(member => { - const name = member?.name; - return typeof name === 'string' ? name : ''; - }) - .filter(name => name.length > 0) - .slice(0, limit); -} - -export class TMDBSeasonAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TMDBSeasonAPI'; - this.apiDescription = 'A community built Series DB (seasons).'; - this.apiUrl = 'https://www.themoviedb.org/'; - this.types = [MediaType.Season]; - this.typeMappings = new Map(); - this.typeMappings.set('tv', 'season'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const searchResponse = await client.GET('/3/search/tv', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (searchResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (searchResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`); - } - - const searchData = searchResponse.data; - - if (!searchData?.results || searchData.total_results === 0) { - return []; - } - - const topResults = searchData.results.slice(0, 20); - - return await Promise.all( - topResults.map(async result => { - let totalSeasons = 0; - if (typeof result.id === 'number') { - try { - const detailsResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { series_id: result.id }, - }, - fetch: fetch, - }); - - if (detailsResponse.response.status === 200 && Array.isArray(detailsResponse.data?.seasons)) { - totalSeasons = detailsResponse.data.seasons.length; - } - } catch { - // Ignore detail errors and use 0 as fallback. - } - } - - return new SeasonModel({ - title: `${result.name ?? result.original_name ?? ''}`, - englishTitle: result.name ?? result.original_name ?? '', - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: result.id?.toString() ?? '', - seasonTitle: result.name ?? result.original_name ?? '', - seasonNumber: totalSeasons, - }); - }), - ); - } - - // Fetch all seasons for a given series - async getSeasonsForSeries(tvId: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const seriesResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { series_id: Number.parseInt(tvId, 10) }, - }, - fetch: fetch, - }); - - if (seriesResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seriesResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); - } - - const seriesData = seriesResponse.data; - const seriesName = seriesData?.name ?? ''; - - const ret: SeasonModel[] = []; - - if (Array.isArray(seriesData?.seasons)) { - for (const season of seriesData.seasons) { - const seasonNumber = season.season_number ?? 0; - const titleText = `${seriesName} - Season ${seasonNumber}`; - - ret.push( - new SeasonModel({ - title: titleText, - englishTitle: titleText, - year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: `${tvId}/season/${seasonNumber}`, - seasonTitle: season.name ?? titleText, - seasonNumber: seasonNumber, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - // Expect season ids like "12345/season/2" - const m = /^(\d+)\/season\/(\d+)$/.exec(id); - if (!m) { - throw Error(`MDB | Invalid season id "${id}". Expected format "/season/".`); - } - - const tvId = Number.parseInt(m[1], 10); - const seasonNumber = Number.parseInt(m[2], 10); - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - - // Fetch season details - const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { - series_id: tvId, - season_number: seasonNumber, - }, - }, - fetch: fetch, - }); - - if (seasonResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seasonResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`); - } - - const seasonData = seasonResponse.data; - if (!seasonData) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // Fetch parent series to build consistent titles and inherit fields - const seriesResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { series_id: tvId }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (seriesResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seriesResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); - } - - const seriesData = seriesResponse.data; - - if (!seriesData) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - const seriesName = seriesData?.name ?? ''; - const airDate = seasonData.air_date ?? ''; - const titleText = `${seriesName} - Season ${seasonData.season_number}`; - - // Get airedTo as the air_date of the last episode, if available - let airedTo = 'unknown'; - if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) { - const lastEp = seasonData.episodes[seasonData.episodes.length - 1]; - if (lastEp?.air_date) airedTo = lastEp.air_date; - } - const formattedAiredTo = airedTo === 'unknown' ? 'unknown' : (this.plugin.dateFormatter.format(airedTo, this.apiDateFormat) ?? airedTo); - - return new SeasonModel({ - title: titleText, - englishTitle: titleText, - year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - url: `https://www.themoviedb.org/tv/${tvId.toString()}/season/${seasonData.season_number}`, - id: `${tvId.toString()}/season/${seasonData.season_number}`, - seasonTitle: seasonData.name ?? titleText, - seasonNumber: seasonData.season_number ?? seasonNumber, - episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, - airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', - airedTo: formattedAiredTo, - plot: seasonData.overview ?? '', - image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', - genres: extractNames(seriesData.genres), - writer: extractNames(seriesData.created_by), - studio: extractNames(seriesData.production_companies), - duration: seriesData.episode_run_time?.[0]?.toString() ?? '', - onlineRating: seasonData.vote_average ?? 0, - actors: getTopActorNames((seriesData as { credits?: CreditsLike }).credits), - released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), - streamingServices: [], - airing: ['Returning Series'].includes(seriesData.status ?? ''), - userData: { watched: false, lastWatched: '', personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return []; - } -} diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts deleted file mode 100644 index cb6ce28..0000000 --- a/src/api/apis/TMDBSeriesAPI.ts +++ /dev/null @@ -1,179 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { SeriesModel } from '../../models/SeriesModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -interface TMDBCreditMember { - name?: string | null; -} - -interface TMDBCreditsResponse { - credits?: { - cast?: TMDBCreditMember[]; - }; -} - -function isNonEmptyString(value: unknown): value is string { - return typeof value === 'string' && value.length > 0; -} - -function getTopCastNames(credits: TMDBCreditsResponse['credits'], size: number): string[] { - return (credits?.cast ?? []) - .map(c => c.name) - .filter(isNonEmptyString) - .slice(0, size); -} - -export class TMDBSeriesAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TMDBSeriesAPI'; - this.apiDescription = 'A community built Series DB.'; - this.apiUrl = 'https://www.themoviedb.org/'; - this.types = [MediaType.Series]; - this.typeMappings = new Map(); - this.typeMappings.set('tv', 'series'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - if (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/search/tv', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.total_results === 0 || !data.results) { - return []; - } - - // console.debug(data.results); - - const ret: MediaTypeModel[] = []; - - for (const result of data.results) { - ret.push( - new SeriesModel({ - type: 'series', - title: result.original_name, - englishTitle: result.name, - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: result.id.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { series_id: parseInt(id) }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // console.debug(result); - const credits = (result as TMDBCreditsResponse).credits; - - return new SeriesModel({ - type: 'series', - title: result.original_name, - englishTitle: result.name, - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - url: `https://www.themoviedb.org/tv/${result.id}`, - id: result.id.toString(), - - plot: result.overview ?? '', - genres: result.genres?.map(g => g.name).filter(isNonEmptyString) ?? [], - writer: result.created_by?.map(c => c.name).filter(isNonEmptyString) ?? [], - studio: result.production_companies?.map(s => s.name).filter(isNonEmptyString) ?? [], - episodes: result.number_of_episodes, - duration: result.episode_run_time?.[0]?.toString() ?? 'unknown', - onlineRating: result.vote_average, - actors: getTopCastNames(credits, 5), - image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null, - - released: ['Returning Series', 'Cancelled', 'Ended'].includes(result.status!), - streamingServices: [], - airing: ['Returning Series'].includes(result.status!), - airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown', - airedTo: ['Returning Series'].includes(result.status!) ? 'unknown' : (this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown'), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts deleted file mode 100644 index 0139d4e..0000000 --- a/src/api/apis/VNDBAPI.ts +++ /dev/null @@ -1,264 +0,0 @@ -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'; - -enum VNDevStatus { - Finished, - InDevelopment, - Cancelled, -} - -enum TagSpoiler { - None, - Minor, - Major, -} - -enum TagCategory { - Content = 'cont', - Sexual = 'ero', - Technical = 'tech', -} - -/** - * A partial `POST /vn` response payload; desired fields should be listed in the request body. - */ -interface VNJSONResponse { - more: boolean; - results: [ - { - id: string; - title: string; - titles: [ - { - title: string; - lang: string; - }, - ]; - devstatus: VNDevStatus; - released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents - image: { - url: string; - sexual: number; - } | null; - rating: number | null; - tags: [ - { - id: string; - name: string; - category: TagCategory; - rating: number; - spoiler: TagSpoiler; - }, - ]; - developers: [ - { - id: string; - name: string; - }, - ]; - }, - ]; -} - -/** - * A partial `POST /release` response payload; desired fields should be listed in the request body. - */ -interface ReleaseJSONResponse { - more: boolean; - results: [ - { - id: string; - producers: [ - { - id: string; - name: string; - developer: boolean; - publisher: boolean; - }, - ]; - }, - ]; -} - -export class VNDBAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'VNDB API'; - this.apiDescription = 'A free API for visual novels.'; - this.apiUrl = 'https://api.vndb.org/kana'; - this.types = [MediaType.Game]; - } - - /** - * Make a `POST` request to the VNDB API. - * @param endpoint The API endpoint to query. E.g. "/vn". - * @param body A JSON object defining the query, following the VNDB API structure. - * @returns A JSON object representing the query response. - * @throws Error The request returned an unsuccessful or unexpected HTTP status code. - * @see {@link https://api.vndb.org/kana#api-structure} - */ - private async postQuery(endpoint: string, body: string): Promise { - const fetchData = await requestUrl({ - url: `${this.apiUrl}${endpoint}`, - method: 'POST', - contentType: 'application/json', - body: body, - throw: false, - }); - - if (fetchData.status !== 200) { - switch (fetchData.status) { - case 400: - throw Error(`MDB | Invalid request body or query [${fetchData.text}].`); - case 404: - throw Error(`MDB | Invalid API path or HTTP method.`); - case 429: - throw Error(`MDB | Throttled.`); - case 500: - throw Error(`MDB | VNDB server error.`); - case 502: - throw Error(`MDB | VNDB server is down.`); - default: - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - } - - return fetchData.json; - } - - /** - * Make a `POST` request to the `/vn` endpoint. - * Queries visual novel entries. - * @see {@link https://api.vndb.org/kana#post-vn} - */ - private postVNQuery(body: string): Promise { - return this.postQuery('/vn', body) as Promise; - } - - /** - * Make a `POST` request to the `/release` endpoint. - * Queries release entries. - * @see {@link https://api.vndb.org/kana#post-release} - */ - private postReleaseQuery(body: string): Promise { - return this.postQuery('/release', body) as Promise; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - /* SFW Filter: has ANY official&&complete&&standalone&&SFW release - OR has NO official&&standalone&&NSFW release - OR has the `In-game Sexual Content Toggle` (g2708) tag */ - // prettier-ignore - const vnData = await this.postVNQuery(`{ - "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : - `, ["or" - , ["release", "=", ["and" - , ["official", "=", "1"] - , ["rtype", "=", "complete"] - , ["patch", "!=", "1"] - , ["has_ero", "!=", "1"] - ]] - , ["release", "!=", ["and" - , ["official", "=", "1"] - , ["patch", "!=", "1"] - , ["has_ero", "=", "1"] - ]] - , ["tag", "=", "g2708"] - ]`} - , ["search", "=", "${title}"] - ], - "fields": "title, titles{title, lang}, released", - "sort": "searchrank", - "results": 20 - }`); - - const ret: MediaTypeModel[] = []; - for (const vn of vnData.results) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: vn.title, - englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', - dataSource: this.apiName, - id: vn.id, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const vnData = await this.postVNQuery(`{ - "filters": ["id", "=", "${id}"], - "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" - }`); - - if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); - const vn = vnData.results[0]; - const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; - vn.released ??= 'Unknown'; - - const releaseData = await this.postReleaseQuery(`{ - "filters": ["and" - , ["vn", "=" - , ["id", "=", "${id}"] - ] - , ["official", "=", 1] - ], - "fields": "producers.name, producers.publisher, producers.developer", - "results": 100 - }`); - - return new GameModel({ - type: MediaType.Game, - title: vn.title, - englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: releasedIsDate ? new Date(vn.released).getFullYear().toString() : vn.released, - dataSource: this.apiName, - url: `https://vndb.org/${vn.id}`, - id: vn.id, - - developers: vn.developers.map(d => d.name), - publishers: releaseData.results - .flatMap(r => r.producers) - .filter(p => p.publisher) - .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list - .map(p => p.name) - .unique(), - genres: vn.tags - .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) - .sort((t1, t2) => t2.rating - t1.rating) - .map(t => t.name), - onlineRating: vn.rating ?? NaN, - // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively - image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, - - released: vn.devstatus === VNDevStatus.Finished, - releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, - - userData: { - played: false, - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.VNDBAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts deleted file mode 100644 index 9b3b289..0000000 --- a/src/api/apis/WikipediaAPI.ts +++ /dev/null @@ -1,112 +0,0 @@ -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'; - -interface SearchResponse { - query: { - search: { - title: string; - pageid: number; - }[]; - }; -} - -interface IdResponse { - query: { - pages: Record; - }; -} - -interface WikipediaPage { - pageid: number; - title: string; - contentmodel: string; - pagelanguage: string; - pagelanguagehtmlcode: string; - pagelanguagedir: string; - touched: string; // ISO date string - lastrevid: number; - length: number; - fullurl: string; - editurl: string; - canonicalurl: string; -} -export class WikipediaAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'Wikipedia API'; - this.apiDescription = 'The API behind Wikipedia'; - this.apiUrl = 'https://www.wikipedia.com'; - this.types = [MediaType.Wiki]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; - const fetchData = await fetch(searchUrl); - // console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json()) as SearchResponse; - console.debug(data); - const ret: MediaTypeModel[] = []; - - for (const result of data.query.search) { - ret.push( - new WikiModel({ - type: 'wiki', - title: result.title, - englishTitle: result.title, - year: '', - dataSource: this.apiName, - id: result.pageid.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; - const fetchData = await fetch(searchUrl); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json()) as IdResponse; - // console.debug(data); - const result = Object.values(data?.query?.pages)[0]; - - return new WikiModel({ - title: result.title, - englishTitle: result.title, - dataSource: this.apiName, - url: result.fullurl, - id: result.pageid.toString(), - - wikiUrl: result.fullurl, - lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), - length: result.length, - - userData: {}, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.WikipediaAPI_disabledMediaTypes; - } -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 8a83e92..0000000 --- a/src/main.ts +++ /dev/null @@ -1,743 +0,0 @@ -import type { TFile } from 'obsidian'; -import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian'; -import { requestUrl, normalizePath } from 'obsidian'; -import { APIManager } from './api/APIManager'; -import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; -import { ComicVineAPI } from './api/apis/ComicVineAPI'; -import { GiantBombAPI } from './api/apis/GiantBombAPI'; -import { IGDBAPI } from './api/apis/IGDBAPI'; -import { MALAPI } from './api/apis/MALAPI'; -import { MALAPIManga } from './api/apis/MALAPIManga'; -import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; -import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; -import { OMDbAPI } from './api/apis/OMDbAPI'; -import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; -import { RAWGAPI } from './api/apis/RAWGAPI'; -import { SteamAPI } from './api/apis/SteamAPI'; -import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; -import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; -import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; -import { VNDBAPI } from './api/apis/VNDBAPI'; -import { WikipediaAPI } from './api/apis/WikipediaAPI'; -import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; -import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; -import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; -import type { MediaTypeModel } from './models/MediaTypeModel'; -import type { SeasonModel } from './models/SeasonModel'; -import { PropertyMapper } from './settings/PropertyMapper'; -import { PropertyMappingModel } from './settings/PropertyMapping'; -import type { MediaDbPluginSettings } from './settings/Settings'; -import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings'; -import { BulkImportHelper } from './utils/BulkImportHelper'; -import { DateFormatter } from './utils/DateFormatter'; -import type { MediaType } from './utils/MediaType'; -import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; -import type { SearchModalOptions } from './utils/ModalHelper'; -import { ModalHelper } from './utils/ModalHelper'; -import type { CreateNoteOptions } from './utils/Utils'; -import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; -import 'src/styles.css'; - -export type Metadata = Record; - -export interface MediaTypeModelObj { - id: string; - type: MediaType; - dataSource: string; -} - -export default class MediaDbPlugin extends Plugin { - settings!: MediaDbPluginSettings; - apiManager!: APIManager; - mediaTypeManager!: MediaTypeManager; - modelPropertyMapper!: PropertyMapper; - modalHelper!: ModalHelper; - bulkImportHelper!: BulkImportHelper; - dateFormatter!: DateFormatter; - - frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; - - async onload(): Promise { - this.apiManager = new APIManager(); - // register APIs - this.apiManager.registerAPI(new OMDbAPI(this)); - this.apiManager.registerAPI(new MALAPI(this)); - this.apiManager.registerAPI(new MALAPIManga(this)); - this.apiManager.registerAPI(new WikipediaAPI(this)); - this.apiManager.registerAPI(new MusicBrainzAPI(this)); - this.apiManager.registerAPI(new SteamAPI(this)); - this.apiManager.registerAPI(new TMDBSeriesAPI(this)); - this.apiManager.registerAPI(new TMDBSeasonAPI(this)); - this.apiManager.registerAPI(new TMDBMovieAPI(this)); - this.apiManager.registerAPI(new BoardGameGeekAPI(this)); - this.apiManager.registerAPI(new OpenLibraryAPI(this)); - this.apiManager.registerAPI(new ComicVineAPI(this)); - this.apiManager.registerAPI(new MobyGamesAPI(this)); - this.apiManager.registerAPI(new GiantBombAPI(this)); - this.apiManager.registerAPI(new IGDBAPI(this)); - this.apiManager.registerAPI(new RAWGAPI(this)); - this.apiManager.registerAPI(new VNDBAPI(this)); - - this.mediaTypeManager = new MediaTypeManager(); - this.modelPropertyMapper = new PropertyMapper(this); - this.modalHelper = new ModalHelper(this); - this.bulkImportHelper = new BulkImportHelper(this); - this.dateFormatter = new DateFormatter(); - - await this.loadSettings(); - // register the settings tab - this.addSettingTab(new MediaDbSettingTab(this.app, this)); - - this.mediaTypeManager.updateTemplates(this.settings); - this.mediaTypeManager.updateFolders(this.settings); - this.dateFormatter.setFormat(this.settings.customDateFormat); - - // add icon to the left ribbon - const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', () => this.createEntryWithAdvancedSearchModal()); - ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); - - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file) => { - if (file instanceof TFolder) { - menu.addItem(item => { - item.setTitle('Import folder as Media DB entries') - .setIcon('database') - .onClick(() => this.bulkImportHelper.import(file)); - }); - } - }), - ); - - // register command to open search modal - this.addCommand({ - id: 'open-media-db-search-modal', - name: 'Create Media DB entry', - callback: () => this.createEntryWithSearchModal(), - }); - for (const mediaType of MEDIA_TYPES) { - this.addCommand({ - id: `open-media-db-search-modal-with-${mediaType}`, - name: `Create Media DB entry (${unCamelCase(mediaType)})`, - callback: () => this.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), - }); - } - this.addCommand({ - id: 'open-media-db-advanced-search-modal', - name: 'Create Media DB entry (advanced search)', - callback: () => this.createEntryWithAdvancedSearchModal(), - }); - // register command to open id search modal - this.addCommand({ - id: 'open-media-db-id-search-modal', - name: 'Create Media DB entry by id', - callback: () => this.createEntryWithIdSearchModal(), - }); - // register command to update the open note - this.addCommand({ - id: 'update-media-db-note', - name: 'Update open note (this will recreate the note)', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.updateActiveNote(false); - } - return true; - }, - }); - this.addCommand({ - id: 'update-media-db-note-metadata', - name: 'Update metadata', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.updateActiveNote(true); - } - return true; - }, - }); - // register link insert command - this.addCommand({ - id: 'add-media-db-link', - name: 'Insert link', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.createLinkWithSearchModal(); - } - return true; - }, - }); - } - - async createLinkWithSearchModal(): Promise { - const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { - return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); - }); - - if (!apiSearchResults) { - return; - } - - const selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - }); - - if (!selectResults || selectResults.length < 1) { - return; - } - - const link = `[${selectResults[0].title}](${selectResults[0].url})`; - - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - - // Make sure the user is editing a Markdown file. - if (view) { - view.editor.replaceRange(link, view.editor.getCursor()); - } - } - - async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { - let types: string[] = []; - 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 { - return await this.apiManager.query(searchModalData.query, apis); - } catch (e) { - console.warn('MDB | Query failed:', e); - new Notice(`Search failed: ${e}`); - return []; - } - }); - - if (!apiSearchResults || apiSearchResults.length === 0) { - new Notice('No results found.'); - return; - } - - // filter the results - apiSearchResults = apiSearchResults.filter(x => types.includes(x.type)); - - if (apiSearchResults.length === 0) { - new Notice('No results found for the selected types.'); - return; - } - - // Show selection modal - for seasons, skip detail query - const selectResults = - types.length === 1 && types[0] === 'season' - ? await this.modalHelper.openSelectModal( - { - elements: apiSearchResults, - description: 'Select one search result to proceed.', - submitButtonText: 'Ok', - }, - async selectModalData => selectModalData.selected, - ) - : await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => this.queryDetails(selectModalData.selected)); - - if (!selectResults || selectResults.length === 0) { - return; - } - - // Handle season selection for both direct season searches and series-to-season conversion - const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults); - if (seasonHandlingResult.handled) { - return; - } - - // Show preview and confirm - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => previewModalData.confirmed); - if (!confirmed) { - return; - } - - // User confirmed, create notes and exit - await this.createMediaDbNotes(selectResults); - } - - /** - * Handles the season workflow for both direct season searches and series-to-season conversion. - * Returns an object indicating what happened and how to proceed. - */ - private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { - // Case 1: User searched specifically for seasons and selected a series from TMDB - if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { - const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); - return { handled: true, seasonsCreated: created }; - } - - // Case 2: User searched for series but it's actually from TMDBSeasonAPI - // (This happens when searching for seasons returns series results) - if (types.includes('series') && selectResults.some(r => r.dataSource === 'TMDBSeriesAPI')) { - const seriesResults = selectResults.filter(r => r.dataSource === 'TMDBSeriesAPI'); - // If only one series result and user searched for seasons, show season selection - if (seriesResults.length === 1 && types.includes('season')) { - const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); - return { handled: true, seasonsCreated: created }; - } - } - - return { handled: false }; - } - - /** - * Shows the season selection modal for a given series and creates notes for selected seasons. - * Returns true if seasons were successfully created, false if cancelled. - */ - private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { - const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; - if (!tmdbSeasonAPI) { - new Notice('TMDBSeasonAPI not available.'); - return false; - } - - try { - // Fetch all seasons for the selected series - const allSeasons = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); - if (!allSeasons || allSeasons.length === 0) { - new Notice('No seasons found for this series.'); - return false; - } - - // Show season selection modal - const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); - if (!selectedSeasons || selectedSeasons.length === 0) { - return false; - } - - // Create notes for all selected seasons in parallel - await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); - new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); - return true; - } catch (e) { - console.warn('MDB | Error in season selection workflow:', e); - new Notice(`Error loading seasons: ${e}`); - return false; - } - } - - /** - * Shows the season selection modal and returns the selected seasons. - */ - private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { - const modal = new MediaDbSeasonSelectModal( - this, - allSeasons.map(s => ({ - season_number: s.seasonNumber, - name: s.seasonTitle || s.title, - episode_count: s.episodes || 0, - air_date: s.year, - poster_path: s.image, - })), - true, - seriesTitle, - ); - - return new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); - }); - } - - /** - * Creates notes for all selected seasons by fetching full metadata and creating entries. - */ - private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { - await Promise.all( - selectedSeasons.map(async selectedSeason => { - const seasonModel = allSeasons.find(s => s.seasonNumber === selectedSeason.season_number); - if (seasonModel) { - try { - // Fetch full metadata using getById - const fullMetadata = await tmdbSeasonAPI.getById(seasonModel.id); - await this.createMediaDbNotes([fullMetadata]); - } catch (e) { - console.warn(`MDB | Failed to create season ${selectedSeason.season_number}:`, e); - new Notice(`Failed to create season ${selectedSeason.season_number}: ${e}`); - } - } - }), - ); - } - - async createEntryWithAdvancedSearchModal(): Promise { - const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { - return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); - }); - - if (!apiSearchResults || apiSearchResults.length === 0) { - new Notice('No results found.'); - return; - } - - const selectResults = - (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - })) ?? []; - if (selectResults.length < 1) { - return; - } - - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; - } - - await this.createMediaDbNotes(selectResults); - } - - async createEntryWithIdSearchModal(): Promise { - let idSearchResult: MediaTypeModel | undefined = undefined; - let proceed: boolean = false; - - while (!proceed) { - idSearchResult = await this.modalHelper.openIdSearchModal({}, async idSearchModalData => { - return await this.apiManager.queryDetailedInfoById(idSearchModalData.query, idSearchModalData.api); - }); - if (!idSearchResult) { - return; - } - - proceed = await this.modalHelper.openPreviewModal({ elements: [idSearchResult] }, async previewModalData => { - return previewModalData.confirmed; - }); - } - - if (!idSearchResult) { - return; - } - await this.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); - } - - async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { - // Create notes in parallel for better performance - const results = await Promise.allSettled(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }))); - - // Report any failures - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - console.warn('MDB | Some notes failed to create:', failures); - new Notice(`${models.length - failures.length} of ${models.length} notes created successfully.`); - } - } - - async queryDetails(models: MediaTypeModel[]): Promise { - // Query details in parallel for better performance - const results = await Promise.allSettled(models.map(model => this.apiManager.queryDetailedInfo(model))); - - // Filter out failures and return successful results - const detailModels: MediaTypeModel[] = results - .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled' && r.value !== undefined) - .map(r => r.value!); - - // Log failures for debugging - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - console.warn('MDB | Some detail queries failed:', failures); - } - - return detailModels; - } - - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - try { - console.debug('MDB | creating new note'); - - options.openNote = this.settings.openNoteInNewTab; - - if (this.settings.imageDownload) { - await this.downloadImageForMediaModel(mediaTypeModel); - } - - const fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); - - options.folder ??= await this.mediaTypeManager.getFolder(mediaTypeModel, this.app); - - const targetFile = await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options); - - if (this.settings.enableTemplaterIntegration) { - await useTemplaterPluginInFile(this.app, targetFile); - } - } catch (e) { - console.warn(e); - new Notice(`${e}`); - } - } - - /** - * Tries to download the image for a media model. - * - * @param mediaTypeModel - * @returns true if the image was downloaded, false otherwise - */ - private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise { - if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { - try { - const imageUrl = mediaTypeModel.image; - const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg'; - const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`; - const imagePath = normalizePath(`${this.settings.imageFolder}/${imageFileName}`); - - if (!this.app.vault.getAbstractFileByPath(this.settings.imageFolder)) { - await this.app.vault.createFolder(this.settings.imageFolder); - } - - if (!this.app.vault.getAbstractFileByPath(imagePath)) { - const response = await requestUrl({ url: imageUrl, method: 'GET' }); - await this.app.vault.createBinary(imagePath, response.arrayBuffer); - } - - // Update model to use local image path - mediaTypeModel.image = `[[${imagePath}]]`; - return true; - } catch (e) { - console.warn('MDB | Failed to download image:', e); - } - } - - return false; - } - - generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { - const fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); - return stringifyYaml(fileMetadata); - } - - /** - * Generates the content of a note from a media model and some options. - * - * @param mediaTypeModel - * @param options - */ - async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); - let fileMetadata: Record; - - if (this.settings.useDefaultFrontMatter) { - fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); - } else { - fileMetadata = { - id: mediaTypeModel.id, - type: mediaTypeModel.type, - dataSource: mediaTypeModel.dataSource, - }; - } - - let fileContent = ''; - template = options.attachTemplate ? template : ''; - - ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); - ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); - - if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { - // Include the media variable in all templater commands by using a top level JavaScript execution command. - fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; - } else { - fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; - } - - return fileContent; - } - - async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { - if (!fileToAttach) { - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); - fileMetadata = { ...attachFileMetadata, ...fileMetadata }; - - let attachFileContent: string = await this.app.vault.read(fileToAttach); - const regExp = new RegExp(this.frontMatterRexExpPattern); - attachFileContent = attachFileContent.replace(regExp, ''); - attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; - fileContent += attachFileContent; - - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { - if (!template) { - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - const templateMetadata = this.getMetaDataFromFileContent(template); - fileMetadata = { ...templateMetadata, ...fileMetadata }; - - const regExp = new RegExp(this.frontMatterRexExpPattern); - const attachFileContent = template.replace(regExp, ''); - fileContent += attachFileContent; - - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - getMetaDataFromFileContent(fileContent: string): Metadata { - let metadata: Metadata; - - const regExp = new RegExp(this.frontMatterRexExpPattern); - const frontMatterRegExpResult = regExp.exec(fileContent); - if (!frontMatterRegExpResult) { - return {}; - } - let frontMatter = frontMatterRegExpResult[0]; - if (!frontMatter) { - return {}; - } - frontMatter = frontMatter.substring(4); - frontMatter = frontMatter.substring(0, frontMatter.length - 3); - - metadata = parseYaml(frontMatter) as Metadata; - - if (!metadata) { - metadata = {}; - } - - console.debug(`MDB | metadata read from file content`, metadata); - - return metadata; - } - - getMetadataFromFileCache(file: TFile): Metadata { - const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter; - return structuredClone(metadata ?? {}); - } - - /** - * Creates a note in the vault. - * - * @param fileName - * @param fileContent - * @param options - */ - async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise { - // 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`; - - // look if file already exists and ask if it should be overwritten - const file = this.app.vault.getAbstractFileByPath(filePath); - if (file) { - const shouldOverwrite = await new Promise(resolve => { - new ConfirmOverwriteModal(this.app, fileName, resolve).open(); - }); - - if (!shouldOverwrite) { - throw new Error('MDB | file creation cancelled by user'); - } - - await this.app.vault.delete(file); - } - - // create the file - const targetFile = await this.app.vault.create(filePath, fileContent); - console.debug(`MDB | created new file at ${filePath}`); - - // open newly created file - if (options.openNote) { - const activeLeaf = this.app.workspace.getUnpinnedLeaf(); - if (!activeLeaf) { - console.warn('MDB | no active leaf, not opening newly created note'); - return targetFile; - } - await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); - } - - return targetFile; - } - - /** - * Update the active note by querying the API again. - * 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 = this.app.workspace.getActiveFile() ?? undefined; - if (!activeFile) { - throw new Error('MDB | there is no active note'); - } - - let metadata = this.getMetadataFromFileCache(activeFile); - metadata = this.modelPropertyMapper.convertObjectBack(metadata); - - console.debug(`MDB | read metadata`, metadata); - - if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { - throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); - } - - const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; - console.debug(`MDB | validOldMetadata`, validOldMetadata); - - const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); - console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); - - let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); - if (!newMediaTypeModel) { - return; - } - - newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); - console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); - - if (onlyMetadata) { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); - } else { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true }); - } - } - - async loadSettings(): Promise { - const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; - const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); - const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); - - // delete old api keys - // @ts-ignore - delete loadedSettings.BoardgameGeekKey; - // @ts-ignore - delete loadedSettings.ComicVineKey; - // @ts-ignore - delete loadedSettings.GiantBombKey; - // @ts-ignore - delete loadedSettings.MobyGamesKey; - // @ts-ignore - delete loadedSettings.OMDbKey; - // @ts-ignore - delete loadedSettings.TMDBKey; - - // Migrate property mappings using the dedicated migration method - const migratedModels = PropertyMappingModel.migrateModels( - loadedSettings.propertyMappingModels || [], - defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), - ); - - // Store as plain data for serialization - loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); - - this.settings = loadedSettings; - - await this.saveSettings(); - } - - async saveSettings(): Promise { - this.mediaTypeManager.updateTemplates(this.settings); - this.mediaTypeManager.updateFolders(this.settings); - this.dateFormatter.setFormat(this.settings.customDateFormat); - - await this.saveData(this.settings); - } -} diff --git a/src/utils/ModalHelper.ts b/src/utils/ModalHelper.ts deleted file mode 100644 index a8d1503..0000000 --- a/src/utils/ModalHelper.ts +++ /dev/null @@ -1,537 +0,0 @@ -import { Notice } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import { MediaDbAdvancedSearchModal } from '../modals/MediaDbAdvancedSearchModal'; -import { MediaDbIdSearchModal } from '../modals/MediaDbIdSearchModal'; -import { MediaDbPreviewModal } from '../modals/MediaDbPreviewModal'; -import { MediaDbSearchModal } from '../modals/MediaDbSearchModal'; -import { MediaDbSearchResultModal } from '../modals/MediaDbSearchResultModal'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { MediaType } from './MediaType'; - -export enum ModalResultCode { - SUCCESS = 'SUCCESS', - SKIP = 'SKIP', - CLOSE = 'CLOSE', - ERROR = 'ERROR', -} - -type ModalResult = - | { - code: ModalResultCode.CLOSE; - } - | { - code: ModalResultCode.ERROR; - error: Error; - } - | { - code: ModalResultCode.SUCCESS; - data: T; - }; - -type SkippableModalResult = - | ModalResult - | { - code: ModalResultCode.SKIP; - }; - -/** - * Object containing the data {@link ModalHelper.createSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type SearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type AdvancedSearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createIdSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type IdSearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createSelectModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type SelectModalResult = SkippableModalResult; - -/** - * Object containing the data {@link ModalHelper.createPreviewModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type PreviewModalResult = ModalResult; - -/** - * The data the search modal returns. - * - query: the query string - * - types: the selected APIs - */ -export interface SearchModalData { - query: string; - types: MediaType[]; -} - -/** - * The data the advanced search modal returns. - * - query: the query string - * - apis: the selected APIs - */ -export interface AdvancedSearchModalData { - query: string; - apis: string[]; -} - -/** - * The data the id search modal returns. - * - query: the query string - * - apis: the selected APIs - */ -export interface IdSearchModalData { - query: string; - api: string; -} - -/** - * The data the select modal returns. - * - selected: the selected items - */ -export interface SelectModalData { - selected: MediaTypeModel[]; -} - -/** - * The data the preview modal returns. - * - confirmed: whether the selected element has been confirmed - */ -export interface PreviewModalData { - confirmed: boolean; -} - -/** - * Options for the search modal. - * - modalTitle: the title of the modal - * - preselectedTypes: a list of preselected Types - * - prefilledSearchString: prefilled query - */ -export interface SearchModalOptions { - modalTitle?: string; - preselectedTypes?: MediaType[]; - prefilledSearchString?: string; -} - -/** - * Options for the advanced search modal. - * - modalTitle: the title of the modal - * - preselectedAPIs: a list of preselected APIs - * - prefilledSearchString: prefilled query - */ -export interface AdvancedSearchModalOptions { - modalTitle?: string; - preselectedAPIs?: string[]; - prefilledSearchString?: string; -} - -/** - * Options for the id search modal. - * - modalTitle: the title of the modal - * - preselectedAPIs: a list of preselected APIs - * - prefilledSearchString: prefilled query - */ -export interface IdSearchModalOptions { - modalTitle?: string; - preselectedAPI?: string; - prefilledSearchString?: string; -} - -/** - * Options for the select modal. - * - modalTitle: the title of the modal - * - elements: the elements the user can select from - * - multiSelect: whether to allow multiselect - * - skipButton: whether to add a skip button to the modal - */ -export interface SelectModalOptions { - elements?: MediaTypeModel[]; - multiSelect?: boolean; - modalTitle?: string; - skipButton?: boolean; - description?: string; // Add this - submitButtonText?: string; // Add this too -} -/** - * Options for the preview modal. - * - modalTitle: the title of the modal - * - elements: the elements to preview - */ -export interface PreviewModalOptions { - modalTitle?: string; - elements?: MediaTypeModel[]; -} - -export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { - modalTitle: 'Media DB Search', - preselectedTypes: [], - prefilledSearchString: '', -}; - -export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = { - modalTitle: 'Media DB Advanced Search', - preselectedAPIs: [], - prefilledSearchString: '', -}; - -export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = { - modalTitle: 'Media DB Id Search', - preselectedAPI: undefined, - prefilledSearchString: '', -}; - -export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = { - modalTitle: 'Media DB Search Results', - elements: [], - multiSelect: true, - skipButton: false, -}; - -export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = { - modalTitle: 'Media DB Preview', - elements: [], -}; - -export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { - elements: [], - multiSelect: true, - modalTitle: '', - skipButton: false, - description: 'Select one or multiple search results.', - submitButtonText: 'Ok', -}; - -/** - * A class providing multiple usefull functions for dealing with the plugins modals. - */ -export class ModalHelper { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - /** - * Creates an {@link MediaDbSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createSearchModal(searchModalOptions: SearchModalOptions): Promise<{ searchModalResult: SearchModalResult; searchModal: MediaDbSearchModal }> { - const modal = new MediaDbSearchModal(this.plugin, searchModalOptions); - const res: SearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { searchModalResult: res, searchModal: modal }; - } - - /** - * Opens an {@link MediaDbSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openSearchModal( - searchModalOptions: SearchModalOptions, - submitCallback: (searchModalData: SearchModalData) => Promise, - ): Promise { - const { searchModalResult, searchModal } = await this.createSearchModal(searchModalOptions); - console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`); - - if (searchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(searchModalResult.error); - new Notice(searchModalResult.error.toString()); - searchModal.close(); - return undefined; - } - - if (searchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(searchModalResult.data); - searchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - searchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbAdvancedSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createAdvancedSearchModal( - advancedSearchModalOptions: AdvancedSearchModalOptions, - ): Promise<{ advancedSearchModalResult: AdvancedSearchModalResult; advancedSearchModal: MediaDbAdvancedSearchModal }> { - const modal = new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions); - const res: AdvancedSearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { advancedSearchModalResult: res, advancedSearchModal: modal }; - } - - /** - * Opens an {@link MediaDbAdvancedSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openAdvancedSearchModal( - advancedSearchModalOptions: AdvancedSearchModalOptions, - submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise, - ): Promise { - const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions); - console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`); - - if (advancedSearchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(advancedSearchModalResult.error); - new Notice(advancedSearchModalResult.error.toString()); - advancedSearchModal.close(); - return undefined; - } - - if (advancedSearchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(advancedSearchModalResult.data); - advancedSearchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - advancedSearchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbIdSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise<{ idSearchModalResult: IdSearchModalResult; idSearchModal: MediaDbIdSearchModal }> { - const modal = new MediaDbIdSearchModal(this.plugin, idSearchModalOptions); - const res: IdSearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { idSearchModalResult: res, idSearchModal: modal }; - } - - /** - * Opens an {@link MediaDbIdSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openIdSearchModal( - idSearchModalOptions: IdSearchModalOptions, - submitCallback: (idSearchModalData: IdSearchModalData) => Promise, - ): Promise { - const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions); - console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`); - - if (idSearchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(idSearchModalResult.error); - new Notice(idSearchModalResult.error.toString()); - idSearchModal.close(); - return undefined; - } - - if (idSearchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes = await submitCallback(idSearchModalResult.data); - idSearchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - idSearchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbSearchResultModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} - * @returns the user input or nothing and a reference to the modal. - */ - async createSelectModal(selectModalOptions: SelectModalOptions): Promise<{ selectModalResult: SelectModalResult; selectModal: MediaDbSearchResultModal }> { - const modal = new MediaDbSearchResultModal(this.plugin, selectModalOptions); - const res: SelectModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setSkipCallback(() => resolve({ code: ModalResultCode.SKIP })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { selectModalResult: res, selectModal: modal }; - } - - /** - * Opens an {@link MediaDbSearchResultModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} - * @param submitCallback the callback that gets executed after the modal has been submitted, but before it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openSelectModal( - selectModalOptions: SelectModalOptions, - submitCallback: (selectModalData: SelectModalData) => Promise, - ): Promise { - const { selectModalResult, selectModal } = await this.createSelectModal(selectModalOptions); - console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`); - - if (selectModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(selectModalResult.error); - new Notice(selectModalResult.error.toString()); - selectModal.close(); - return undefined; - } - - if (selectModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - if (selectModalResult.code === ModalResultCode.SKIP) { - // selection was skipped - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(selectModalResult.data); - selectModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - selectModal.close(); - return; - } - } - - async createPreviewModal(previewModalOptions: PreviewModalOptions): Promise<{ previewModalResult: PreviewModalResult; previewModal: MediaDbPreviewModal }> { - //todo: handle attachFile for existing files - const modal = new MediaDbPreviewModal(this.plugin, previewModalOptions); - const res: PreviewModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { previewModalResult: res, previewModal: modal }; - } - - async openPreviewModal(previewModalOptions: PreviewModalOptions, submitCallback: (previewModalData: PreviewModalData) => Promise): Promise { - const { previewModalResult, previewModal } = await this.createPreviewModal(previewModalOptions); - console.debug(`MDB | previewModal closed with code ${previewModalResult.code}`); - - if (previewModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(previewModalResult.error); - new Notice(previewModalResult.error.toString()); - previewModal.close(); - return false; - } - - if (previewModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return false; - } - - try { - const callbackRes: boolean = await submitCallback(previewModalResult.data); - previewModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - previewModal.close(); - return true; - } - } -} diff --git a/tsconfig.json b/tsconfig.json index 49b6fcb..7ce937b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,12 @@ { "compilerOptions": { + "paths": { + "packages/*": ["./packages/*"], + "tests/*": ["./tests/*"] + }, "module": "ESNext", - "target": "ESNext", + "target": "ES2022", + "noEmit": true, "allowJs": true, "checkJs": true, "noImplicitAny": true, @@ -22,5 +27,5 @@ "jsxImportSource": "solid-js", "types": ["vite/client"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"] + "include": ["packages/obsidian/**/*.ts", "packages/obsidian/**/*.tsx"] } diff --git a/vite.config.ts b/vite.config.mts similarity index 70% rename from vite.config.ts rename to vite.config.mts index 3a8f24b..e42ec6d 100644 --- a/vite.config.ts +++ b/vite.config.mts @@ -1,20 +1,20 @@ -import { defineConfig } from 'vite'; -import solidPlugin from 'vite-plugin-solid'; -import { builtinModules } from 'node:module'; -import { getBuildBanner } from './automation/build/buildBanner'; +import { UserConfig, defineConfig } from 'vite'; +import solid from 'vite-plugin-solid'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; -import manifest from './manifest.json' with { type: 'json' }; import path from 'path'; +import { builtinModules } from 'node:module'; +import { dtsBundlePlugin, getBuildBanner } from '@lemons_dev/lemons-obsidian-plugin-automation'; +import manifest from './manifest.json' with { type: 'json' }; -const entryFile = 'src/main.ts'; +const entryFile = 'packages/obsidian/src/main.ts'; -export default defineConfig(({ mode }) => { +export default defineConfig(async ({ mode }) => { const prod = mode === 'production'; - const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}`; + const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}/`; - const plugins = [ - solidPlugin(), + let plugins = [ + solid(), banner({ outDir: outDir, content: getBuildBanner(prod ? 'Release Build' : 'Dev Build', version => version), @@ -30,13 +30,16 @@ export default defineConfig(({ mode }) => { ]; return { - plugins, - + plugins: plugins, resolve: { alias: { - src: path.resolve(__dirname, './src'), + packages: path.resolve(__dirname, './packages'), }, }, + define: { + // Disable all logs and debug statements in production, but keep warnings and errors + __LOG_LEVEL__: prod ? 2 : 4, + }, build: { lib: { entry: path.resolve(__dirname, entryFile), @@ -45,11 +48,12 @@ export default defineConfig(({ mode }) => { formats: ['cjs'], }, minify: prod, + target: 'es2022', sourcemap: prod ? false : 'inline', cssCodeSplit: false, emptyOutDir: false, outDir: '', - rollupOptions: { + rolldownOptions: { input: { main: path.resolve(__dirname, entryFile), }, @@ -76,5 +80,5 @@ export default defineConfig(({ mode }) => { ], }, }, - }; + } as UserConfig; }); From 5a032a87ae316e3ac0e089e712443f84838123df Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 6 May 2026 17:03:31 +0200 Subject: [PATCH 02/11] cleanup of new result code --- packages/obsidian/src/api/APIManager.ts | 22 ++++---- packages/obsidian/src/api/APIModel.ts | 6 +-- .../obsidian/src/api/apis/BoardGameGeekAPI.ts | 30 +++++------ .../obsidian/src/api/apis/ComicVineAPI.ts | 24 ++++----- packages/obsidian/src/api/apis/IGDBAPI.ts | 36 ++++++------- packages/obsidian/src/api/apis/MALAPI.ts | 16 +++--- packages/obsidian/src/api/apis/MALAPIManga.ts | 14 ++--- .../obsidian/src/api/apis/MusicBrainzAPI.ts | 28 +++++----- packages/obsidian/src/api/apis/OMDbAPI.ts | 40 +++++++------- .../obsidian/src/api/apis/OpenLibraryAPI.ts | 22 ++++---- packages/obsidian/src/api/apis/RAWGAPI.ts | 24 ++++----- packages/obsidian/src/api/apis/SteamAPI.ts | 26 ++++----- .../obsidian/src/api/apis/TMDBMovieAPI.ts | 32 +++++------ .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 54 +++++++++---------- .../obsidian/src/api/apis/TMDBSeriesAPI.ts | 32 +++++------ packages/obsidian/src/api/apis/VNDBAPI.ts | 32 +++++------ .../obsidian/src/api/apis/WikipediaAPI.ts | 22 ++++---- packages/obsidian/src/utils/ErrorReporter.ts | 12 ++--- .../src/utils/{AppError.ts => MDBError.ts} | 12 ++--- .../obsidian/src/utils/MediaDbEntryHelper.ts | 30 +++++------ .../obsidian/src/utils/MediaDbFileHelper.ts | 28 +++++----- packages/obsidian/src/utils/ModalHelper.ts | 32 +++++------ packages/obsidian/src/utils/result.ts | 52 ++++++++++++------ 23 files changed, 323 insertions(+), 303 deletions(-) rename packages/obsidian/src/utils/{AppError.ts => MDBError.ts} (62%) diff --git a/packages/obsidian/src/api/APIManager.ts b/packages/obsidian/src/api/APIManager.ts index de9ba41..65309fd 100644 --- a/packages/obsidian/src/api/APIManager.ts +++ b/packages/obsidian/src/api/APIManager.ts @@ -1,14 +1,14 @@ import type { APIModel } from 'packages/obsidian/src/api/APIModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; export interface ApiQueryOk { items: MediaTypeModel[]; - warnings: AppError[]; + warnings: MDBError[]; } export class APIManager { @@ -24,14 +24,14 @@ export class APIManager { * @param query * @param apisToQuery */ - async query(query: string, apisToQuery: string[]): Promise> { + async query(query: string, apisToQuery: string[]): Promise> { Logger.debug(`MDB | api manager queried with "${query}"`); const apis = this.apis.filter(api => apisToQuery.includes(api.apiName)); const results = await Promise.all(apis.map(api => api.searchByTitle(query))); const items: MediaTypeModel[] = []; - const warnings: AppError[] = []; + const warnings: MDBError[] = []; for (const result of results) { if (result.ok) { items.push(...result.value); @@ -43,8 +43,8 @@ export class APIManager { if (items.length === 0 && warnings.length > 0) { // If all APIs failed, surface an error (using the first as representative) return err( - toAppError(warnings[0], { - kind: AppErrorKind.Api, + toMdbError(warnings[0], { + kind: MDBErrorKind.Api, message: 'Failed to query APIs', userMessage: 'Failed to query APIs', context: { query, apisToQuery }, @@ -64,7 +64,7 @@ export class APIManager { * * @param item */ - async queryDetailedInfo(item: MediaTypeModel): Promise> { + async queryDetailedInfo(item: MediaTypeModel): Promise> { return await this.queryDetailedInfoById(item.id, item.dataSource); } @@ -74,7 +74,7 @@ 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) { const result = await api.getById(id); @@ -88,8 +88,8 @@ export class APIManager { } return err( - toAppError(new Error(`API not found: ${apiName}`), { - kind: AppErrorKind.Validation, + toMdbError(new Error(`API not found: ${apiName}`), { + kind: MDBErrorKind.Validation, message: `API not found: ${apiName}`, userMessage: `API not found: ${apiName}`, context: { apiName, id }, diff --git a/packages/obsidian/src/api/APIModel.ts b/packages/obsidian/src/api/APIModel.ts index 7527e99..c5bfe5d 100644 --- a/packages/obsidian/src/api/APIModel.ts +++ b/packages/obsidian/src/api/APIModel.ts @@ -1,6 +1,6 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; @@ -16,9 +16,9 @@ export abstract class APIModel { * * @param title the title to query for */ - abstract searchByTitle(title: string): Promise>; + abstract searchByTitle(title: string): Promise>; - abstract getById(id: string): Promise>; + abstract getById(id: string): Promise>; abstract getDisabledMediaTypes(): MediaType[]; diff --git a/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts index 889b779..331ed40 100644 --- a/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts +++ b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -25,12 +25,12 @@ export class BoardGameGeekAPI extends APIModel { this.types = [MediaType.BoardGame]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -46,8 +46,8 @@ export class BoardGameGeekAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -61,7 +61,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -70,7 +70,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -103,12 +103,12 @@ export class BoardGameGeekAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -124,8 +124,8 @@ export class BoardGameGeekAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -139,7 +139,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -148,7 +148,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, @@ -162,7 +162,7 @@ export class BoardGameGeekAPI extends APIModel { const boardgame = response.querySelector('boardgame'); if (!boardgame) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received invalid data from ${this.apiName}.`, userMessage: `Received invalid data from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/ComicVineAPI.ts b/packages/obsidian/src/api/apis/ComicVineAPI.ts index 0babc39..3a8a0b1 100644 --- a/packages/obsidian/src/api/apis/ComicVineAPI.ts +++ b/packages/obsidian/src/api/apis/ComicVineAPI.ts @@ -5,9 +5,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -27,12 +27,12 @@ export class ComicVineAPI extends APIModel { this.types = [MediaType.ComicManga]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -45,8 +45,8 @@ export class ComicVineAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -59,7 +59,7 @@ export class ComicVineAPI extends APIModel { // console.debug(fetchData); if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -85,12 +85,12 @@ export class ComicVineAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -103,8 +103,8 @@ export class ComicVineAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -119,7 +119,7 @@ export class ComicVineAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, diff --git a/packages/obsidian/src/api/apis/IGDBAPI.ts b/packages/obsidian/src/api/apis/IGDBAPI.ts index acfbf61..a63958c 100644 --- a/packages/obsidian/src/api/apis/IGDBAPI.ts +++ b/packages/obsidian/src/api/apis/IGDBAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -60,7 +60,7 @@ export class IGDBAPI extends APIModel { this.types = [MediaType.Game]; } - private async getAuthToken(): Promise> { + private async getAuthToken(): Promise> { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return ok(this.accessToken); @@ -69,7 +69,7 @@ export class IGDBAPI extends APIModel { if (!clientId || !clientSecret) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Client ID or Client Secret for ${this.apiName} missing.`, userMessage: `Client ID or Client Secret for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -82,8 +82,8 @@ export class IGDBAPI extends APIModel { method: 'POST', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying Twitch auth for ${this.apiName}`, userMessage: `Network error querying Twitch auth for ${this.apiName}`, context: { apiName: this.apiName }, @@ -96,7 +96,7 @@ export class IGDBAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Auth failed for ${this.apiName}. Check Credentials.`, userMessage: `Auth failed for ${this.apiName}. Check Credentials.`, context: { apiName: this.apiName, status: response.status }, @@ -109,12 +109,12 @@ export class IGDBAPI extends APIModel { return ok(this.accessToken); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); if (!clientId) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Client ID for ${this.apiName} missing.`, userMessage: `Client ID for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -134,8 +134,8 @@ export class IGDBAPI extends APIModel { body: queryBody, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -147,7 +147,7 @@ export class IGDBAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, @@ -172,12 +172,12 @@ export class IGDBAPI extends APIModel { ); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); if (!clientId) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Client ID for ${this.apiName} missing.`, userMessage: `Client ID for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -197,8 +197,8 @@ export class IGDBAPI extends APIModel { body: queryBody, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -210,7 +210,7 @@ export class IGDBAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, @@ -220,7 +220,7 @@ export class IGDBAPI extends APIModel { const data = response.json as IGDBGame[]; if (!data || data.length === 0) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No result found for ID ${id}`, userMessage: `No result found for ID ${id}`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/MALAPI.ts b/packages/obsidian/src/api/apis/MALAPI.ts index 11d1c02..1461b97 100644 --- a/packages/obsidian/src/api/apis/MALAPI.ts +++ b/packages/obsidian/src/api/apis/MALAPI.ts @@ -4,9 +4,9 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; @@ -33,7 +33,7 @@ export class MALAPI extends APIModel { this.typeMappings.set('ova', 'ova'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -51,7 +51,7 @@ export class MALAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -108,7 +108,7 @@ export class MALAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -124,7 +124,7 @@ export class MALAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -135,7 +135,7 @@ export class MALAPI extends APIModel { if (result === undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data found for ID ${id} in ${this.apiName}.`, userMessage: `No data found for ID ${id} in ${this.apiName}.`, context: { apiName: this.apiName, id }, @@ -248,7 +248,7 @@ export class MALAPI extends APIModel { } return err({ - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: `MDB | Unknown media type for id ${id}`, userMessage: `Unknown media type for id ${id}`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/MALAPIManga.ts b/packages/obsidian/src/api/apis/MALAPIManga.ts index 424449b..f864fa5 100644 --- a/packages/obsidian/src/api/apis/MALAPIManga.ts +++ b/packages/obsidian/src/api/apis/MALAPIManga.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; @@ -35,7 +35,7 @@ export class MALAPIManga extends APIModel { this.typeMappings.set('novel', 'novel'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -53,7 +53,7 @@ export class MALAPIManga extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -106,7 +106,7 @@ export class MALAPIManga extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -122,7 +122,7 @@ export class MALAPIManga extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -133,7 +133,7 @@ export class MALAPIManga extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data found for ID ${id} in ${this.apiName}.`, userMessage: `No data found for ID ${id} in ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/MusicBrainzAPI.ts b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts index a61d7d0..2c4a534 100644 --- a/packages/obsidian/src/api/apis/MusicBrainzAPI.ts +++ b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -108,7 +108,7 @@ export class MusicBrainzAPI extends APIModel { this.types = [MediaType.MusicRelease]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; @@ -121,8 +121,8 @@ export class MusicBrainzAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -137,7 +137,7 @@ export class MusicBrainzAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -172,7 +172,7 @@ export class MusicBrainzAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); // Fetch release group @@ -185,8 +185,8 @@ export class MusicBrainzAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -199,7 +199,7 @@ export class MusicBrainzAPI extends APIModel { if (groupResponse.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${groupResponse.status} from ${this.apiName}.`, userMessage: `Received status code ${groupResponse.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: groupResponse.status, id }, @@ -212,7 +212,7 @@ export class MusicBrainzAPI extends APIModel { const firstRelease = result.releases?.[0]; if (!firstRelease) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | No releases found in release group.', userMessage: 'No releases found in release group.', context: { apiName: this.apiName, id }, @@ -231,8 +231,8 @@ export class MusicBrainzAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id, releaseId: firstRelease.id }, @@ -245,7 +245,7 @@ export class MusicBrainzAPI extends APIModel { if (releaseResponse.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`, userMessage: `Received status code ${releaseResponse.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: releaseResponse.status, id, releaseId: firstRelease.id }, diff --git a/packages/obsidian/src/api/apis/OMDbAPI.ts b/packages/obsidian/src/api/apis/OMDbAPI.ts index 2439378..ab57f8c 100644 --- a/packages/obsidian/src/api/apis/OMDbAPI.ts +++ b/packages/obsidian/src/api/apis/OMDbAPI.ts @@ -5,9 +5,9 @@ import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -79,13 +79,13 @@ export class OMDbAPI extends APIModel { this.typeMappings.set('game', 'game'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -98,8 +98,8 @@ export class OMDbAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -113,7 +113,7 @@ export class OMDbAPI extends APIModel { if (response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -121,7 +121,7 @@ export class OMDbAPI extends APIModel { } if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, @@ -132,7 +132,7 @@ export class OMDbAPI extends APIModel { if (!data) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName }, @@ -145,7 +145,7 @@ export class OMDbAPI extends APIModel { } return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received error from ${this.apiName}: ${data.Error}`, userMessage: `${data.Error}`, context: { apiName: this.apiName }, @@ -203,13 +203,13 @@ export class OMDbAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -222,8 +222,8 @@ export class OMDbAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -237,7 +237,7 @@ export class OMDbAPI extends APIModel { if (response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -245,7 +245,7 @@ export class OMDbAPI extends APIModel { } if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, @@ -256,7 +256,7 @@ export class OMDbAPI extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, @@ -265,7 +265,7 @@ export class OMDbAPI extends APIModel { if (result.Response === 'False') { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received error from ${this.apiName}: ${result.Error}`, userMessage: `${result.Error}`, context: { apiName: this.apiName, id }, @@ -275,7 +275,7 @@ export class OMDbAPI extends APIModel { const type = this.typeMappings.get(result.Type.toLowerCase()); if (type === undefined) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `${result.Type.toLowerCase()} is an unsupported type.`, userMessage: `${result.Type.toLowerCase()} is an unsupported type.`, context: { apiName: this.apiName, id, type: result.Type }, @@ -375,7 +375,7 @@ export class OMDbAPI extends APIModel { } return err({ - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: `MDB | Unknown media type for id ${id}`, userMessage: `Unknown media type for id ${id}`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts index 6a52bdb..08df3be 100644 --- a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { BookModel } from 'packages/obsidian/src/models/BookModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -48,7 +48,7 @@ export class OpenLibraryAPI extends APIModel { this.types = [MediaType.Book]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://openlibrary.org/' }); @@ -63,8 +63,8 @@ export class OpenLibraryAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -78,7 +78,7 @@ export class OpenLibraryAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -109,7 +109,7 @@ export class OpenLibraryAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://openlibrary.org/' }); @@ -125,8 +125,8 @@ export class OpenLibraryAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -140,7 +140,7 @@ export class OpenLibraryAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -155,7 +155,7 @@ export class OpenLibraryAPI extends APIModel { const result = data.docs?.[0]; if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data found for ID ${id} in ${this.apiName}.`, userMessage: `No data found for ID ${id}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/RAWGAPI.ts b/packages/obsidian/src/api/apis/RAWGAPI.ts index 21d603b..a3f13be 100644 --- a/packages/obsidian/src/api/apis/RAWGAPI.ts +++ b/packages/obsidian/src/api/apis/RAWGAPI.ts @@ -3,8 +3,8 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -40,11 +40,11 @@ export class RAWGAPI extends APIModel { this.types = [MediaType.Game]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -57,8 +57,8 @@ export class RAWGAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -71,7 +71,7 @@ export class RAWGAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Error ${response.status} from ${this.apiName}.`, userMessage: `Error ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, @@ -95,11 +95,11 @@ export class RAWGAPI extends APIModel { ); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -112,8 +112,8 @@ export class RAWGAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -126,7 +126,7 @@ export class RAWGAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Error ${response.status} from ${this.apiName}.`, userMessage: `Error ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, diff --git a/packages/obsidian/src/api/apis/SteamAPI.ts b/packages/obsidian/src/api/apis/SteamAPI.ts index 8a47efa..e331709 100644 --- a/packages/obsidian/src/api/apis/SteamAPI.ts +++ b/packages/obsidian/src/api/apis/SteamAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -148,7 +148,7 @@ export class SteamAPI extends APIModel { this.typeMappings.set('game', 'game'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; @@ -157,8 +157,8 @@ export class SteamAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -171,7 +171,7 @@ export class SteamAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -200,7 +200,7 @@ export class SteamAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; @@ -209,8 +209,8 @@ export class SteamAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -223,7 +223,7 @@ export class SteamAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, @@ -242,7 +242,7 @@ export class SteamAPI extends APIModel { } if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | API returned invalid data.', userMessage: 'Steam returned invalid data.', context: { apiName: this.apiName, id }, @@ -254,8 +254,8 @@ export class SteamAPI extends APIModel { // Check if a poster version of the image exists, else use the header image const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`; const existsResult = await fromPromise(imageUrlExists(imageUrl), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Failed to validate image URL for ${this.apiName}`, userMessage: `Failed to validate image URL for ${this.apiName}`, context: { apiName: this.apiName, id, imageUrl }, diff --git a/packages/obsidian/src/api/apis/TMDBMovieAPI.ts b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts index 9f2dbf1..ca3e89e 100644 --- a/packages/obsidian/src/api/apis/TMDBMovieAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -59,13 +59,13 @@ export class TMDBMovieAPI extends APIModel { this.typeMappings.set('movie', 'movie'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -87,8 +87,8 @@ export class TMDBMovieAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -103,7 +103,7 @@ export class TMDBMovieAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -111,7 +111,7 @@ export class TMDBMovieAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -122,7 +122,7 @@ export class TMDBMovieAPI extends APIModel { if (!data) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName }, @@ -153,13 +153,13 @@ export class TMDBMovieAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -181,8 +181,8 @@ export class TMDBMovieAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -197,7 +197,7 @@ export class TMDBMovieAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -205,7 +205,7 @@ export class TMDBMovieAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -216,7 +216,7 @@ export class TMDBMovieAPI extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts index 867879e..ea16027 100644 --- a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -63,13 +63,13 @@ export class TMDBSeasonAPI extends APIModel { this.typeMappings.set('tv', 'season'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -91,8 +91,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -106,7 +106,7 @@ export class TMDBSeasonAPI extends APIModel { if (searchResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -115,7 +115,7 @@ export class TMDBSeasonAPI extends APIModel { if (searchResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${searchResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: searchResponse.response.status }, @@ -169,11 +169,11 @@ export class TMDBSeasonAPI extends APIModel { } // Fetch all seasons for a given series - async getSeasonsForSeries(tvId: string): Promise> { + async getSeasonsForSeries(tvId: string): Promise> { const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, tvId }, @@ -192,8 +192,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, tvId }, @@ -207,7 +207,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, tvId }, @@ -216,7 +216,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: seriesResponse.response.status, tvId }, @@ -250,13 +250,13 @@ export class TMDBSeasonAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -267,7 +267,7 @@ export class TMDBSeasonAPI extends APIModel { const m = /^(\d+)\/season\/(\d+)$/.exec(id); if (!m) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Invalid season id "${id}". Expected format "/season/".`, userMessage: `Invalid season id "${id}".`, context: { apiName: this.apiName, id }, @@ -294,8 +294,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -309,7 +309,7 @@ export class TMDBSeasonAPI extends APIModel { if (seasonResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, id }, @@ -318,7 +318,7 @@ export class TMDBSeasonAPI extends APIModel { if (seasonResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${seasonResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: seasonResponse.response.status, id }, @@ -328,7 +328,7 @@ export class TMDBSeasonAPI extends APIModel { const seasonData = seasonResponse.data; if (!seasonData) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, @@ -349,8 +349,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -364,7 +364,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, id }, @@ -373,7 +373,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: seriesResponse.response.status, id }, @@ -384,7 +384,7 @@ export class TMDBSeasonAPI extends APIModel { if (!seriesData) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts index 40f68d7..61f727f 100644 --- a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -50,13 +50,13 @@ export class TMDBSeriesAPI extends APIModel { this.typeMappings.set('tv', 'series'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -78,8 +78,8 @@ export class TMDBSeriesAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -93,7 +93,7 @@ export class TMDBSeriesAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -101,7 +101,7 @@ export class TMDBSeriesAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -112,7 +112,7 @@ export class TMDBSeriesAPI extends APIModel { if (!data) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName }, @@ -143,13 +143,13 @@ export class TMDBSeriesAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -171,8 +171,8 @@ export class TMDBSeriesAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -186,7 +186,7 @@ export class TMDBSeriesAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, id }, @@ -194,7 +194,7 @@ export class TMDBSeriesAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -205,7 +205,7 @@ export class TMDBSeriesAPI extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/VNDBAPI.ts b/packages/obsidian/src/api/apis/VNDBAPI.ts index eae27de..0506f4b 100644 --- a/packages/obsidian/src/api/apis/VNDBAPI.ts +++ b/packages/obsidian/src/api/apis/VNDBAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -110,7 +110,7 @@ export class VNDBAPI extends APIModel { * @returns A JSON object representing the query response. * @see {@link https://api.vndb.org/kana#api-structure} */ - private async postQuery(endpoint: string, body: string): Promise> { + private async postQuery(endpoint: string, body: string): Promise> { const fetchDataResult = await fromPromise( requestUrl({ url: `${this.apiUrl}${endpoint}`, @@ -120,8 +120,8 @@ export class VNDBAPI extends APIModel { throw: false, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, endpoint }, @@ -136,42 +136,42 @@ export class VNDBAPI extends APIModel { switch (fetchData.status) { case 400: return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Invalid request body or query [${fetchData.text}].`, userMessage: 'Invalid VNDB request.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 404: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | Invalid API path or HTTP method.', userMessage: 'VNDB endpoint not found.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 429: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | VNDB throttled the request.', userMessage: 'VNDB throttled the request. Please try again later.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 500: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | VNDB server error.', userMessage: 'VNDB server error.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 502: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | VNDB server is down.', userMessage: 'VNDB server is down.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); default: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, endpoint, status: fetchData.status }, @@ -187,7 +187,7 @@ export class VNDBAPI extends APIModel { * Queries visual novel entries. * @see {@link https://api.vndb.org/kana#post-vn} */ - private async postVNQuery(body: string): Promise> { + private async postVNQuery(body: string): Promise> { const result = await this.postQuery('/vn', body); return result.ok ? ok(result.value as VNJSONResponse) : err(result.error); } @@ -197,12 +197,12 @@ export class VNDBAPI extends APIModel { * Queries release entries. * @see {@link https://api.vndb.org/kana#post-release} */ - private async postReleaseQuery(body: string): Promise> { + private async postReleaseQuery(body: string): Promise> { const result = await this.postQuery('/release', body); return result.ok ? ok(result.value as ReleaseJSONResponse) : err(result.error); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); /* SFW Filter: has ANY official&&complete&&standalone&&SFW release @@ -253,7 +253,7 @@ export class VNDBAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const vnDataResult = await this.postVNQuery(`{ @@ -267,7 +267,7 @@ export class VNDBAPI extends APIModel { if (vnData.results.length !== 1) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Expected 1 result from query, got ${vnData.results.length}.`, userMessage: 'Unexpected VNDB response.', context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/WikipediaAPI.ts b/packages/obsidian/src/api/apis/WikipediaAPI.ts index 2765786..392c53e 100644 --- a/packages/obsidian/src/api/apis/WikipediaAPI.ts +++ b/packages/obsidian/src/api/apis/WikipediaAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { WikiModel } from 'packages/obsidian/src/models/WikiModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -53,7 +53,7 @@ export class WikipediaAPI extends APIModel { this.types = [MediaType.Wiki]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; @@ -66,7 +66,7 @@ export class WikipediaAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -75,8 +75,8 @@ export class WikipediaAPI extends APIModel { const response = fetchData as { status: number; json(): Promise }; const dataResult = await fromPromise(response.json(), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Failed to parse response from ${this.apiName}`, userMessage: `Failed to parse response from ${this.apiName}`, context: { apiName: this.apiName }, @@ -105,7 +105,7 @@ export class WikipediaAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; @@ -117,7 +117,7 @@ export class WikipediaAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, @@ -126,8 +126,8 @@ export class WikipediaAPI extends APIModel { const response = fetchData as { status: number; json(): Promise }; const dataResult = await fromPromise(response.json(), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Failed to parse response from ${this.apiName}`, userMessage: `Failed to parse response from ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -141,7 +141,7 @@ export class WikipediaAPI extends APIModel { const result = Object.values(data?.query?.pages)[0]; if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/utils/ErrorReporter.ts b/packages/obsidian/src/utils/ErrorReporter.ts index 4155ae1..dc8b693 100644 --- a/packages/obsidian/src/utils/ErrorReporter.ts +++ b/packages/obsidian/src/utils/ErrorReporter.ts @@ -1,22 +1,22 @@ import { Notice } from 'obsidian'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; export class ErrorReporter { - notice(error: AppError): void { + notice(error: MDBError): void { const message = error.userMessage ?? error.message; new Notice(message); } - log(error: AppError): void { + log(error: MDBError): void { Logger.warn('MDB | error', error); } - report(error: AppError): void { + report(error: MDBError): void { this.log(error); - if (error.kind !== AppErrorKind.Cancelled) { + if (error.kind !== MDBErrorKind.Cancelled) { this.notice(error); } } diff --git a/packages/obsidian/src/utils/AppError.ts b/packages/obsidian/src/utils/MDBError.ts similarity index 62% rename from packages/obsidian/src/utils/AppError.ts rename to packages/obsidian/src/utils/MDBError.ts index 50db684..84f08a2 100644 --- a/packages/obsidian/src/utils/AppError.ts +++ b/packages/obsidian/src/utils/MDBError.ts @@ -1,4 +1,4 @@ -export enum AppErrorKind { +export enum MDBErrorKind { Validation = 'Validation', Api = 'Api', Network = 'Network', @@ -8,17 +8,15 @@ export enum AppErrorKind { Unexpected = 'Unexpected', } -export interface AppError { - kind: AppErrorKind; +export interface MDBError { + kind: MDBErrorKind; message: string; userMessage?: string; cause?: unknown; context?: Record; } -export const appError = (error: AppError): AppError => error; - -export const toAppError = (cause: unknown, fallback: Omit): AppError => { +export function toMdbError(cause: unknown, fallback: Omit): MDBError { const message = cause instanceof Error ? cause.message : String(cause); return { ...fallback, message: fallback.message || message, cause }; -}; +} diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 222a11c..2f558d3 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -5,8 +5,8 @@ import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/Medi import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; @@ -17,7 +17,7 @@ export class MediaDbEntryHelper { this.plugin = plugin; } - private reportAppError(error: AppError): void { + private reportMdbError(error: MDBError): void { this.plugin.errorReporter.report(error); } @@ -29,12 +29,12 @@ export class MediaDbEntryHelper { const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); if (!apiSearchResults.ok) { - this.reportAppError(apiSearchResults.error); + this.reportMdbError(apiSearchResults.error); return; } if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); return; } @@ -66,18 +66,18 @@ export class MediaDbEntryHelper { const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName); const apiSearchResults = await this.plugin.apiManager.query(searchData.query, apis); if (!apiSearchResults.ok) { - this.reportAppError(apiSearchResults.error); + this.reportMdbError(apiSearchResults.error); return; } if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); return; } const filteredSearchResults = apiSearchResults.value.items.filter(result => types.includes(result.getMediaType())); if (filteredSearchResults.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' }); return; } @@ -121,12 +121,12 @@ export class MediaDbEntryHelper { const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); if (!apiSearchResults.ok) { - this.reportAppError(apiSearchResults.error); + this.reportMdbError(apiSearchResults.error); return; } if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); return; } @@ -160,7 +160,7 @@ export class MediaDbEntryHelper { const queriedIdResult = await this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api); if (!queriedIdResult.ok) { - this.reportAppError(queriedIdResult.error); + this.reportMdbError(queriedIdResult.error); return; } @@ -183,7 +183,7 @@ export class MediaDbEntryHelper { const createNoteResult = await this.plugin.fileHelper.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); if (!createNoteResult.ok) { - this.reportAppError(createNoteResult.error); + this.reportMdbError(createNoteResult.error); } } @@ -195,7 +195,7 @@ export class MediaDbEntryHelper { if (result.ok && result.value) { detailModels.push(result.value); } else if (!result.ok) { - this.reportAppError(result.error); + this.reportMdbError(result.error); } } @@ -228,7 +228,7 @@ export class MediaDbEntryHelper { const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); if (!allSeasonsResult.ok) { - this.reportAppError(allSeasonsResult.error); + this.reportMdbError(allSeasonsResult.error); new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`); return false; } @@ -276,7 +276,7 @@ export class MediaDbEntryHelper { if (seasonModel) { const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id); if (!fullMetadataResult.ok) { - this.reportAppError(fullMetadataResult.error); + this.reportMdbError(fullMetadataResult.error); new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`); return; } diff --git a/packages/obsidian/src/utils/MediaDbFileHelper.ts b/packages/obsidian/src/utils/MediaDbFileHelper.ts index 42f1bdd..4c7db6f 100644 --- a/packages/obsidian/src/utils/MediaDbFileHelper.ts +++ b/packages/obsidian/src/utils/MediaDbFileHelper.ts @@ -4,9 +4,9 @@ import { Notice, normalizePath, parseYaml, requestUrl, stringifyYaml } from 'obs import type MediaDbPlugin from 'packages/obsidian/src/main'; import { ConfirmOverwriteModal } from 'packages/obsidian/src/modals/ConfirmOverwriteModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; @@ -29,7 +29,7 @@ export class MediaDbFileHelper { this.plugin = plugin; } - async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise> { + async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise> { const results = await Promise.all(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile }))); const failures = results.filter(result => !result.ok); @@ -45,7 +45,7 @@ export class MediaDbFileHelper { return ok(undefined); } - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise> { + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise> { Logger.debug('MDB | creating new note'); options.openNote = this.plugin.settings.openNoteInNewTab; @@ -58,7 +58,7 @@ export class MediaDbFileHelper { } const fileContentResult = await this.attempt(() => this.generateMediaDbNoteContents(mediaTypeModel, options), { - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: 'Failed to generate note contents', userMessage: 'Failed to generate note contents', }); @@ -67,7 +67,7 @@ export class MediaDbFileHelper { } const folderResult = await this.attempt(() => this.plugin.mediaTypeManager.getFolder(mediaTypeModel, this.plugin.app), { - kind: AppErrorKind.Vault, + kind: MDBErrorKind.Vault, message: 'Failed to determine note folder', userMessage: 'Failed to determine note folder', }); @@ -84,7 +84,7 @@ export class MediaDbFileHelper { if (this.plugin.settings.enableTemplaterIntegration) { const templaterResult = await this.attempt(() => useTemplaterPluginInFile(this.plugin.app, targetFileResult.value), { - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: 'Failed to apply templater to the note', userMessage: 'Failed to apply templater to the note', }); @@ -240,11 +240,11 @@ export class MediaDbFileHelper { return structuredClone(metadata ?? {}); } - async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise> { + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise> { const folder = options.folder ?? this.plugin.app.vault.getAbstractFileByPath('/'); if (!folder || !(folder instanceof TFolder)) { - return err({ kind: AppErrorKind.Validation, message: 'MDB | invalid folder', userMessage: 'MDB | invalid folder' }); + return err({ kind: MDBErrorKind.Validation, message: 'MDB | invalid folder', userMessage: 'MDB | invalid folder' }); } fileName = replaceIllegalFileNameCharactersInString(fileName); @@ -257,7 +257,7 @@ export class MediaDbFileHelper { }); if (!shouldOverwrite) { - return err({ kind: AppErrorKind.Cancelled, message: 'MDB | file creation cancelled by user', userMessage: 'MDB | file creation cancelled by user' }); + return err({ kind: MDBErrorKind.Cancelled, message: 'MDB | file creation cancelled by user', userMessage: 'MDB | file creation cancelled by user' }); } await this.plugin.app.fileManager.trashFile(file); @@ -278,7 +278,7 @@ export class MediaDbFileHelper { return ok(targetFile); } - private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise> { + private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise> { if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { const imageUrl = mediaTypeModel.image; const imageResult = await this.attempt( @@ -299,7 +299,7 @@ export class MediaDbFileHelper { mediaTypeModel.image = `[[${imagePath}]]`; }, { - kind: AppErrorKind.Network, + kind: MDBErrorKind.Network, message: 'MDB | Failed to download image', userMessage: 'Failed to download image', }, @@ -314,12 +314,12 @@ export class MediaDbFileHelper { return ok(undefined); } - private async attempt(operation: () => Promise | T, fallback: Omit): Promise> { + private async attempt(operation: () => Promise | T, fallback: Omit): Promise> { return await Promise.resolve() .then(operation) .then( value => ok(value), - cause => err(toAppError(cause, fallback)), + cause => err(toMdbError(cause, fallback)), ); } } diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index 89a1b24..36f4b61 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -5,8 +5,8 @@ import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreview import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Outcome } from 'packages/obsidian/src/utils/result'; import { cancelled, failure, OutcomeStatus, skipped, success } from 'packages/obsidian/src/utils/result'; @@ -106,7 +106,7 @@ export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { }; interface ModalCoreResult { - modalResult: Outcome; + modalResult: Outcome; modal: TModal; } @@ -119,10 +119,10 @@ export class ModalHelper { private async openModalCore( createModal: () => TModal, - wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, + wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, ): Promise> { const modal = createModal(); - const modalResult = await new Promise>(resolve => { + const modalResult = await new Promise>(resolve => { wireHandlers(modal, resolve); modal.open(); }); @@ -130,7 +130,7 @@ export class ModalHelper { return { modalResult, modal }; } - private async resolveOutcome(outcomePromise: Promise>): Promise { + private async resolveOutcome(outcomePromise: Promise>): Promise { const outcome = await outcomePromise; if (outcome.status === OutcomeStatus.Ok) { @@ -144,14 +144,14 @@ export class ModalHelper { return undefined; } - async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { + async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbSearchModal(this.plugin, searchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Search modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Search modal closed with an error' }))); return; } @@ -171,14 +171,14 @@ export class ModalHelper { return await this.resolveOutcome(this.createSearchModalOutcome(searchModalOptions)); } - async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { + async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); return; } @@ -198,14 +198,14 @@ export class ModalHelper { return await this.resolveOutcome(this.createAdvancedSearchModalOutcome(advancedSearchModalOptions)); } - async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { + async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Id search modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Id search modal closed with an error' }))); return; } @@ -225,7 +225,7 @@ export class ModalHelper { return await this.resolveOutcome(this.createIdSearchModalOutcome(idSearchModalOptions)); } - async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { + async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), (modal, resolve) => { @@ -233,7 +233,7 @@ export class ModalHelper { modal.setSkipCallback(() => resolve(skipped())); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Select modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Select modal closed with an error' }))); return; } @@ -253,14 +253,14 @@ export class ModalHelper { return await this.resolveOutcome(this.createSelectModalOutcome(selectModalOptions)); } - async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { + async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbPreviewModal(this.plugin, previewModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Preview modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Preview modal closed with an error' }))); return; } diff --git a/packages/obsidian/src/utils/result.ts b/packages/obsidian/src/utils/result.ts index 64c71c3..325a5f6 100644 --- a/packages/obsidian/src/utils/result.ts +++ b/packages/obsidian/src/utils/result.ts @@ -8,30 +8,44 @@ export interface Err { } export type Result = Ok | Err; -export const ok = (value: T): Ok => ({ ok: true, value }); -export const err = (error: E): Err => ({ ok: false, error }); +export function ok(value: T): Ok { + return { ok: true, value }; +} +export function err(error: E): Err { + return { ok: false, error }; +} -export const isOk = (result: Result): result is Ok => result.ok; -export const isErr = (result: Result): result is Err => !result.ok; +export function isOk(result: Result): result is Ok { + return result.ok; +} +export function isErr(result: Result): result is Err { + return !result.ok; +} -export const mapResult = (result: Result, mapper: (value: T) => U): Result => (result.ok ? ok(mapper(result.value)) : result); +export function mapResult(result: Result, mapper: (value: T) => U): Result { + return result.ok ? ok(mapper(result.value)) : result; +} -export const mapError = (result: Result, mapper: (error: E) => F): Result => (result.ok ? result : err(mapper(result.error))); +export function mapError(result: Result, mapper: (error: E) => F): Result { + return result.ok ? result : err(mapper(result.error)); +} -export const andThen = (result: Result, binder: (value: T) => Result): Result => (result.ok ? binder(result.value) : result); +export function andThen(result: Result, binder: (value: T) => Result): Result { + return result.ok ? binder(result.value) : result; +} -export const tapError = (result: Result, sideEffect: (error: E) => void): Result => { +export function tapError(result: Result, sideEffect: (error: E) => void): Result { if (!result.ok) sideEffect(result.error); return result; -}; +} -export const fromPromise = async (promise: Promise, onError: (cause: unknown) => E): Promise> => { +export async function fromPromise(promise: Promise, onError: (cause: unknown) => E): Promise> { try { return ok(await promise); } catch (cause) { return err(onError(cause)); } -}; +} export enum OutcomeStatus { Ok = 'ok', @@ -46,7 +60,15 @@ export type Outcome = | { status: OutcomeStatus.Skipped } | { status: OutcomeStatus.Error; error: E }; -export const cancelled = (): Outcome => ({ status: OutcomeStatus.Cancelled }); -export const skipped = (): Outcome => ({ status: OutcomeStatus.Skipped }); -export const success = (data: T): Outcome => ({ status: OutcomeStatus.Ok, data }); -export const failure = (error: E): Outcome => ({ status: OutcomeStatus.Error, error }); +export function cancelled(): Outcome { + return { status: OutcomeStatus.Cancelled }; +} +export function skipped(): Outcome { + return { status: OutcomeStatus.Skipped }; +} +export function success(data: T): Outcome { + return { status: OutcomeStatus.Ok, data }; +} +export function failure(error: E): Outcome { + return { status: OutcomeStatus.Error, error }; +} From 898b495f521a182e950d896ce44ec38f2b4ae65d Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 21 May 2026 20:04:33 +0200 Subject: [PATCH 03/11] more changes to modal flow, fixes some issues I hope --- .../src/modals/MediaDbAdvancedSearchModal.ts | 5 +- .../src/modals/MediaDbIdSearchModal.ts | 5 +- .../obsidian/src/modals/MediaDbSearchModal.ts | 5 +- packages/obsidian/src/styles.css | 27 ++++ .../obsidian/src/utils/MediaDbEntryHelper.ts | 56 ++++++-- packages/obsidian/src/utils/ModalHelper.ts | 132 ++++++++++++++---- test/modal-helper.test.ts | 113 +++++++++++++++ 7 files changed, 301 insertions(+), 42 deletions(-) create mode 100644 test/modal-helper.test.ts diff --git a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index 04f0598..3bedff7 100644 --- a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -38,6 +38,8 @@ export class MediaDbAdvancedSearchModal extends Modal { keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { + event.preventDefault(); + event.stopImmediatePropagation(); void this.search(); } } @@ -57,8 +59,9 @@ export class MediaDbAdvancedSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn?.setDisabled(false); + this.searchBtn?.setDisabled(true); this.searchBtn?.setButtonText('Searching...'); + this.searchBtn?.buttonEl.addClass('media-db-plugin-button-loading'); this.submitCallback?.({ query: this.query, apis: apis }); } diff --git a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 1f65d30..1427d1e 100644 --- a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -38,6 +38,8 @@ export class MediaDbIdSearchModal extends Modal { keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { + event.preventDefault(); + event.stopImmediatePropagation(); void this.search(); } } @@ -55,8 +57,9 @@ export class MediaDbIdSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn?.setDisabled(false); + this.searchBtn?.setDisabled(true); this.searchBtn?.setButtonText('Searching...'); + this.searchBtn?.buttonEl.addClass('media-db-plugin-button-loading'); this.submitCallback?.({ query: this.query, api: this.selectedApi }); } diff --git a/packages/obsidian/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts index e62828d..03398cb 100644 --- a/packages/obsidian/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -41,6 +41,8 @@ export class MediaDbSearchModal extends Modal { keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { + event.preventDefault(); + event.stopImmediatePropagation(); void this.search(); } } @@ -60,8 +62,9 @@ export class MediaDbSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn?.setDisabled(false); + this.searchBtn?.setDisabled(true); this.searchBtn?.setButtonText('Searching...'); + this.searchBtn?.buttonEl.addClass('media-db-plugin-button-loading'); this.submitCallback?.({ query: this.query, types: types }); } diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index 18a0f55..71687f3 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -65,6 +65,10 @@ small.media-db-plugin-list-text { } .media-db-plugin-search-input { + box-sizing: border-box; + display: block; + max-width: 100%; + min-width: 100%; width: 100%; } @@ -72,6 +76,29 @@ small.media-db-plugin-list-text { /*outline: 1px solid white;*/ } +.media-db-plugin-button-loading { + align-items: center; + display: inline-flex; + gap: var(--size-4-2); + justify-content: center; +} + +.media-db-plugin-button-loading::before { + animation: media-db-plugin-button-loading-spin 0.8s linear infinite; + border: 2px solid currentColor; + border-radius: 50%; + border-top-color: transparent; + content: ''; + height: 14px; + width: 14px; +} + +@keyframes media-db-plugin-button-loading-spin { + to { + transform: rotate(360deg); + } +} + .media-db-plugin-preview { border-radius: var(--modal-radius); border: var(--modal-border-width) solid var(--modal-border-color); diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 2f558d3..7714161 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -8,7 +8,9 @@ import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; -import type { SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import type { ModalLifecycle, ModalSession, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import type { Outcome } from 'packages/obsidian/src/utils/result'; +import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; export class MediaDbEntryHelper { readonly plugin: MediaDbPlugin; @@ -21,13 +23,34 @@ export class MediaDbEntryHelper { this.plugin.errorReporter.report(error); } + private getModalData(modalResult: Outcome): T | undefined { + if (modalResult.status === OutcomeStatus.Ok) { + return modalResult.data; + } + + if (modalResult.status === OutcomeStatus.Error) { + this.reportMdbError(modalResult.error); + } + + return undefined; + } + + private async runModalQuery(session: ModalSession, query: () => Promise): Promise { + return this.getModalData(await this.plugin.modalHelper.runModalTask(session, query, { kind: MDBErrorKind.Api, message: 'API query failed' })); + } + async createLinkWithSearchModal(): Promise { - const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + const advancedSearchSession = await this.plugin.modalHelper.createAdvancedSearchModalSession({}); + const advancedSearch = this.getModalData(advancedSearchSession.modalResult); if (!advancedSearch) { return; } - const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + const apiSearchResults = await this.runModalQuery(advancedSearchSession, () => this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis)); + if (!apiSearchResults) { + return; + } + if (!apiSearchResults.ok) { this.reportMdbError(apiSearchResults.error); return; @@ -57,14 +80,19 @@ export class MediaDbEntryHelper { } async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { - const searchData = await this.plugin.modalHelper.promptSearchModal(searchModalOptions ?? {}); + const searchSession = await this.plugin.modalHelper.createSearchModalSession(searchModalOptions ?? {}); + const searchData = this.getModalData(searchSession.modalResult); if (!searchData) { return; } const types = searchData.types; const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName); - const apiSearchResults = await this.plugin.apiManager.query(searchData.query, apis); + const apiSearchResults = await this.runModalQuery(searchSession, () => this.plugin.apiManager.query(searchData.query, apis)); + if (!apiSearchResults) { + return; + } + if (!apiSearchResults.ok) { this.reportMdbError(apiSearchResults.error); return; @@ -114,12 +142,17 @@ export class MediaDbEntryHelper { } async createEntryWithAdvancedSearchModal(): Promise { - const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + const advancedSearchSession = await this.plugin.modalHelper.createAdvancedSearchModalSession({}); + const advancedSearch = this.getModalData(advancedSearchSession.modalResult); if (!advancedSearch) { return; } - const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + const apiSearchResults = await this.runModalQuery(advancedSearchSession, () => this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis)); + if (!apiSearchResults) { + return; + } + if (!apiSearchResults.ok) { this.reportMdbError(apiSearchResults.error); return; @@ -153,12 +186,17 @@ export class MediaDbEntryHelper { let proceed = false; while (!proceed) { - const idSearchData = await this.plugin.modalHelper.promptIdSearchModal({}); + const idSearchSession = await this.plugin.modalHelper.createIdSearchModalSession({}); + const idSearchData = this.getModalData(idSearchSession.modalResult); if (!idSearchData) { return; } - const queriedIdResult = await this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api); + const queriedIdResult = await this.runModalQuery(idSearchSession, () => this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api)); + if (!queriedIdResult) { + return; + } + if (!queriedIdResult.ok) { this.reportMdbError(queriedIdResult.error); return; diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index 36f4b61..b014a8d 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -105,9 +105,16 @@ export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { submitButtonText: 'Ok', }; -interface ModalCoreResult { +export interface ModalLifecycle { + open(): void; + close(): void; +} + +export interface ModalSession { modalResult: Outcome; modal: TModal; + close(): void; + isCancelled(): boolean; } export class ModalHelper { @@ -117,17 +124,39 @@ export class ModalHelper { this.plugin = plugin; } - private async openModalCore( + private async openModalCore( createModal: () => TModal, wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, - ): Promise> { + ): Promise> { const modal = createModal(); + let closeRequested = false; + let cancelledByUser = false; + let resolved = false; const modalResult = await new Promise>(resolve => { - wireHandlers(modal, resolve); + const resolveSession = (result: Outcome): void => { + if (result.status === OutcomeStatus.Cancelled && !closeRequested) { + cancelledByUser = true; + } + + if (!resolved) { + resolved = true; + resolve(result); + } + }; + + wireHandlers(modal, resolveSession); modal.open(); }); - return { modalResult, modal }; + return { + modalResult, + modal, + close: (): void => { + closeRequested = true; + modal.close(); + }, + isCancelled: (): boolean => cancelledByUser, + }; } private async resolveOutcome(outcomePromise: Promise>): Promise { @@ -144,8 +173,45 @@ export class ModalHelper { return undefined; } + async runModalTask( + session: ModalSession, + task: () => Promise, + errorFallback: MDBError = { kind: MDBErrorKind.Modal, message: 'Modal task failed' }, + ): Promise> { + try { + const result = await task(); + + if (session.isCancelled()) { + return cancelled(); + } + + return success(result); + } catch (err) { + if (session.isCancelled()) { + return cancelled(); + } + + return failure(toMdbError(err, errorFallback)); + } finally { + if (!session.isCancelled()) { + session.close(); + } + } + } + async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.createSearchModalSession(searchModalOptions); + const { modalResult } = session; + + if (modalResult.status === OutcomeStatus.Ok) { + session.close(); + } + + return modalResult; + } + + async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise> { + return await this.openModalCore( () => new MediaDbSearchModal(this.plugin, searchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -159,12 +225,6 @@ export class ModalHelper { }); }, ); - - if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); - } - - return modalResult; } async promptSearchModal(searchModalOptions: SearchModalOptions): Promise { @@ -172,7 +232,18 @@ export class ModalHelper { } async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.createAdvancedSearchModalSession(advancedSearchModalOptions); + const { modalResult } = session; + + if (modalResult.status === OutcomeStatus.Ok) { + session.close(); + } + + return modalResult; + } + + async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { + return await this.openModalCore( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -186,12 +257,6 @@ export class ModalHelper { }); }, ); - - if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); - } - - return modalResult; } async promptAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise { @@ -199,7 +264,18 @@ export class ModalHelper { } async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.createIdSearchModalSession(idSearchModalOptions); + const { modalResult } = session; + + if (modalResult.status === OutcomeStatus.Ok) { + session.close(); + } + + return modalResult; + } + + async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise> { + return await this.openModalCore( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -213,12 +289,6 @@ export class ModalHelper { }); }, ); - - if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); - } - - return modalResult; } async promptIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise { @@ -226,7 +296,7 @@ export class ModalHelper { } async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.openModalCore( () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -241,9 +311,10 @@ export class ModalHelper { }); }, ); + const { modalResult } = session; if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) { - modal.close(); + session.close(); } return modalResult; @@ -254,7 +325,7 @@ export class ModalHelper { } async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.openModalCore( () => new MediaDbPreviewModal(this.plugin, previewModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -268,9 +339,10 @@ export class ModalHelper { }); }, ); + const { modalResult } = session; if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); + session.close(); } return modalResult; diff --git a/test/modal-helper.test.ts b/test/modal-helper.test.ts new file mode 100644 index 0000000..02d8f98 --- /dev/null +++ b/test/modal-helper.test.ts @@ -0,0 +1,113 @@ +import { expect, mock, test } from 'bun:test'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { ModalLifecycle, ModalSession } from 'packages/obsidian/src/utils/ModalHelper'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; +import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; + +mock.module('obsidian', () => ({ + AbstractInputSuggest: class {}, + Component: class { + load(): void {} + unload(): void {} + }, + DropdownComponent: class {}, + MarkdownRenderer: { render: async (): Promise => {} }, + MarkdownView: class {}, + Modal: class { + app: unknown; + + constructor(app: unknown) { + this.app = app; + } + + open(): void {} + close(): void {} + }, + Notice: class {}, + normalizePath: (path: string): string => path, + moment: Object.assign((value?: unknown): unknown => value, { locale: (): void => {} }), + parseYaml: (): unknown => ({}), + Plugin: class {}, + PluginSettingTab: class {}, + requestUrl: async (): Promise => ({}), + SecretComponent: class {}, + Setting: class {}, + SettingGroup: class {}, + stringifyYaml: (value: unknown): string => String(value), + TFile: class {}, + TFolder: class {}, + TextComponent: class {}, + ToggleComponent: class {}, +})); + +class FakeModal implements ModalLifecycle { + closeCount = 0; + + open(): void { + // Test modal does not need to render. + } + + close(): void { + this.closeCount += 1; + } +} + +function createModalSession(cancelled: () => boolean): ModalSession { + const modal = new FakeModal(); + + return { + modal, + modalResult: { status: OutcomeStatus.Ok, data: undefined }, + close: (): void => modal.close(), + isCancelled: cancelled, + }; +} + +async function createHelper(): Promise { + const { ModalHelper } = await import('packages/obsidian/src/utils/ModalHelper'); + return new ModalHelper({} as MediaDbPlugin); +} + +test('runModalTask closes the modal after active task success', async () => { + const helper = await createHelper(); + const session = createModalSession(() => false); + + const outcome = await helper.runModalTask(session, async () => 'done'); + + expect(outcome).toEqual({ status: OutcomeStatus.Ok, data: 'done' }); + expect(session.modal.closeCount).toBe(1); +}); + +test('runModalTask ignores late success after cancellation', async () => { + const helper = await createHelper(); + let cancelled = false; + const session = createModalSession(() => cancelled); + + const outcome = await helper.runModalTask(session, async () => { + cancelled = true; + return 'late result'; + }); + + expect(outcome).toEqual({ status: OutcomeStatus.Cancelled }); + expect(session.modal.closeCount).toBe(0); +}); + +test('runModalTask ignores late errors after cancellation', async () => { + const helper = await createHelper(); + let cancelled = false; + const session = createModalSession(() => cancelled); + const fallback: MDBError = { kind: MDBErrorKind.Modal, message: 'Task failed' }; + + const outcome = await helper.runModalTask( + session, + async () => { + cancelled = true; + throw new Error('Late failure'); + }, + fallback, + ); + + expect(outcome).toEqual({ status: OutcomeStatus.Cancelled }); + expect(session.modal.closeCount).toBe(0); +}); From ad568b5c9f041dd96eac768794e3dc9a69b1d4fb Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 21 May 2026 20:14:11 +0200 Subject: [PATCH 04/11] another cleanup pass --- .../src/modals/MediaDbAdvancedSearchModal.ts | 6 +- .../src/modals/MediaDbIdSearchModal.ts | 6 +- .../obsidian/src/modals/MediaDbSearchModal.ts | 7 +- .../src/modals/MediaDbSearchResultModal.ts | 1 + .../src/modals/MediaDbSeasonSelectModal.ts | 7 +- packages/obsidian/src/modals/SelectModal.ts | 6 + .../obsidian/src/utils/MediaDbEntryHelper.ts | 15 +- packages/obsidian/src/utils/ModalHelper.ts | 201 +++++++++--------- 8 files changed, 125 insertions(+), 124 deletions(-) diff --git a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index 3bedff7..2282376 100644 --- a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -22,7 +22,7 @@ export class MediaDbAdvancedSearchModal extends Modal { super(plugin.app); this.plugin = plugin; - this.selectedApis = []; + this.selectedApis = [...(advancedSearchModalOptions.preselectedAPIs ?? [])]; this.title = advancedSearchModalOptions.modalTitle ?? ''; this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; this.isBusy = false; @@ -101,7 +101,9 @@ export class MediaDbAdvancedSearchModal extends Modal { apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); apiToggleComponent.onChange(value => { if (value) { - this.selectedApis.push(api.apiName); + if (!this.selectedApis.includes(api.apiName)) { + this.selectedApis.push(api.apiName); + } } else { this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); } diff --git a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 1427d1e..48a5192 100644 --- a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -23,8 +23,8 @@ export class MediaDbIdSearchModal extends Modal { this.plugin = plugin; this.title = idSearchModalOptions.modalTitle ?? ''; - this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName; - this.query = ''; + this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0]?.apiName ?? ''; + this.query = idSearchModalOptions.prefilledSearchString ?? ''; this.isBusy = false; } @@ -74,6 +74,7 @@ export class MediaDbIdSearchModal extends Modal { const searchComponent = new TextComponent(contentEl); searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); + searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); @@ -93,6 +94,7 @@ export class MediaDbIdSearchModal extends Modal { for (const api of this.plugin.apiManager.apis) { apiSelectorComponent.addOption(api.apiName, api.apiName); } + apiSelectorComponent.setValue(this.selectedApi); apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); diff --git a/packages/obsidian/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts index 03398cb..248da59 100644 --- a/packages/obsidian/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -109,12 +109,13 @@ export class MediaDbSearchModal extends Modal { if (value) { if (currentToggle && currentToggle !== apiToggleComponent) { currentToggle.setValue(false); - this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } currentToggle = apiToggleComponent; - this.selectedTypes.push(mediaType); + this.selectedTypes = [mediaType]; } else { - currentToggle = undefined; + if (currentToggle === apiToggleComponent) { + currentToggle = undefined; + } this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } }); diff --git a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts index e513663..f6f372f 100644 --- a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -62,5 +62,6 @@ export class MediaDbSearchResultModal extends SelectModal { onClose(): void { this.closeCallback?.(); + super.onClose(); } } diff --git a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index 6508c63..5bf902d 100644 --- a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -4,6 +4,7 @@ import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; export interface SeasonSelectModalElement { season_number: number; name: string; + episode_count?: number; air_date?: string; poster_path?: string; } @@ -33,7 +34,6 @@ export class MediaDbSeasonSelectModal extends SelectModal x.isActive()).map(x => x.value); this.submitCallback?.(selected); - this.close(); } skip(): void { @@ -47,4 +47,9 @@ export class MediaDbSeasonSelectModal extends SelectModal void): void { this.closeCallback = cb; } + + onClose(): void { + this.closeCallback?.(); + super.onClose(); + } } diff --git a/packages/obsidian/src/modals/SelectModal.ts b/packages/obsidian/src/modals/SelectModal.ts index 8d9ef3e..d6c142f 100644 --- a/packages/obsidian/src/modals/SelectModal.ts +++ b/packages/obsidian/src/modals/SelectModal.ts @@ -81,6 +81,7 @@ export abstract class SelectModal extends Modal { onOpen(): void { const { contentEl, titleEl } = this; + this.selectModalElements = []; titleEl.createEl('h2', { text: this.title }); contentEl.addClass('media-db-plugin-select-modal'); contentEl.createEl('p', { text: this.description }); @@ -125,6 +126,11 @@ export abstract class SelectModal extends Modal { }); } + onClose(): void { + this.selectModalElements = []; + this.contentEl.empty(); + } + activateHighlighted(): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.isHighlighted()) { diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 7714161..b5c4f7e 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -2,7 +2,6 @@ import { MarkdownView, Notice } from 'obsidian'; import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; -import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; @@ -288,22 +287,16 @@ export class MediaDbEntryHelper { } private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { - const modal = new MediaDbSeasonSelectModal( - this.plugin, - allSeasons.map(season => ({ + return await this.plugin.modalHelper.promptSeasonSelectModal({ + seasons: allSeasons.map(season => ({ season_number: season.seasonNumber, name: season.seasonTitle || season.title, episode_count: season.episodes || 0, air_date: season.year, poster_path: season.image, })), - true, - seriesTitle, - ); - - return await new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); + multiSelect: true, + seriesName: seriesTitle, }); } diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index b014a8d..3df0ee7 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -4,6 +4,8 @@ import { MediaDbIdSearchModal } from 'packages/obsidian/src/modals/MediaDbIdSear import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal'; import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; +import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; @@ -66,6 +68,12 @@ export interface PreviewModalOptions { elements?: MediaTypeModel[]; } +export interface SeasonSelectModalOptions { + seasons?: SeasonSelectModalElement[]; + multiSelect?: boolean; + seriesName?: string; +} + export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { modalTitle: 'Media DB Search', preselectedTypes: [], @@ -110,6 +118,14 @@ export interface ModalLifecycle { close(): void; } +interface CloseAwareModal extends ModalLifecycle { + setCloseCb(closeCallback: (err?: Error) => void): void; +} + +interface SubmitModal extends CloseAwareModal { + setSubmitCb(submitCallback: (res: TData) => void): void; +} + export interface ModalSession { modalResult: Outcome; modal: TModal; @@ -134,11 +150,11 @@ export class ModalHelper { let resolved = false; const modalResult = await new Promise>(resolve => { const resolveSession = (result: Outcome): void => { - if (result.status === OutcomeStatus.Cancelled && !closeRequested) { - cancelledByUser = true; - } - if (!resolved) { + if (result.status === OutcomeStatus.Cancelled && !closeRequested) { + cancelledByUser = true; + } + resolved = true; resolve(result); } @@ -159,6 +175,40 @@ export class ModalHelper { }; } + private createCloseOutcome(err: Error | undefined, message: string): Outcome { + if (err) { + return failure(toMdbError(err, { kind: MDBErrorKind.Modal, message })); + } + + return cancelled(); + } + + private async openSubmitModalSession>( + createModal: () => TModal, + closeErrorMessage: string, + wireExtraHandlers?: (modal: TModal, resolve: (result: Outcome) => void) => void, + ): Promise> { + return await this.openModalCore(createModal, (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => resolve(this.createCloseOutcome(err, closeErrorMessage))); + wireExtraHandlers?.(modal, resolve); + }); + } + + private async resolveModalSessionOutcome( + sessionPromise: Promise>, + closeOn: readonly OutcomeStatus[] = [OutcomeStatus.Ok], + ): Promise> { + const session = await sessionPromise; + const { modalResult } = session; + + if (closeOn.includes(modalResult.status)) { + session.close(); + } + + return modalResult; + } + private async resolveOutcome(outcomePromise: Promise>): Promise { const outcome = await outcomePromise; @@ -200,30 +250,13 @@ export class ModalHelper { } async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { - const session = await this.createSearchModalSession(searchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createSearchModalSession(searchModalOptions)); } async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbSearchModal(this.plugin, searchModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Search modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + 'Search modal closed with an error', ); } @@ -232,30 +265,13 @@ export class ModalHelper { } async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - const session = await this.createAdvancedSearchModalSession(advancedSearchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createAdvancedSearchModalSession(advancedSearchModalOptions)); } async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + 'Advanced search modal closed with an error', ); } @@ -264,30 +280,13 @@ export class ModalHelper { } async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { - const session = await this.createIdSearchModalSession(idSearchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createIdSearchModalSession(idSearchModalOptions)); } async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Id search modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + 'Id search modal closed with an error', ); } @@ -296,28 +295,16 @@ export class ModalHelper { } async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { - const session = await this.openModalCore( - () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setSkipCallback(() => resolve(skipped())); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Select modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), + 'Select modal closed with an error', + (modal, resolve) => { + modal.setSkipCallback(() => resolve(skipped())); + }, + ), + [OutcomeStatus.Ok, OutcomeStatus.Skipped], ); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) { - session.close(); - } - - return modalResult; } async promptSelectModal(selectModalOptions: SelectModalOptions): Promise { @@ -325,30 +312,34 @@ export class ModalHelper { } async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { - const session = await this.openModalCore( - () => new MediaDbPreviewModal(this.plugin, previewModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Preview modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => new MediaDbPreviewModal(this.plugin, previewModalOptions), + 'Preview modal closed with an error', + ), ); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; } async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise { return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions)); } + + async createSeasonSelectModalOutcome(seasonSelectModalOptions: SeasonSelectModalOptions): Promise> { + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => + new MediaDbSeasonSelectModal( + this.plugin, + seasonSelectModalOptions.seasons ?? [], + seasonSelectModalOptions.multiSelect ?? true, + seasonSelectModalOptions.seriesName, + ), + 'Season select modal closed with an error', + ), + ); + } + + async promptSeasonSelectModal(seasonSelectModalOptions: SeasonSelectModalOptions): Promise { + return await this.resolveOutcome(this.createSeasonSelectModalOutcome(seasonSelectModalOptions)); + } } From 0fdda8b9ff98636aad83448febc8bc2b0763198d Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:52:45 +0200 Subject: [PATCH 05/11] add repo config --- repo-automation.config.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 repo-automation.config.json diff --git a/repo-automation.config.json b/repo-automation.config.json new file mode 100644 index 0000000..2e5ed34 --- /dev/null +++ b/repo-automation.config.json @@ -0,0 +1,6 @@ +{ + "devBranch": "main", + "releaseBranch": "release", + "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin", + "preconditions": ["bun run typecheck", "bun run format", "bun run lint:fix", "bun run test"] +} From 7695a443503dc62e6928f33177aa77f92cd095da Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:53:40 +0200 Subject: [PATCH 06/11] update deps --- bun.lock | 184 ++++++++++++++++++++++++++------------------------- package.json | 8 +-- 2 files changed, 97 insertions(+), 95 deletions(-) diff --git a/bun.lock b/bun.lock index 9f91cfa..5d55e40 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.9.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", @@ -18,11 +18,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.12", + "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.58.0", - "vite": "^8.0.10", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0", @@ -30,41 +30,41 @@ }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], @@ -130,7 +130,7 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], "@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="], @@ -138,39 +138,39 @@ "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], - "@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="], + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.15", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -184,19 +184,19 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], @@ -204,25 +204,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -260,23 +260,23 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -286,7 +286,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -330,11 +330,11 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.351", "", {}, "sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.365", "", {}, "sha512-xfip4u1QF1s+URFqpA6N+OeFpDGpN7VJz1f3MO3bVL0QYBjpGiZ5/Of7kugvM+o8TTqmanUlviHN3c8M9vYWCw=="], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], - "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -346,7 +346,7 @@ "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -364,7 +364,7 @@ "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + "eslint-module-utils": ["eslint-module-utils@2.13.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ=="], "eslint-plugin-depend": ["eslint-plugin-depend@1.3.1", "", { "dependencies": { "empathic": "^2.0.0", "module-replacements": "^2.8.0", "semver": "^7.6.3" } }, "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw=="], @@ -468,7 +468,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -550,7 +550,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -624,7 +624,7 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -644,7 +644,7 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "obsidian": ["obsidian@1.13.0", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-PHw5+SAPlJ0S3leFvJ0wgFg63Z3DavxL6+d1ll+8toXR2ZlYKc1rMWqdUv9LgUbTwPQUyY6yfhOMMivampRRiQ=="], "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="], @@ -682,7 +682,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -704,7 +704,7 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -712,7 +712,7 @@ "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], - "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -748,7 +748,7 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], + "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], @@ -782,7 +782,7 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -806,15 +806,15 @@ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], + "typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -822,7 +822,7 @@ "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], - "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], @@ -844,15 +844,15 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "which-typed-array": ["which-typed-array@1.1.21", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], @@ -870,13 +870,15 @@ "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@redocly/openapi-core/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -888,13 +890,13 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "eslint-compat-utils/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-compat-utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "eslint-plugin-depend/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-depend/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -906,11 +908,11 @@ "eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "eslint-plugin-n/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-n/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + "eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], - "eslint-plugin-obsidianmd/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-obsidianmd/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], @@ -920,7 +922,7 @@ "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - "jsonc-eslint-parser/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "jsonc-eslint-parser/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], @@ -934,17 +936,17 @@ "@microsoft/eslint-plugin-sdl/eslint-plugin-security/safe-regex": ["safe-regex@1.1.0", "", { "dependencies": { "ret": "~0.1.10" } }, "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg=="], - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "eslint-plugin-json-schema-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/package.json b/package.json index 5661e62..b743540 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.9.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", @@ -34,11 +34,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.12", + "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.58.0", - "vite": "^8.0.10", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0" From 768c916fa797adda68475edfd2a865d4a01fe130 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:54:13 +0200 Subject: [PATCH 07/11] fix config --- repo-automation.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo-automation.config.json b/repo-automation.config.json index 2e5ed34..560b511 100644 --- a/repo-automation.config.json +++ b/repo-automation.config.json @@ -1,5 +1,5 @@ { - "devBranch": "main", + "devBranch": "master", "releaseBranch": "release", "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin", "preconditions": ["bun run typecheck", "bun run format", "bun run lint:fix", "bun run test"] From 07c0421dc51528da4f2c4f73a0ba1da0f049efa3 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:55:48 +0200 Subject: [PATCH 08/11] fix errors --- bun.lock | 4 ++-- package.json | 2 +- packages/obsidian/src/main.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 5d55e40..614ffc2 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", - "eslint-plugin-obsidianmd": "^0.2.9", + "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", @@ -380,7 +380,7 @@ "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], - "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="], + "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.3.0", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-QvGDI6B2nxJBrsZKGTg31da2A/fEJNlnwN+fRZkaoPIu1QL3fYXUdpP7ThyMdr/0iTYQxifb9lt2X9cpydQx1w=="], "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], diff --git a/package.json b/package.json index b743540..a247822 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", - "eslint-plugin-obsidianmd": "^0.2.9", + "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts index 3bddc83..48401d7 100644 --- a/packages/obsidian/src/main.ts +++ b/packages/obsidian/src/main.ts @@ -32,7 +32,7 @@ import { ModalHelper } from 'packages/obsidian/src/utils/ModalHelper'; import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; export default class MediaDbPlugin extends Plugin { - settings!: MediaDbPluginSettings; + declare settings: MediaDbPluginSettings; apiManager!: APIManager; mediaTypeManager!: MediaTypeManager; modelPropertyMapper!: PropertyMapper; From ee985419e0f2d3e8930f95dc1411943b74316198 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:56:06 +0200 Subject: [PATCH 09/11] [auto] bump version to `0.8.0-canary.20260602T115558` --- manifest-beta.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index de49a26..a4f1e1a 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,8 +1,8 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB", - "version": "0.8.0-canary.20260406T152718", - "minAppVersion": "1.5.0", + "version": "0.8.0-canary.20260602T115558", + "minAppVersion": "1.12.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev", From 21ad8c59d0b101a84b26ed5bccbffe42c21e6f12 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 3 Jun 2026 19:06:03 +0200 Subject: [PATCH 10/11] remove solid; rework season search --- bun.lock | 133 ++------------- package.json | 3 - packages/obsidian/src/api/APIModel.ts | 15 ++ .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 6 +- .../obsidian/src/api/apis/TMDBSeriesAPI.ts | 4 + .../src/models/SeasonSearchResultModel.ts | 30 ++++ packages/obsidian/src/settings/Icon.tsx | 24 --- .../settings/PropertyMappingModelComponent.ts | 159 ++++++++++++++++++ .../PropertyMappingModelComponent.tsx | 138 --------------- .../PropertyMappingModelsComponent.ts | 40 +++++ .../PropertyMappingModelsComponent.tsx | 16 -- packages/obsidian/src/settings/Settings.ts | 43 +++-- packages/obsidian/src/styles.css | 7 + .../obsidian/src/utils/BulkImportHelper.ts | 29 +++- .../obsidian/src/utils/MediaDbEntryHelper.ts | 111 +++++++++--- tsconfig.json | 4 +- vite.config.mts | 2 - 17 files changed, 407 insertions(+), 357 deletions(-) create mode 100644 packages/obsidian/src/models/SeasonSearchResultModel.ts delete mode 100644 packages/obsidian/src/settings/Icon.tsx create mode 100644 packages/obsidian/src/settings/PropertyMappingModelComponent.ts delete mode 100644 packages/obsidian/src/settings/PropertyMappingModelComponent.tsx create mode 100644 packages/obsidian/src/settings/PropertyMappingModelsComponent.ts delete mode 100644 packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx diff --git a/bun.lock b/bun.lock index 614ffc2..e7ee5d4 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", "@types/bun": "^1.3.14", "eslint": "^9.39.4", - "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", @@ -18,13 +17,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0", }, }, @@ -32,40 +29,8 @@ "packages": { "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - - "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - - "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], @@ -110,16 +75,6 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lemons_dev/lemons-obsidian-plugin-automation": ["@lemons_dev/lemons-obsidian-plugin-automation@0.1.3", "", { "dependencies": { "@lemons_dev/parsinom": "^0.2.1", "moment": "^2.30.1", "string-argv": "^0.3.2" }, "peerDependencies": { "vite": "^8.0.9" }, "bin": { "lemons-automation": "bin/lemons-automation.js" } }, "sha512-Nv1eONy1Q9citS+nsMMasgL9ZfhKb57uinJmHRF+F1H+9HSiRkB5wlx9J/S1ou9cJL8IAhFvv72KGzE9HTIWtg=="], "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.2.1", "", {}, "sha512-ew+G3gm5aWJBnIhrysxI7k09FCAtAoj48wbTzr16NoX9ndhB8UnhmqjTxYVv46DsG88HQo+DTWPIFQW5qTAwwg=="], @@ -176,14 +131,6 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -196,7 +143,7 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], @@ -260,22 +207,14 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], - - "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -286,8 +225,6 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], @@ -302,14 +239,10 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], @@ -330,8 +263,6 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.365", "", {}, "sha512-xfip4u1QF1s+URFqpA6N+OeFpDGpN7VJz1f3MO3bVL0QYBjpGiZ5/Of7kugvM+o8TTqmanUlviHN3c8M9vYWCw=="], - "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], @@ -354,8 +285,6 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], @@ -434,8 +363,6 @@ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -470,8 +397,6 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -536,8 +461,6 @@ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -552,8 +475,6 @@ "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-migrate": ["json-schema-migrate@2.0.0", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ=="], @@ -602,12 +523,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -624,8 +541,6 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -666,8 +581,6 @@ "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -724,11 +637,7 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - - "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -748,10 +657,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], - - "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -814,9 +719,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -826,12 +729,8 @@ "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], - "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "vite-plugin-static-copy": ["vite-plugin-static-copy@4.1.0", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ=="], - "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -850,8 +749,6 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], @@ -862,8 +759,6 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@microsoft/eslint-plugin-sdl/eslint-plugin-security": ["eslint-plugin-security@1.4.0", "", { "dependencies": { "safe-regex": "^1.1.0" } }, "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA=="], @@ -878,28 +773,22 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "eslint-compat-utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "eslint-plugin-depend/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint-plugin-json-schema-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "eslint-plugin-json-schema-validator/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], @@ -908,26 +797,24 @@ "eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "eslint-plugin-n/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], "eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], - "eslint-plugin-obsidianmd/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "json-schema-migrate/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - "jsonc-eslint-parser/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "toml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -946,6 +833,8 @@ "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/package.json b/package.json index a247822..ff7d109 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", "@types/bun": "^1.3.14", "eslint": "^9.39.4", - "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", @@ -34,13 +33,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0" } } diff --git a/packages/obsidian/src/api/APIModel.ts b/packages/obsidian/src/api/APIModel.ts index c5bfe5d..cc7808b 100644 --- a/packages/obsidian/src/api/APIModel.ts +++ b/packages/obsidian/src/api/APIModel.ts @@ -1,9 +1,18 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; +export interface SeasonListAPIModel extends APIModel { + getSeasonsForSeries(seriesId: string): Promise>; +} + +export function isSeasonListAPIModel(api: APIModel | undefined): api is SeasonListAPIModel { + return typeof api?.getSeasonsForSeries === 'function'; +} + export abstract class APIModel { apiName!: string; apiUrl!: string; @@ -22,6 +31,12 @@ export abstract class APIModel { abstract getDisabledMediaTypes(): MediaType[]; + getSeasonsForSeries?(seriesId: string): Promise>; + + getSeasonApiNameForSeries(_series: MediaTypeModel): string | undefined { + return undefined; + } + hasType(type: MediaType): boolean { const disabledMediaTypes = this.getDisabledMediaTypes(); return this.types.includes(type) && !disabledMediaTypes.includes(type); diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts index ea16027..ae70295 100644 --- a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -3,6 +3,7 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel'; import { Logger } from 'packages/obsidian/src/utils/Logger'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; @@ -153,14 +154,13 @@ export class TMDBSeasonAPI extends APIModel { } } - return new SeasonModel({ + return new SeasonSearchResultModel({ title: `${result.name ?? result.original_name ?? ''}`, englishTitle: result.name ?? result.original_name ?? '', year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', dataSource: this.apiName, id: result.id?.toString() ?? '', - seasonTitle: result.name ?? result.original_name ?? '', - seasonNumber: totalSeasons, + seasonCount: totalSeasons, }); }), ); diff --git a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts index 61f727f..1a7f7af 100644 --- a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts @@ -252,4 +252,8 @@ export class TMDBSeriesAPI extends APIModel { getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes; } + + getSeasonApiNameForSeries(_series: MediaTypeModel): string { + return 'TMDBSeasonAPI'; + } } diff --git a/packages/obsidian/src/models/SeasonSearchResultModel.ts b/packages/obsidian/src/models/SeasonSearchResultModel.ts new file mode 100644 index 0000000..5d03338 --- /dev/null +++ b/packages/obsidian/src/models/SeasonSearchResultModel.ts @@ -0,0 +1,30 @@ +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { migrateObject } from 'packages/obsidian/src/utils/Utils'; + +export type SeasonSearchResultData = ModelToData; + +export class SeasonSearchResultModel extends MediaTypeModel { + seasonCount: number; + + constructor(obj: SeasonSearchResultData) { + super(); + this.seasonCount = 0; + + migrateObject(this, obj, this); + this.type = this.getMediaType(); + } + + getTags(): string[] { + return []; + } + + getMediaType(): MediaType { + return MediaType.Season; + } + + getSummary(): string { + return `${this.seasonCount} ${this.seasonCount === 1 ? 'season' : 'seasons'}`; + } +} diff --git a/packages/obsidian/src/settings/Icon.tsx b/packages/obsidian/src/settings/Icon.tsx deleted file mode 100644 index 9723197..0000000 --- a/packages/obsidian/src/settings/Icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { onMount, Show } from 'solid-js'; -import { setIcon } from 'obsidian'; - -interface IconProps { - iconName?: string; -} - -export default function Icon(props: IconProps) { - let iconEl: HTMLDivElement | undefined; - - onMount(() => { - if (iconEl) { - setIcon(iconEl, props.iconName || ''); - } - }); - - return ( - 0}> -
-
-
-
- ); -} diff --git a/packages/obsidian/src/settings/PropertyMappingModelComponent.ts b/packages/obsidian/src/settings/PropertyMappingModelComponent.ts new file mode 100644 index 0000000..ac41658 --- /dev/null +++ b/packages/obsidian/src/settings/PropertyMappingModelComponent.ts @@ -0,0 +1,159 @@ +import { Component, setIcon } from 'obsidian'; +import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping'; +import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions } from 'packages/obsidian/src/settings/PropertyMapping'; +import { capitalizeFirstLetter } from 'packages/obsidian/src/utils/Utils'; + +interface PropertyMappingModelComponentProps { + model: PropertyMappingModelData; + save: (model: PropertyMappingModelData) => void; +} + +export default class PropertyMappingModelComponent extends Component { + private readonly containerEl: HTMLElement; + private readonly save: (model: PropertyMappingModelData) => void; + private modelData: PropertyMappingModelData; + private unsavedChanges = false; + + constructor(containerEl: HTMLElement, props: PropertyMappingModelComponentProps) { + super(); + this.containerEl = containerEl; + this.modelData = props.model; + this.save = props.save; + } + + override onload(): void { + this.render(); + } + + override onunload(): void { + this.containerEl.empty(); + } + + private get validationResult(): { res: boolean; err?: Error } { + const model = PropertyMappingModel.fromJSON(this.modelData); + return model.validate(); + } + + private handleSave(): void { + const model = PropertyMappingModel.fromJSON(this.modelData); + if (!model.validate().res) { + return; + } + + this.save(model.toJSON()); + this.unsavedChanges = false; + this.render(); + } + + private onModelUpdate(): void { + this.unsavedChanges = true; + this.render(); + } + + private render(): void { + this.containerEl.empty(); + + const validationResult = this.validationResult; + const rootEl = this.containerEl.createDiv('media-db-plugin-property-mappings-model-container'); + const headerEl = rootEl.createDiv('media-db-plugin-property-mappings-model-header'); + headerEl.createDiv({ cls: 'setting-item-name', text: capitalizeFirstLetter(this.modelData.type) }); + + const actionsEl = headerEl.createDiv('media-db-plugin-property-mappings-model-actions'); + if (this.unsavedChanges) { + actionsEl.createDiv({ cls: 'media-db-plugin-property-mapping-unsaved-changes', text: 'Unsaved changes' }); + } + + const saveButtonEl = actionsEl.createEl('button', { + cls: `media-db-plugin-property-mappings-save-button ${validationResult.res ? 'mod-cta' : 'mod-muted'}`, + text: 'Save', + }); + saveButtonEl.addEventListener('click', () => this.handleSave()); + + if (!validationResult.res) { + rootEl.createDiv({ + cls: 'media-db-plugin-property-mapping-validation', + text: validationResult.err?.message ?? '', + }); + } + + const tableContainerEl = rootEl.createDiv('media-db-plugin-property-mappings-table-container'); + const tableEl = tableContainerEl.createEl('table', 'media-db-plugin-property-mappings-table'); + const tableHeadEl = tableEl.createEl('thead'); + const headerRowEl = tableHeadEl.createEl('tr'); + headerRowEl.createEl('th', { cls: 'col-property', text: 'Property' }); + headerRowEl.createEl('th', { cls: 'col-mapping', text: 'Mapping' }); + headerRowEl.createEl('th', { cls: 'col-new-name', text: 'New name' }); + headerRowEl.createEl('th', { cls: 'col-wikilink', text: 'Wikilink' }); + + const tableBodyEl = tableEl.createEl('tbody'); + for (const [index, property] of this.modelData.properties.entries()) { + const rowEl = tableBodyEl.createEl('tr'); + const propertyCellEl = rowEl.createEl('td', 'col-property'); + propertyCellEl.createEl('code', { text: property.property }); + + if (property.locked) { + const lockedCellEl = rowEl.createEl('td', 'col-locked'); + lockedCellEl.colSpan = 3; + lockedCellEl.createDiv({ + cls: 'media-db-plugin-property-binding-text', + text: 'property cannot be remapped', + }); + continue; + } + + const mappingCellEl = rowEl.createEl('td', 'col-mapping'); + const selectEl = mappingCellEl.createEl('select', 'dropdown'); + for (const remappingOption of propertyMappingOptions) { + selectEl.createEl('option', { + attr: { value: remappingOption }, + text: remappingOption, + }); + } + selectEl.value = propertyMappingOptions.includes(property.mapping) ? property.mapping : PropertyMappingOption.Default; + selectEl.addEventListener('change', event => { + const targetEl = event.currentTarget as HTMLSelectElement; + this.modelData.properties[index].mapping = targetEl.value as PropertyMappingOption; + this.modelData.properties[index].newProperty = ''; + this.onModelUpdate(); + }); + + const newNameCellEl = rowEl.createEl('td', 'col-new-name'); + if (property.mapping === PropertyMappingOption.Map) { + const mappingToEl = newNameCellEl.createDiv('media-db-plugin-property-mapping-to'); + const iconWrapperEl = mappingToEl.createDiv('icon-wrapper'); + const iconEl = iconWrapperEl.createDiv('icon'); + setIcon(iconEl, 'arrow-right'); + + const inputEl = mappingToEl.createEl('input', { + cls: 'media-db-plugin-property-mapping-input', + type: 'text', + }); + inputEl.spellcheck = false; + inputEl.value = property.newProperty; + inputEl.addEventListener('input', event => { + const targetEl = event.currentTarget as HTMLInputElement; + this.modelData.properties[index].newProperty = targetEl.value; + this.onModelUpdate(); + }); + } else { + newNameCellEl.createSpan({ + cls: 'media-db-plugin-property-mapping-to-disabled', + text: 'N/A', + }); + } + + const wikilinkCellEl = rowEl.createEl('td', 'col-wikilink'); + const wikilinkLabelEl = wikilinkCellEl.createEl('label', { + cls: 'media-db-plugin-property-mapping-wikilink-label', + attr: { title: 'Convert value to wikilink ([[value]])' }, + }); + const wikilinkInputEl = wikilinkLabelEl.createEl('input', { type: 'checkbox' }); + wikilinkInputEl.checked = property.wikilink ?? false; + wikilinkInputEl.addEventListener('change', event => { + const targetEl = event.currentTarget as HTMLInputElement; + this.modelData.properties[index].wikilink = targetEl.checked; + this.onModelUpdate(); + }); + } + } +} diff --git a/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx deleted file mode 100644 index c31bdbf..0000000 --- a/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { createSignal, createMemo, For, Show } from 'solid-js'; -import { createStore } from 'solid-js/store'; -import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; -import { capitalizeFirstLetter } from '../utils/Utils'; -import Icon from './Icon'; - -interface PropertyMappingModelComponentProps { - model: PropertyMappingModelData; - save: (model: PropertyMappingModelData) => void; -} - -export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { - const [unsavedChanges, setUnsavedChanges] = createSignal(false); - - // Create a store from the model's plain data - const [modelData, setModelData] = createStore(props.model); - - // Derive the validation result reactively - const validationResult = createMemo(() => { - const model = PropertyMappingModel.fromJSON(modelData); - return model.validate(); - }); - - const onModelUpdate = () => { - setUnsavedChanges(true); - }; - - const handleSave = () => { - const model = PropertyMappingModel.fromJSON(modelData); - if (model.validate().res) { - props.save(model); - setUnsavedChanges(false); - } - }; - - return ( -
-
-
{capitalizeFirstLetter(modelData.type)}
- -
- -
Unsaved changes
-
- - -
-
- - -
{validationResult().err?.message}
-
- -
- - - - - - - - - - - - {(property, index) => ( - - - - -
property cannot be remapped
- - } - > -
- - - - - - - )} - - -
PropertyMappingNew nameWikilink
- {property.property} - - - - N/A} - > -
- - { - setModelData('properties', index(), 'newProperty', e.currentTarget.value); - onModelUpdate(); - }} - /> -
-
-
-
-
- ); -} diff --git a/packages/obsidian/src/settings/PropertyMappingModelsComponent.ts b/packages/obsidian/src/settings/PropertyMappingModelsComponent.ts new file mode 100644 index 0000000..269669d --- /dev/null +++ b/packages/obsidian/src/settings/PropertyMappingModelsComponent.ts @@ -0,0 +1,40 @@ +import { Component } from 'obsidian'; +import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping'; +import PropertyMappingModelComponent from 'packages/obsidian/src/settings/PropertyMappingModelComponent'; + +interface PropertyMappingModelsComponentProps { + models?: PropertyMappingModelData[]; + save: (model: PropertyMappingModelData) => void; +} + +export default class PropertyMappingModelsComponent extends Component { + private readonly containerEl: HTMLElement; + private readonly models: PropertyMappingModelData[]; + private readonly save: (model: PropertyMappingModelData) => void; + + constructor(containerEl: HTMLElement, props: PropertyMappingModelsComponentProps) { + super(); + this.containerEl = containerEl; + this.models = props.models ?? []; + this.save = props.save; + } + + override onload(): void { + this.containerEl.empty(); + const rootEl = this.containerEl.createDiv('setting-item media-db-plugin-property-mappings-models-container'); + + for (const model of this.models) { + const modelContainerEl = rootEl.createDiv(); + this.addChild( + new PropertyMappingModelComponent(modelContainerEl, { + model, + save: this.save, + }), + ); + } + } + + override onunload(): void { + this.containerEl.empty(); + } +} diff --git a/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx deleted file mode 100644 index 35274ff..0000000 --- a/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { For } from 'solid-js'; -import { type PropertyMappingModelData } from './PropertyMapping'; -import PropertyMappingModelComponent from './PropertyMappingModelComponent'; - -interface PropertyMappingModelsComponentProps { - models?: PropertyMappingModelData[]; - save: (model: PropertyMappingModelData) => void; -} - -export default function PropertyMappingModelsComponent(props: PropertyMappingModelsComponentProps) { - return ( -
- {model => } -
- ); -} diff --git a/packages/obsidian/src/settings/Settings.ts b/packages/obsidian/src/settings/Settings.ts index 40ceb8d..b0885f5 100644 --- a/packages/obsidian/src/settings/Settings.ts +++ b/packages/obsidian/src/settings/Settings.ts @@ -10,7 +10,6 @@ import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderS import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; -import { render } from 'solid-js/web'; function createDateFormatDescription(preview: string): DocumentFragment { return createFragment(frag => { @@ -425,14 +424,25 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings // MARK: Settings Tab export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; + private propertyMappingModelsComponent?: PropertyMappingModelsComponent; constructor(app: App, plugin: MediaDbPlugin) { super(app, plugin); this.plugin = plugin; } + override hide(): void { + this.propertyMappingModelsComponent?.unload(); + this.propertyMappingModelsComponent = undefined; + super.hide(); + } + display(): void { const { containerEl } = this; + if (this.propertyMappingModelsComponent) { + this.propertyMappingModelsComponent.unload(); + this.propertyMappingModelsComponent = undefined; + } containerEl.empty(); const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt)); @@ -837,24 +847,21 @@ export class MediaDbSettingTab extends PluginSettingTab { mappingGroup.setHeading('Property mappings'); mappingGroup.addSetting(setting => { setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription()); + const propertyMappingsEl = setting.descEl.createDiv(); + this.propertyMappingModelsComponent = new PropertyMappingModelsComponent(propertyMappingsEl, { + models: structuredClone(this.plugin.settings.propertyMappingModels), + save: (model: PropertyMappingModelData): void => { + // Update the matching model in settings (stored as plain data) + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } - render( - () => - PropertyMappingModelsComponent({ - models: structuredClone(this.plugin.settings.propertyMappingModels), - save: (model: PropertyMappingModelData): void => { - // Update the matching model in settings (stored as plain data) - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } - - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); - void this.plugin.saveSettings(); - }, - }), - setting.descEl, - ); + new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + void this.plugin.saveSettings(); + }, + }); + this.propertyMappingModelsComponent.load(); }); } } diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index 71687f3..ed3a79e 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -120,6 +120,13 @@ small.media-db-plugin-list-text { } /* Property Mapping Component Styles */ +.media-db-plugin-property-mappings-models-container { + display: flex; + gap: 10px; + flex-direction: column; + align-items: stretch; +} + .media-db-plugin-property-mappings-model-container { margin-bottom: var(--size-4-8); } diff --git a/packages/obsidian/src/utils/BulkImportHelper.ts b/packages/obsidian/src/utils/BulkImportHelper.ts index f91ae37..cd1d535 100644 --- a/packages/obsidian/src/utils/BulkImportHelper.ts +++ b/packages/obsidian/src/utils/BulkImportHelper.ts @@ -1,7 +1,7 @@ import type { TFolder } from 'obsidian'; import { TFile } from 'obsidian'; import type MediaDbPlugin from 'packages/obsidian/src/main'; -import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal'; +import { MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils'; @@ -26,7 +26,7 @@ export class BulkImportHelper { async import(folder: TFolder): Promise { const erroredFiles: BulkImportError[] = []; - let canceled: boolean = false; + let canceled = false; const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{ selectedAPI: string; @@ -87,7 +87,11 @@ export class BulkImportHelper { } if (modelResult.value) { - await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined); + const createResult = await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined); + if (!createResult.ok) { + return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message }; + } + return undefined; } @@ -127,8 +131,25 @@ export class BulkImportHelper { return { filePath: file.path, error: `no search results selected` }; } + const seasonSelectionResult = await this.plugin.entryHelper.handleSeasonSearchSelections(selectModalResult.data.selected, appendContent ? file : undefined); + if (seasonSelectionResult.handled) { + if (!seasonSelectionResult.created) { + return { filePath: file.path, error: 'season selection canceled or no season note was created' }; + } + + return undefined; + } + const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected); - await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined); + if (detailedResults.length === 0) { + return { filePath: file.path, error: 'failed to load details for selected search results' }; + } + + const createResult = await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined); + if (!createResult.ok) { + return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message }; + } + return undefined; } diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index b5c4f7e..428c465 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -1,9 +1,12 @@ +import type { TFile } from 'obsidian'; import { MarkdownView, Notice } from 'obsidian'; -import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; +import type { SeasonListAPIModel } from 'packages/obsidian/src/api/APIModel'; +import { isSeasonListAPIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; @@ -112,6 +115,7 @@ export class MediaDbEntryHelper { types.length === 1 && types[0] === MediaType.Season ? await this.plugin.modalHelper.promptSelectModal({ elements: filteredSearchResults, + multiSelect: false, description: 'Select one search result to proceed.', submitButtonText: 'Ok', }) @@ -121,7 +125,11 @@ export class MediaDbEntryHelper { return; } - const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected); + if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) { + return; + } + + const selectResults = await this.queryDetails(selectResultsData.selected); if (selectResults.length === 0) { return; @@ -167,6 +175,10 @@ export class MediaDbEntryHelper { return; } + if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) { + return; + } + const selectResults = await this.queryDetails(selectResultsData.selected); if (selectResults.length < 1) { return; @@ -239,31 +251,62 @@ export class MediaDbEntryHelper { return detailModels; } - private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { - if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { - const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); - return { handled: true, seasonsCreated: created }; + private async handleSeasonWorkflow(types: MediaType[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { + if (!types.includes(MediaType.Series) || !types.includes(MediaType.Season)) { + return { handled: false }; } - if (types.includes('series') && selectResults.some(result => result.dataSource === 'TMDBSeriesAPI')) { - const seriesResults = selectResults.filter(result => result.dataSource === 'TMDBSeriesAPI'); - if (seriesResults.length === 1 && types.includes('season')) { - const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); - return { handled: true, seasonsCreated: created }; - } + const seriesResults = selectResults.filter(result => result.getMediaType() === MediaType.Series); + if (seriesResults.length !== 1) { + return { handled: false }; + } + + const seriesResult = seriesResults[0]; + const sourceApi = this.plugin.apiManager.getApiByName(seriesResult.dataSource); + const seasonApiName = sourceApi?.getSeasonApiNameForSeries(seriesResult); + if (seasonApiName) { + const created = await this.showSeasonSelectAndCreate(seriesResult.id, seriesResult.title, undefined, seasonApiName); + return { handled: true, seasonsCreated: created }; } return { handled: false }; } - private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { - const tmdbSeasonAPI = this.plugin.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI | undefined; - if (!tmdbSeasonAPI) { - new Notice('TMDBSeasonAPI not available.'); + private isSeasonSearchResult(model: MediaTypeModel): model is SeasonSearchResultModel { + return model instanceof SeasonSearchResultModel || (model.getMediaType() === MediaType.Season && typeof (model as { seasonCount?: unknown }).seasonCount === 'number'); + } + + async handleSeasonSearchSelections(selectedResults: MediaTypeModel[], attachFile?: TFile): Promise<{ handled: boolean; created: boolean }> { + const seasonSearchResults = selectedResults.filter(result => this.isSeasonSearchResult(result)); + if (seasonSearchResults.length === 0) { + return { handled: false, created: false }; + } + + if (selectedResults.length !== 1) { + new Notice('Select exactly one season search result before choosing seasons.'); + return { handled: true, created: false }; + } + + const selectedResult = seasonSearchResults[0]; + return { + handled: true, + created: await this.showSeasonSelectAndCreate(selectedResult.id, selectedResult.englishTitle || selectedResult.title, attachFile, selectedResult.dataSource), + }; + } + + private getSeasonListApi(apiName: string): SeasonListAPIModel | undefined { + const api = this.plugin.apiManager.getApiByName(apiName); + return isSeasonListAPIModel(api) ? api : undefined; + } + + private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string, attachFile: TFile | undefined, seasonApiName: string): Promise { + const seasonAPI = this.getSeasonListApi(seasonApiName); + if (!seasonAPI) { + new Notice(`${seasonApiName} does not support season selection.`); return false; } - const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); + const allSeasonsResult = await seasonAPI.getSeasonsForSeries(seriesId); if (!allSeasonsResult.ok) { this.reportMdbError(allSeasonsResult.error); new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`); @@ -281,8 +324,13 @@ export class MediaDbEntryHelper { return false; } - await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); - new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); + const createdCount = await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, seasonAPI, attachFile); + if (createdCount === 0) { + new Notice('No season entries were created.'); + return false; + } + + new Notice(`Successfully created ${createdCount} season ${createdCount === 1 ? 'entry' : 'entries'}.`); return true; } @@ -300,21 +348,36 @@ export class MediaDbEntryHelper { }); } - private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { - await Promise.all( + private async createNotesForSelectedSeasons( + selectedSeasons: SeasonSelectModalElement[], + allSeasons: SeasonModel[], + seasonAPI: SeasonListAPIModel, + attachFile?: TFile, + ): Promise { + const results = await Promise.all( selectedSeasons.map(async selectedSeason => { const seasonModel = allSeasons.find(season => season.seasonNumber === selectedSeason.season_number); if (seasonModel) { - const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id); + const fullMetadataResult = await seasonAPI.getById(seasonModel.id); if (!fullMetadataResult.ok) { this.reportMdbError(fullMetadataResult.error); new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`); - return; + return false; } - await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]); + const createResult = await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value], attachFile); + if (!createResult.ok) { + this.reportMdbError(createResult.error); + return false; + } + + return true; } + + return false; }), ); + + return results.filter(created => created).length; } } diff --git a/tsconfig.json b/tsconfig.json index 7ce937b..bb7ef48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,9 +23,7 @@ "sourceMap": true, "lib": ["DOM", "ESNext"], "allowSyntheticDefaultImports": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", "types": ["vite/client"] }, - "include": ["packages/obsidian/**/*.ts", "packages/obsidian/**/*.tsx"] + "include": ["packages/obsidian/**/*.ts"] } diff --git a/vite.config.mts b/vite.config.mts index e42ec6d..2cf072f 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,5 +1,4 @@ import { UserConfig, defineConfig } from 'vite'; -import solid from 'vite-plugin-solid'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; import path from 'path'; @@ -14,7 +13,6 @@ export default defineConfig(async ({ mode }) => { const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}/`; let plugins = [ - solid(), banner({ outDir: outDir, content: getBuildBanner(prod ? 'Release Build' : 'Dev Build', version => version), From 3d2789a73050b07c064a8a215d010bcbee5c41e0 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 3 Jun 2026 19:09:38 +0200 Subject: [PATCH 11/11] add check pr workflow --- .github/workflows/checkPR.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/checkPR.yml diff --git a/.github/workflows/checkPR.yml b/.github/workflows/checkPR.yml new file mode 100644 index 0000000..96fb046 --- /dev/null +++ b/.github/workflows/checkPR.yml @@ -0,0 +1,25 @@ +name: Run Checks and Tests on PR + +on: pull_request + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install Dependencies + id: build + run: | + bun install + + - name: Run Checks + run: | + bun run check \ No newline at end of file