add obsidian eslint plugin, solve major issues

This commit is contained in:
Moritz Jung 2026-05-06 16:26:59 +02:00
parent 84214a1c3c
commit 498e2611ae
105 changed files with 5702 additions and 5308 deletions

View file

@ -13,9 +13,14 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Determine prerelease status - name: Determine prerelease status
id: status id: status
@ -27,23 +32,32 @@ jobs:
fi fi
- name: Install Bun - name: Install Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v2
with: with:
bun-version: latest bun-version: latest
- name: Build - name: Build
id: build id: build
run: | run: |
bun install bun install --frozen-lockfile
bun run build bun run build
mkdir ${{ env.PLUGIN_NAME }} mkdir ${{ env.PLUGIN_NAME }}
cp dist/main.js dist/manifest.json dist/styles.css ${{ 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 }} zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }}
ls 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 - name: Release
id: release id: release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
prerelease: ${{ steps.status.outputs.prerelease }} prerelease: ${{ steps.status.outputs.prerelease }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -116,20 +116,18 @@ Now you select the result you want, and the plugin will cast its magic, creating
### Currently supported APIs: ### Currently supported APIs:
| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support | | 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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | 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 |
| [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 | | [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes |
| 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 | | [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 |
| [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 #### 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 - 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 - 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" - 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 #### 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) ` - 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` - 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` - 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) - [Comic Vine](https://www.comicvine.gamespot.com)
- you can find this ID in the URL - 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` - e.g. for "Boule & Bill" the URL looks like this `https://comicvine.gamespot.com/boule-bill/4050-70187/` so the ID is `4050-70187`

View file

@ -1,32 +0,0 @@
import { RequestUrlParam, RequestUrlResponse } from 'obsidian';
export function requestUrl(request: RequestUrlParam): Promise<RequestUrlResponse> {
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<string, string> object
const headers: Record<string, string> = {};
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;
});
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
`;
}

View file

@ -1,5 +0,0 @@
{
"devBranch": "master",
"releaseBranch": "release",
"github": "https://github.com/mProjectsCode/obsidian-media-db-plugin"
}

View file

@ -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();

View file

@ -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<void> {
// 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);
}
}

View file

@ -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();

View file

@ -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"]
}

View file

@ -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<void> {
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<string> {
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<number> {
console.log(`${message} `);
let optionNumbers = new Map<string, number>();
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<void> {
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',
};

View file

@ -1,6 +0,0 @@
export class UserError extends Error {}
export interface ProjectConfig {
corePackages: string[];
packages: string[];
}

View file

@ -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<number> = 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<string> = 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<Version> = 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),
];
}

295
bun.lock
View file

@ -5,33 +5,34 @@
"": { "": {
"name": "obsidian-media-db-plugin", "name": "obsidian-media-db-plugin",
"devDependencies": { "devDependencies": {
"@happy-dom/global-registrator": "^20.8.9", "@happy-dom/global-registrator": "^20.9.0",
"@lemons_dev/parsinom": "^0.1.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3",
"@types/bun": "^1.3.11", "@types/bun": "^1.3.13",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-plugin-import": "^2.32.0", "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", "eslint-plugin-only-warn": "^1.2.1",
"iso-639-2": "^3.0.2", "iso-639-2": "^3.0.2",
"obsidian": "latest", "obsidian": "latest",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"openapi-typescript": "^7.13.0", "openapi-typescript": "^7.13.0",
"prettier": "^3.8.1", "prettier": "^3.8.3",
"solid-js": "^1.9.12", "solid-js": "^1.9.12",
"string-argv": "^0.3.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.58.0", "typescript-eslint": "^8.58.0",
"vite": "^8.0.5", "vite": "^8.0.10",
"vite-plugin-banner": "^0.8.1", "vite-plugin-banner": "^0.8.1",
"vite-plugin-solid": "^2.11.12", "vite-plugin-solid": "^2.11.12",
"vite-plugin-static-copy": "^4.0.1", "vite-plugin-static-copy": "^4.1.0",
}, },
}, },
}, },
"packages": { "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.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=="], "@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/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=="], "@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=="], "@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=="], "@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/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/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=="], "@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/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=="], "@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/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=="], "@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=="], "@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/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/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=="], "@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=="], "@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/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/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.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@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/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=="], "@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=="], "@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=="], "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=="], "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=="], "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-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.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.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.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=="], "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=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
@ -247,19 +266,19 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "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-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=="], "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": ["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-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.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-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-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-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=="], "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-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=="], "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=="], "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-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=="], "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=="], "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
@ -403,7 +452,9 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "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-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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@ -551,20 +612,24 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "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=="], "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=="], "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-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=="], "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-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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": ["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=="], "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-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.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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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-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-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=="], "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=="], "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-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=="], "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=="], "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=="], "@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/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=="], "@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=="], "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-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-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-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=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "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=="], "@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=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
} }
} }

View file

@ -1,17 +1,25 @@
// @ts-check // @ts-check
import eslint from '@eslint/js'; import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
import only_warn from 'eslint-plugin-only-warn'; 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'], files: ['packages/obsidian/src/**/*.ts'],
extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked], extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
...obsidianmd.configs.recommended,
],
languageOptions: { languageOptions: {
parser: tseslint.parser, parser: tseslint.parser,
parserOptions: { parserOptions: {
@ -21,7 +29,8 @@ export default tseslint.config(
plugins: { plugins: {
// @ts-ignore // @ts-ignore
'only-warn': only_warn, 'only-warn': only_warn,
import: plugin_import, 'no-relative-import-paths': no_relative_import_paths,
obsidianmd: obsidianmd,
}, },
rules: { rules: {
'@typescript-eslint/no-explicit-any': ['warn'], '@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/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
'@typescript-eslint/restrict-template-expressions': 'off', '@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/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/explicit-function-return-type': ['warn'], '@typescript-eslint/explicit-function-return-type': ['warn'],
'@typescript-eslint/require-await': 'off', '@typescript-eslint/no-deprecated': 'off',
'obsidianmd/ui/sentence-case': 'off',
}, },
}, },
); );

View file

@ -2,7 +2,7 @@
"id": "obsidian-media-db-plugin", "id": "obsidian-media-db-plugin",
"name": "Media DB", "name": "Media DB",
"version": "0.8.0", "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.", "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", "author": "Moritz Jung",
"authorUrl": "https://www.moritzjung.dev", "authorUrl": "https://www.moritzjung.dev",

View file

@ -6,41 +6,41 @@
"scripts": { "scripts": {
"dev": "vite build --watch --mode development", "dev": "vite build --watch --mode development",
"build": "bun run tsc && vite build --mode production", "build": "bun run tsc && vite build --mode production",
"tsc": "tsc -noEmit -skipLibCheck", "typecheck": "tsc -noEmit -skipLibCheck",
"test": "bun test", "test": "bun test",
"test:log": "LOG_TESTS=true bun test", "test:log": "LOG_TESTS=true bun test",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"lint": "eslint --max-warnings=0 --no-warn-ignored src/**", "lint": "eslint --max-warnings=0 --no-warn-ignored packages/obsidian/src/**",
"lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**", "lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored packages/obsidian/src/**",
"check": "bun run format:check && bun run tsc && bun run lint", "check": "bun run format:check && bun run typecheck && bun run lint",
"check:fix": "bun run format && bun run tsc && bun run lint:fix", "check:fix": "bun run format && bun run typecheck && bun run lint:fix",
"release": "bun run automation/release.ts", "release": "lemons-automation release"
"stats": "bun run automation/stats.ts"
}, },
"keywords": [], "keywords": [],
"author": "Moritz Jung", "author": "Moritz Jung",
"license": "GPL-3.0", "license": "GPL-3.0",
"devDependencies": { "devDependencies": {
"@happy-dom/global-registrator": "^20.8.9", "@happy-dom/global-registrator": "^20.9.0",
"@lemons_dev/parsinom": "^0.1.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3",
"@types/bun": "^1.3.11", "@types/bun": "^1.3.13",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-plugin-import": "^2.32.0", "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", "eslint-plugin-only-warn": "^1.2.1",
"iso-639-2": "^3.0.2", "iso-639-2": "^3.0.2",
"obsidian": "latest", "obsidian": "latest",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"openapi-typescript": "^7.13.0", "openapi-typescript": "^7.13.0",
"prettier": "^3.8.1", "prettier": "^3.8.3",
"solid-js": "^1.9.12", "solid-js": "^1.9.12",
"string-argv": "^0.3.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.58.0", "typescript-eslint": "^8.58.0",
"vite": "^8.0.5", "vite": "^8.0.10",
"vite-plugin-banner": "^0.8.1", "vite-plugin-banner": "^0.8.1",
"vite-plugin-solid": "^2.11.12", "vite-plugin-solid": "^2.11.12",
"vite-plugin-static-copy": "^4.0.1" "vite-plugin-static-copy": "^4.1.0"
} }
} }

View file

@ -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<Result<ApiQueryOk, AppError>> {
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<Result<MediaTypeModel | undefined, AppError>> {
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<Result<MediaTypeModel | undefined, AppError>> {
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);
}
}

View file

@ -1,6 +1,8 @@
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { MediaType } from '../utils/MediaType'; 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 { export abstract class APIModel {
apiName!: string; apiName!: string;
@ -14,9 +16,9 @@ export abstract class APIModel {
* *
* @param title the title to query for * @param title the title to query for
*/ */
abstract searchByTitle(title: string): Promise<MediaTypeModel[]>; abstract searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>>;
abstract getById(id: string): Promise<MediaTypeModel>; abstract getById(id: string): Promise<Result<MediaTypeModel, AppError>>;
abstract getDisabledMediaTypes(): MediaType[]; abstract getDisabledMediaTypes(): MediaType[];

View file

@ -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<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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;
}
}

View file

@ -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<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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;
}
}

View file

@ -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<MediaTypeModel[]> {
// 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<paths>({ 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<MediaTypeModel> {
// 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<paths>({ 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;
// }
// }

View file

@ -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<Result<string, AppError>> {
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<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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 ?? [];
}
}

View file

@ -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<string, string>;
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<string, string>();
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<Result<MediaTypeModel[], AppError>> {
Logger.log(`MDB | api "${this.apiName}" queried by Title`);
const client = createClient<paths>({ 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<Result<MediaTypeModel, AppError>> {
Logger.log(`MDB | api "${this.apiName}" queried by ID`);
const client = createClient<paths>({ 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;
}
}

View file

@ -1,11 +1,16 @@
import createClient from 'openapi-fetch'; import createClient from 'openapi-fetch';
import type MediaDbPlugin from '../../main'; import { APIModel } from 'packages/obsidian/src/api/APIModel';
import { ComicMangaModel } from '../../models/ComicMangaModel'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel';
import { MediaType } from '../../utils/MediaType'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { isTruthy, obsidianFetch } from '../../utils/Utils'; import type { AppError } from 'packages/obsidian/src/utils/AppError';
import { APIModel } from '../APIModel'; import { AppErrorKind } from 'packages/obsidian/src/utils/AppError';
import type { paths } from '../schemas/MALAPI'; 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 { export class MALAPIManga extends APIModel {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -30,8 +35,8 @@ export class MALAPIManga extends APIModel {
this.typeMappings.set('novel', 'novel'); this.typeMappings.set('novel', 'novel');
} }
async searchByTitle(title: string): Promise<MediaTypeModel[]> { async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
console.log(`MDB | api "${this.apiName}" queried by Title`); Logger.log(`MDB | api "${this.apiName}" queried by Title`);
const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' }); const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' });
@ -47,7 +52,12 @@ export class MALAPIManga extends APIModel {
}); });
if (response.error !== undefined) { 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; const data = response.data?.data;
@ -93,11 +103,11 @@ export class MALAPIManga extends APIModel {
); );
} }
return ret; return ok(ret);
} }
async getById(id: string): Promise<MediaTypeModel> { async getById(id: string): Promise<Result<MediaTypeModel, AppError>> {
console.log(`MDB | api "${this.apiName}" queried by ID`); Logger.log(`MDB | api "${this.apiName}" queried by ID`);
const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' }); const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' });
@ -111,13 +121,23 @@ export class MALAPIManga extends APIModel {
}); });
if (response.error !== undefined) { 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; const result = response.data?.data;
if (!result) { 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(); const resType = result.type?.toLowerCase();
@ -125,35 +145,37 @@ export class MALAPIManga extends APIModel {
const year = result.published?.prop?.from?.year?.toString() ?? ''; const year = result.published?.prop?.from?.year?.toString() ?? '';
const new_id = result.mal_id?.toString(); const new_id = result.mal_id?.toString();
return new ComicMangaModel({ return ok(
subType: type, new ComicMangaModel({
title: result.title, subType: type,
plot: result.synopsis ?? undefined, title: result.title,
englishTitle: result.title_english ?? result.title, plot: result.synopsis ?? undefined,
alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), englishTitle: result.title_english ?? result.title,
year: year, alternateTitles: result.titles?.map(x => x.title).filter(isTruthy),
dataSource: this.apiName, year: year,
url: result.url, dataSource: this.apiName,
id: new_id, url: result.url,
id: new_id,
genres: result.genres?.map(x => x.name).filter(isTruthy), genres: result.genres?.map(x => x.name).filter(isTruthy),
authors: result.authors?.map(x => x.name).filter(isTruthy), authors: result.authors?.map(x => x.name).filter(isTruthy),
chapters: result.chapters, chapters: result.chapters,
volumes: result.volumes, volumes: result.volumes,
onlineRating: result.score, onlineRating: result.score,
image: result.images?.jpg?.image_url, image: result.images?.jpg?.image_url,
released: true, released: true,
publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat),
publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat),
status: result.status, status: result.status,
userData: { userData: {
read: false, read: false,
lastRead: '', lastRead: '',
personalRating: 0, personalRating: 0,
}, },
}); }),
);
} }
getDisabledMediaTypes(): MediaType[] { getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.MALAPIManga_disabledMediaTypes; return this.plugin.settings.MALAPIManga_disabledMediaTypes;

View file

@ -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<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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')}`;
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('movie', 'movie');
this.typeMappings.set('series', 'series');
this.typeMappings.set('game', 'game');
}
async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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;
}
}

View file

@ -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<Result<MediaTypeModel[], AppError>> {
Logger.log(`MDB | api "${this.apiName}" queried by Title`);
const client = createClient<paths>({ 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<Result<MediaTypeModel, AppError>> {
Logger.log(`MDB | api "${this.apiName}" queried by ID`);
const client = createClient<paths>({ 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;
}
}

View file

@ -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<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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 ?? [];
}
}

View file

@ -1,10 +1,15 @@
import { requestUrl } from 'obsidian'; import { requestUrl } from 'obsidian';
import type MediaDbPlugin from '../../main'; import { APIModel } from 'packages/obsidian/src/api/APIModel';
import { GameModel } from '../../models/GameModel'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { GameModel } from 'packages/obsidian/src/models/GameModel';
import { MediaType } from '../../utils/MediaType'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { imageUrlExists } from '../../utils/Utils'; import type { AppError } from 'packages/obsidian/src/utils/AppError';
import { APIModel } from '../APIModel'; 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 { interface SearchResponse {
appid: string; appid: string;
@ -143,16 +148,34 @@ export class SteamAPI extends APIModel {
this.typeMappings.set('game', 'game'); this.typeMappings.set('game', 'game');
} }
async searchByTitle(title: string): Promise<MediaTypeModel[]> { async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
console.log(`MDB | api "${this.apiName}" queried by Title`); Logger.log(`MDB | api "${this.apiName}" queried by Title`);
const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`;
const fetchData = await requestUrl({ const fetchDataResult = await fromPromise(
url: searchUrl, 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) { 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[]; 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<MediaTypeModel> { async getById(id: string): Promise<Result<MediaTypeModel, AppError>> {
console.log(`MDB | api "${this.apiName}" queried by ID`); Logger.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`;
const fetchData = await requestUrl({ const fetchDataResult = await fromPromise(
url: searchUrl, 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) { 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); // console.debug(await fetchData.json);
@ -200,14 +241,27 @@ export class SteamAPI extends APIModel {
} }
} }
if (!result) { 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); // console.debug(result);
// Check if a poster version of the image exists, else use the header image // 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 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; let finalimageurl;
if (exists) { if (exists) {
finalimageurl = imageUrl; finalimageurl = imageUrl;
@ -215,29 +269,31 @@ export class SteamAPI extends APIModel {
finalimageurl = result.header_image ?? ''; finalimageurl = result.header_image ?? '';
} }
return new GameModel({ return ok(
type: MediaType.Game, new GameModel({
title: result.name, type: MediaType.Game,
englishTitle: result.name, title: result.name,
year: new Date(result.release_date.date).getFullYear().toString(), englishTitle: result.name,
dataSource: this.apiName, year: new Date(result.release_date.date).getFullYear().toString(),
url: `https://store.steampowered.com/app/${result.steam_appid}`, dataSource: this.apiName,
id: result.steam_appid.toString(), url: `https://store.steampowered.com/app/${result.steam_appid}`,
id: result.steam_appid.toString(),
developers: result.developers, developers: result.developers,
publishers: result.publishers, publishers: result.publishers,
genres: result.genres?.map(x => x.description), genres: result.genres?.map(x => x.description),
onlineRating: result.metacritic?.score, onlineRating: result.metacritic?.score,
image: finalimageurl, image: finalimageurl,
released: !result.release_date?.coming_soon, released: !result.release_date?.coming_soon,
releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat),
userData: { userData: {
played: false, played: false,
personalRating: 0, personalRating: 0,
}, },
}); }),
);
} }
getDisabledMediaTypes(): MediaType[] { getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.SteamAPI_disabledMediaTypes; return this.plugin.settings.SteamAPI_disabledMediaTypes;

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('movie', 'movie');
}
async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
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<paths>({ 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<Result<MediaTypeModel, AppError>> {
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<paths>({ 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;
}
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('tv', 'season');
}
async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
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<paths>({ 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<Result<SeasonModel[], AppError>> {
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<paths>({ 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<Result<MediaTypeModel, AppError>> {
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 "<series_id>/season/<season_number>".`,
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<paths>({ 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 [];
}
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('tv', 'series');
}
async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
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<paths>({ 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<Result<MediaTypeModel, AppError>> {
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<paths>({ 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;
}
}

View file

@ -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<Result<unknown, AppError>> {
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<Result<VNJSONResponse, AppError>> {
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<Result<ReleaseJSONResponse, AppError>> {
const result = await this.postQuery('/release', body);
return result.ok ? ok(result.value as ReleaseJSONResponse) : err(result.error);
}
async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
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<Result<MediaTypeModel, AppError>> {
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;
}
}

View file

@ -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<string, WikipediaPage>;
};
}
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<Result<MediaTypeModel[], AppError>> {
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<unknown> };
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<Result<MediaTypeModel, AppError>> {
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<unknown> };
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;
}
}

View file

@ -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<void> {
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<void> {
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<void> {
this.mediaTypeManager.updateTemplates(this.settings);
this.mediaTypeManager.updateFolders(this.settings);
this.dateFormatter.setFormat(this.settings.customDateFormat);
await this.saveData(this.settings);
}
}

View file

@ -1,8 +1,8 @@
import type { ButtonComponent } from 'obsidian'; import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbAdvancedSearchModal extends Modal { export class MediaDbAdvancedSearchModal extends Modal {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -71,7 +71,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
const placeholder = 'Search by title'; const placeholder = 'Search by title';
const searchComponent = new TextComponent(contentEl); const searchComponent = new TextComponent(contentEl);
searchComponent.inputEl.style.width = '100%'; searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder); searchComponent.setPlaceholder(placeholder);
searchComponent.setValue(this.query); searchComponent.setValue(this.query);
searchComponent.onChange(value => (this.query = value)); searchComponent.onChange(value => (this.query = value));
@ -85,13 +85,13 @@ export class MediaDbAdvancedSearchModal extends Modal {
// const apiToggleComponents: Component[] = []; // const apiToggleComponents: Component[] = [];
for (const api of this.plugin.apiManager.apis) { 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' }); const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
apiToggleTextWrapper.createEl('span', { text: api.apiName, cls: 'media-db-plugin-list-text' }); apiToggleTextWrapper.createSpan({ text: api.apiName, cls: 'media-db-plugin-list-text' });
apiToggleTextWrapper.createEl('small', { text: api.apiDescription, 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); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
apiToggleComponent.setTooltip(api.apiName); apiToggleComponent.setTooltip(api.apiName);

View file

@ -1,8 +1,8 @@
import type { ButtonComponent } from 'obsidian'; import type { ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian'; import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type { APIModel } from '../api/APIModel'; import type { APIModel } from 'packages/obsidian/src/api/APIModel';
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import { BulkImportLookupMethod } from '../utils/BulkImportHelper'; import { BulkImportLookupMethod } from 'packages/obsidian/src/utils/BulkImportHelper';
export class MediaDbBulkImportModal extends Modal { export class MediaDbBulkImportModal extends Modal {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -47,14 +47,14 @@ export class MediaDbBulkImportModal extends Modal {
contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' }); contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' });
const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); const appendContentToggleElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
appendContentToggleTextWrapper.createEl('span', { appendContentToggleTextWrapper.createSpan({
text: 'If this is enabled, the plugin will override metadata fields with the same name.', text: 'If this is enabled, the plugin will override metadata fields with the same name.',
cls: 'media-db-plugin-list-text', 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); const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper);
appendContentToggle.setValue(false); appendContentToggle.setValue(false);
@ -81,9 +81,9 @@ export class MediaDbBulkImportModal extends Modal {
contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); const fieldNameWrapperEl = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); const fieldNameLabelWrapperEl = fieldNameWrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' }); fieldNameLabelWrapperEl.createSpan({ text: 'Using the property named', cls: 'media-db-plugin-list-text' });
const fieldNameComponent = new TextComponent(fieldNameWrapperEl); const fieldNameComponent = new TextComponent(fieldNameWrapperEl);
fieldNameComponent.setPlaceholder('title / id'); 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 { 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 wrapperEl = parentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); const labelWrapperEl = wrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' }); labelWrapperEl.createSpan({ text: label, cls: 'media-db-plugin-list-text' });
const dropDownComponent = new DropdownComponent(wrapperEl); const dropDownComponent = new DropdownComponent(wrapperEl);
dropDownComponent.onChange(onChange); dropDownComponent.onChange(onChange);

View file

@ -1,8 +1,8 @@
import type { ButtonComponent } from 'obsidian'; import type { ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian';
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; import type { IdSearchModalData, IdSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbIdSearchModal extends Modal { export class MediaDbIdSearchModal extends Modal {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -69,7 +69,7 @@ export class MediaDbIdSearchModal extends Modal {
const placeholder = 'Search by id'; const placeholder = 'Search by id';
const searchComponent = new TextComponent(contentEl); const searchComponent = new TextComponent(contentEl);
searchComponent.inputEl.style.width = '100%'; searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder); searchComponent.setPlaceholder(placeholder);
searchComponent.onChange(value => (this.query = value)); searchComponent.onChange(value => (this.query = value));
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
@ -79,9 +79,9 @@ export class MediaDbIdSearchModal extends Modal {
contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); const apiSelectorWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); const apiSelectorTExtWrapper = apiSelectorWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
apiSelectorTExtWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' }); apiSelectorTExtWrapper.createSpan({ text: 'API to search', cls: 'media-db-plugin-list-text' });
const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper); const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper);
apiSelectorComponent.onChange((value: string) => { apiSelectorComponent.onChange((value: string) => {

View file

@ -1,8 +1,9 @@
import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian';
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper'; import { Logger } from 'packages/obsidian/src/utils/Logger';
import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; 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 { export class MediaDbPreviewModal extends Modal {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -48,14 +49,14 @@ export class MediaDbPreviewModal extends Modal {
previewWrapper.createEl('h3', { text: result.englishTitle }); previewWrapper.createEl('h3', { text: result.englishTitle });
const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' }); 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}\`\`\``; fileContent = `\`\`\`yaml\n${fileContent}\`\`\``;
try { try {
// TODO: fix this not rendering the frontmatter any more // TODO: fix this not rendering the frontmatter any more
await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent); await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent);
} catch (e) { } catch (e) {
console.warn(`mdb | error during rendering of preview`, e); Logger.warn(`mdb | error during rendering of preview`, e);
} }
} }

View file

@ -1,11 +1,11 @@
import type { ButtonComponent } from 'obsidian'; import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaType } from '../utils/MediaType'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; import type { SearchModalData, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; import { SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
import { unCamelCase } from '../utils/Utils'; import { unCamelCase } from 'packages/obsidian/src/utils/Utils';
export class MediaDbSearchModal extends Modal { export class MediaDbSearchModal extends Modal {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -76,7 +76,7 @@ export class MediaDbSearchModal extends Modal {
const searchComponent = new TextComponent(contentEl); const searchComponent = new TextComponent(contentEl);
let currentToggle: ToggleComponent | undefined = undefined; let currentToggle: ToggleComponent | undefined = undefined;
searchComponent.inputEl.style.width = '100%'; searchComponent.inputEl.addClass('media-db-plugin-search-input');
searchComponent.setPlaceholder(placeholder); searchComponent.setPlaceholder(placeholder);
searchComponent.setValue(this.query); searchComponent.setValue(this.query);
searchComponent.onChange(value => (this.query = value)); searchComponent.onChange(value => (this.query = value));
@ -89,12 +89,12 @@ export class MediaDbSearchModal extends Modal {
contentEl.createEl('h3', { text: 'APIs to search' }); contentEl.createEl('h3', { text: 'APIs to search' });
for (const mediaType of MEDIA_TYPES) { 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' }); const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
apiToggleTextWrapper.createEl('span', { text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' }); 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); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
apiToggleComponent.setTooltip(unCamelCase(mediaType)); apiToggleComponent.setTooltip(unCamelCase(mediaType));

View file

@ -1,8 +1,8 @@
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from '../models/MediaTypeModel'; import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper'; import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import { SelectModal } from './SelectModal'; import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper';
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> { export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -41,7 +41,7 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
// Renders each suggestion item. // Renders each suggestion item.
renderElement(item: MediaTypeModel, el: HTMLElement): void { 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.getSummary()}\n` });
el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` });
} }

View file

@ -1,5 +1,5 @@
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import { SelectModal } from './SelectModal'; import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
export interface SeasonSelectModalElement { export interface SeasonSelectModalElement {
season_number: number; season_number: number;
@ -24,7 +24,7 @@ export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalEleme
} }
renderElement(season: SeasonSelectModalElement, el: HTMLElement): void { renderElement(season: SeasonSelectModalElement, el: HTMLElement): void {
el.createEl('div', { text: `${season.name}` }); el.createDiv({ text: `${season.name}` });
if (season.air_date) { if (season.air_date) {
el.createEl('small', { text: `Air date: ${season.air_date}` }); el.createEl('small', { text: `Air date: ${season.air_date}` });
} }

View file

@ -1,7 +1,7 @@
import type { App, ButtonComponent } from 'obsidian'; import type { App, ButtonComponent } from 'obsidian';
import { Modal, Setting } from 'obsidian'; import { Modal, Setting } from 'obsidian';
import { mod } from '../utils/Utils'; import { SelectModalElement } from 'packages/obsidian/src/modals/SelectModalElement';
import { SelectModalElement } from './SelectModalElement'; import { mod } from 'packages/obsidian/src/utils/Utils';
export abstract class SelectModal<T> extends Modal { export abstract class SelectModal<T> extends Modal {
allowMultiSelect: boolean; allowMultiSelect: boolean;
@ -48,7 +48,7 @@ export abstract class SelectModal<T> extends Modal {
this.activateHighlighted(); this.activateHighlighted();
}); });
this.scope.register([], ' ', evt => { this.scope.register([], ' ', evt => {
if (this.elementWrapper && this.elementWrapper === document.activeElement) { if (this.elementWrapper && this.elementWrapper === activeDocument.activeElement) {
this.activateHighlighted(); this.activateHighlighted();
evt.preventDefault(); evt.preventDefault();
} }

View file

@ -1,4 +1,4 @@
import type { SelectModal } from './SelectModal'; import type { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
export class SelectModalElement<T> { export class SelectModalElement<T> {
selectModal: SelectModal<T>; selectModal: SelectModal<T>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type BoardGameData = ModelToData<BoardGameModel>; export type BoardGameData = ModelToData<BoardGameModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type BookData = ModelToData<BookModel>; export type BookData = ModelToData<BookModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type ComicMangaData = ModelToData<ComicMangaModel>; export type ComicMangaData = ModelToData<ComicMangaModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type GameData = ModelToData<GameModel>; export type GameData = ModelToData<GameModel>;

View file

@ -1,4 +1,4 @@
import type { MediaType } from '../utils/MediaType'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
export abstract class MediaTypeModel { export abstract class MediaTypeModel {
type: string; type: string;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type MovieData = ModelToData<MovieModel>; export type MovieData = ModelToData<MovieModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type MusicReleaseData = ModelToData<MusicReleaseModel>; export type MusicReleaseData = ModelToData<MusicReleaseModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type SeasonData = ModelToData<SeasonModel>; export type SeasonData = ModelToData<SeasonModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type SeriesData = ModelToData<SeriesModel>; export type SeriesData = ModelToData<SeriesModel>;

View file

@ -1,7 +1,7 @@
import { MediaType } from '../utils/MediaType'; import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { ModelToData } from '../utils/Utils'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { mediaDbTag, migrateObject } from '../utils/Utils'; import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { MediaTypeModel } from './MediaTypeModel'; import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
export type WikiData = ModelToData<WikiModel>; export type WikiData = ModelToData<WikiModel>;

View file

@ -1,7 +1,8 @@
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaType } from '../utils/MediaType'; import { PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping';
import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { Logger } from 'packages/obsidian/src/utils/Logger';
import { PropertyMappingOption } from './PropertyMapping'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
export class PropertyMapper { export class PropertyMapper {
plugin: MediaDbPlugin; plugin: MediaDbPlugin;
@ -75,7 +76,7 @@ export class PropertyMapper {
if (obj.type === 'manga') { if (obj.type === 'manga') {
obj.type = 'comicManga'; 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)) { if (!MEDIA_TYPES.includes(obj.type as MediaType)) {
return obj; return obj;

View file

@ -1,5 +1,6 @@
import type { MediaType } from '../utils/MediaType'; import { Logger } from 'packages/obsidian/src/utils/Logger';
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from 'packages/obsidian/src/utils/Utils';
// Plain object interfaces for serialization // Plain object interfaces for serialization
export interface PropertyMappingData { export interface PropertyMappingData {
@ -33,7 +34,7 @@ export class PropertyMappingModel {
} }
validate(): { res: boolean; err?: Error } { 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 // check properties
for (const property of this.properties) { for (const property of this.properties) {

View file

@ -1,16 +1,51 @@
import type { App } from 'obsidian'; import type { App } from 'obsidian';
import { Notice, PluginSettingTab, SecretComponent, SettingGroup } 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 { render } from 'solid-js/web';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel'; function createDateFormatDescription(preview: string): DocumentFragment {
import { MediaType } from '../utils/MediaType'; return createFragment(frag => {
import { MEDIA_TYPES } from '../utils/MediaTypeManager'; const container = frag.createDiv();
import { fragWithHTML, unCamelCase } from '../utils/Utils'; container.appendText('Your custom date format. Use ');
import type { PropertyMappingModelData } from './PropertyMapping'; container.createEl('em', { text: "'YYYY-MM-DD'" });
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; container.appendText(' for example.');
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; container.createEl('br');
import { FileSuggest } from './suggesters/FileSuggest'; container.appendText('For more syntax, refer to ');
import { FolderSuggest } from './suggesters/FolderSuggest'; 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 // MARK: Settings
export interface MediaDbPluginSettings { export interface MediaDbPluginSettings {
@ -435,22 +470,14 @@ export class MediaDbSettingTab extends PluginSettingTab {
setting => setting =>
void setting void setting
.setName('Date format') .setName('Date format')
.setDesc( .setDesc(createDateFormatDescription(this.plugin.dateFormatter.getPreview()))
fragWithHTML(
"Your custom date format. Use <em>'YYYY-MM-DD'</em> for example.<br>" +
"For more syntax, refer to <a href='https://momentjs.com/docs/#/displaying/format/'>format reference</a>.<br>" +
"Your current syntax looks like this: <b><a id='media-db-dateformat-preview' style='pointer-events: none; cursor: default; text-decoration: none;'>" +
this.plugin.dateFormatter.getPreview() +
'</a></b>',
),
)
.addText(cb => { .addText(cb => {
cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat)
.setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat)
.onChange(data => { .onChange(data => {
const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat;
this.plugin.settings.customDateFormat = newDateFormat; this.plugin.settings.customDateFormat = newDateFormat;
const previewEl = document.getElementById('media-db-dateformat-preview'); const previewEl = activeDocument.getElementById('media-db-dateformat-preview');
if (previewEl) { if (previewEl) {
previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview
} }
@ -809,15 +836,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
const mappingGroup = new SettingGroup(containerEl); const mappingGroup = new SettingGroup(containerEl);
mappingGroup.setHeading('Property mappings'); mappingGroup.setHeading('Property mappings');
mappingGroup.addSetting(setting => { mappingGroup.addSetting(setting => {
setting setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription());
.setName('Property mappings explanation')
.setDesc(
fragWithHTML(
'<p>Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.</p>' +
'<p>You can choose to keep the original name, rename the property, or remove it entirely.</p>' +
'<p><strong>Remember to save your changes using the save button for each individual category.</strong></p>',
),
);
render( render(
() => () =>

View file

@ -64,6 +64,10 @@ small.media-db-plugin-list-text {
margin-bottom: 10px; margin-bottom: 10px;
} }
.media-db-plugin-search-input {
width: 100%;
}
.media-db-plugin-button:focus { .media-db-plugin-button:focus {
/*outline: 1px solid white;*/ /*outline: 1px solid white;*/
} }

View file

@ -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<string, unknown>;
}
export const appError = (error: AppError): AppError => error;
export const toAppError = (cause: unknown, fallback: Omit<AppError, 'cause'>): AppError => {
const message = cause instanceof Error ? cause.message : String(cause);
return { ...fallback, message: fallback.message || message, cause };
};

View file

@ -1,10 +1,10 @@
import type { TFolder } from 'obsidian'; import type { TFolder } from 'obsidian';
import { TFile } from 'obsidian'; import { TFile } from 'obsidian';
import type MediaDbPlugin from '../main'; import type MediaDbPlugin from 'packages/obsidian/src/main';
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from '../modals/MediaDbBulkImportModal'; import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal';
import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { ModalResultCode } from './ModalHelper'; import { OutcomeStatus } from 'packages/obsidian/src/utils/result';
import { dateTimeToString, markdownTable } from './Utils'; import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils';
export enum BulkImportLookupMethod { export enum BulkImportLookupMethod {
ID = 'id', ID = 'id',
@ -50,7 +50,7 @@ export class BulkImportHelper {
continue; continue;
} }
const metadata = this.plugin.getMetadataFromFileCache(file); const metadata = this.plugin.fileHelper.getMetadataFromFileCache(file);
const lookupValue = metadata[fieldName]; const lookupValue = metadata[fieldName];
if (!lookupValue || typeof lookupValue !== 'string') { if (!lookupValue || typeof lookupValue !== 'string') {
@ -81,60 +81,54 @@ export class BulkImportHelper {
} }
private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> { private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> {
try { const modelResult = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI);
const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); if (!modelResult.ok) {
if (model) { return { filePath: file.path, error: modelResult.error.userMessage ?? modelResult.error.message };
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}` };
} }
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<BulkImportError | undefined> { private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> {
let results: MediaTypeModel[] = []; const resultsResult = await this.plugin.apiManager.query(lookupValue, [selectedAPI]);
try { if (!resultsResult.ok) {
results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); return { filePath: file.path, error: resultsResult.error.userMessage ?? resultsResult.error.message };
} catch (e) {
return { filePath: file.path, error: `${e}` };
} }
const results: MediaTypeModel[] = resultsResult.value.items;
if (!results || results.length === 0) { if (!results || results.length === 0) {
return { filePath: file.path, error: `no search results` }; 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, elements: results,
skipButton: true, skipButton: true,
modalTitle: `Results for '${lookupValue}'`, modalTitle: `Results for '${lookupValue}'`,
}); });
if (selectModalResult.code === ModalResultCode.ERROR) { if (selectModalResult.status === OutcomeStatus.Error) {
selectModal.close(); return { filePath: file.path, error: selectModalResult.error.userMessage ?? selectModalResult.error.message };
return { filePath: file.path, error: selectModalResult.error.message };
} }
if (selectModalResult.code === ModalResultCode.CLOSE) { if (selectModalResult.status === OutcomeStatus.Cancelled) {
selectModal.close();
return { filePath: file.path, error: 'user canceled', canceled: true }; return { filePath: file.path, error: 'user canceled', canceled: true };
} }
if (selectModalResult.code === ModalResultCode.SKIP) { if (selectModalResult.status === OutcomeStatus.Skipped) {
selectModal.close();
return { filePath: file.path, error: 'user skipped' }; return { filePath: file.path, error: 'user skipped' };
} }
if (selectModalResult.data.selected.length === 0) { if (selectModalResult.data.selected.length === 0) {
selectModal.close();
return { filePath: file.path, error: `no search results selected` }; return { filePath: file.path, error: `no search results selected` };
} }
const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected); const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected);
await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined); await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
selectModal.close();
return undefined; return undefined;
} }

View file

@ -1,8 +1,6 @@
import type momentType from 'moment'; import { moment } from 'obsidian';
import type { Moment } from 'moment';
import { moment as obsidianMoment } from 'obsidian';
const moment: typeof momentType = obsidianMoment as unknown as typeof momentType; const momentFn = 'default' in moment ? moment.default : moment;
export class DateFormatter { export class DateFormatter {
private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ'; private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
@ -21,7 +19,7 @@ export class DateFormatter {
} }
getPreview(format?: string): string { getPreview(format?: string): string {
const today = moment(); const today = momentFn();
format ??= this.toFormat; format ??= this.toFormat;
@ -44,32 +42,32 @@ export class DateFormatter {
return null; return null;
} }
let date: Moment; let date: moment.Moment;
if (!dateFormat) { if (!dateFormat) {
date = this.parseWithoutFormat(dateString); date = this.parseWithoutFormat(dateString);
} else { } else {
date = moment(dateString, dateFormat, locale); date = momentFn(dateString, dateFormat, locale);
} }
// format date (if it is valid) // format date (if it is valid)
return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null; 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 // reading date formats other than C2822 or ISO with moment is deprecated
// see https://momentjs.com/docs/#/parsing/string/ // see https://momentjs.com/docs/#/parsing/string/
if (this.hasMomentFormat(dateString)) { if (this.hasMomentFormat(dateString)) {
// expect C2822 or ISO format // expect C2822 or ISO format
return moment(dateString); return momentFn(dateString);
} }
// fall back to native Date parsing for unknown formats // fall back to native Date parsing for unknown formats
return moment(new Date(dateString)); return momentFn(new Date(dateString));
} }
private hasMomentFormat(dateString: string): boolean { 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(); return date.isValid();
} }
} }

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<MediaTypeModel[]> {
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<boolean> {
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<SeasonSelectModalElement[] | undefined> {
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<void> {
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]);
}
}),
);
}
}

View file

@ -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<string, unknown>;
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<Result<void, AppError>> {
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<Result<void, AppError>> {
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<void> {
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<string> {
let template = await this.plugin.mediaTypeManager.getTemplate(mediaTypeModel, this.plugin.app);
let fileMetadata: Record<string, unknown>;
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<Result<TFile, AppError>> {
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<boolean>(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<Result<void, AppError>> {
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<T>(operation: () => Promise<T> | T, fallback: Omit<AppError, 'cause'>): Promise<Result<T, AppError>> {
return await Promise.resolve()
.then(operation)
.then(
value => ok(value),
cause => err(toAppError(cause, fallback)),
);
}
}

View file

@ -1,19 +1,20 @@
import type { App, TFile } from 'obsidian'; import type { App } from 'obsidian';
import { TFile } from 'obsidian';
import { TFolder } from 'obsidian'; import { TFolder } from 'obsidian';
import { BoardGameModel } from '../models/BoardGameModel'; import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel';
import { BookModel } from '../models/BookModel'; import { BookModel } from 'packages/obsidian/src/models/BookModel';
import { ComicMangaModel } from '../models/ComicMangaModel'; import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel';
import { GameModel } from '../models/GameModel'; import { GameModel } from 'packages/obsidian/src/models/GameModel';
import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { MovieModel } from '../models/MovieModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel';
import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel';
import { SeasonModel } from '../models/SeasonModel'; import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
import { SeriesModel } from '../models/SeriesModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel';
import { WikiModel } from '../models/WikiModel'; import { WikiModel } from 'packages/obsidian/src/models/WikiModel';
import type { MediaDbPluginSettings } from '../settings/Settings'; import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings';
import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; import { ILLEGAL_FILENAME_CHARACTERS } from 'packages/obsidian/src/utils/IllegalFilenameCharactersList';
import { MediaType } from './MediaType'; import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { replaceTags } from './Utils'; import { replaceTags } from 'packages/obsidian/src/utils/Utils';
// All media types in alphabetical order // All media types in alphabetical order
export const MEDIA_TYPES: MediaType[] = [ export const MEDIA_TYPES: MediaType[] = [
@ -110,16 +111,16 @@ export class MediaTypeManager {
} }
} }
const template = await app.vault.cachedRead(templateFile as TFile); if (!(templateFile instanceof TFile)) {
// console.log(template); return '';
}
const template = await app.vault.cachedRead(templateFile);
return replaceTags(template, mediaTypeModel); return replaceTags(template, mediaTypeModel);
} }
async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise<TFolder> { async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise<TFolder> {
let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()) ?? '/';
folderPath ??= `/`;
// console.log(folderPath);
if (!(await app.vault.adapter.exists(folderPath))) { if (!(await app.vault.adapter.exists(folderPath))) {
await app.vault.createFolder(folderPath); await app.vault.createFolder(folderPath);

View file

@ -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<T, TModal> {
modalResult: Outcome<T, AppError>;
modal: TModal;
}
export class ModalHelper {
plugin: MediaDbPlugin;
constructor(plugin: MediaDbPlugin) {
this.plugin = plugin;
}
private async openModalCore<TData, TModal extends { open(): void; close(): void }>(
createModal: () => TModal,
wireHandlers: (modal: TModal, resolve: (result: Outcome<TData, AppError>) => void) => void,
): Promise<ModalCoreResult<TData, TModal>> {
const modal = createModal();
const modalResult = await new Promise<Outcome<TData, AppError>>(resolve => {
wireHandlers(modal, resolve);
modal.open();
});
return { modalResult, modal };
}
private async resolveOutcome<T>(outcomePromise: Promise<Outcome<T, AppError>>): Promise<T | undefined> {
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<Outcome<SearchModalData, AppError>> {
const { modalResult, modal } = await this.openModalCore<SearchModalData, MediaDbSearchModal>(
() => 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<SearchModalData | undefined> {
return await this.resolveOutcome(this.createSearchModalOutcome(searchModalOptions));
}
async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise<Outcome<AdvancedSearchModalData, AppError>> {
const { modalResult, modal } = await this.openModalCore<AdvancedSearchModalData, MediaDbAdvancedSearchModal>(
() => 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<AdvancedSearchModalData | undefined> {
return await this.resolveOutcome(this.createAdvancedSearchModalOutcome(advancedSearchModalOptions));
}
async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise<Outcome<IdSearchModalData, AppError>> {
const { modalResult, modal } = await this.openModalCore<IdSearchModalData, MediaDbIdSearchModal>(
() => 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<IdSearchModalData | undefined> {
return await this.resolveOutcome(this.createIdSearchModalOutcome(idSearchModalOptions));
}
async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise<Outcome<SelectModalData, AppError>> {
const { modalResult, modal } = await this.openModalCore<SelectModalData, MediaDbSearchResultModal>(
() => 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<SelectModalData | undefined> {
return await this.resolveOutcome(this.createSelectModalOutcome(selectModalOptions));
}
async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise<Outcome<PreviewModalData, AppError>> {
const { modalResult, modal } = await this.openModalCore<PreviewModalData, MediaDbPreviewModal>(
() => 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<PreviewModalData | undefined> {
return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions));
}
}

View file

@ -1,13 +1,12 @@
import { iso6392 } from 'iso-639-2'; import { iso6392 } from 'iso-639-2';
import type { TFile, TFolder, App } from 'obsidian'; import type { TFile, TFolder, App } from 'obsidian';
import { requestUrl } 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 pluginName: string = 'obsidian-media-db-plugin';
export const contactEmail: string = 'm.projects.code@gmail.com'; export const contactEmail: string = 'm.projects.code@gmail.com';
export const mediaDbTag: string = 'mediaDB'; export const mediaDbTag: string = 'mediaDB';
export const mediaDbVersion: string = '0.5.2'; export const mediaDbVersion: string = '0.8.0';
export const debug: boolean = true;
export function wrapAround(value: number, size: number): number { export function wrapAround(value: number, size: number): number {
if (size <= 0) { if (size <= 0) {
@ -153,10 +152,6 @@ export function markdownTable(content: string[][]): string {
return table; return table;
} }
export function fragWithHTML(html: string): DocumentFragment {
return createFragment(frag => (frag.createDiv().innerHTML = html));
}
export function dateToString(date: Date): string { export function dateToString(date: Date): string {
return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`;
} }
@ -240,7 +235,7 @@ function getTemplaterPlugin(app: App): TemplaterPlugin | null {
return null; return null;
} }
return candidate as TemplaterPlugin; return candidate;
} }
export function hasTemplaterPlugin(app: App): boolean { export function hasTemplaterPlugin(app: App): boolean {

View file

@ -0,0 +1,52 @@
export interface Ok<T> {
ok: true;
value: T;
}
export interface Err<E> {
ok: false;
error: E;
}
export type Result<T, E> = Ok<T> | Err<E>;
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
export const isOk = <T, E>(result: Result<T, E>): result is Ok<T> => result.ok;
export const isErr = <T, E>(result: Result<T, E>): result is Err<E> => !result.ok;
export const mapResult = <T, E, U>(result: Result<T, E>, mapper: (value: T) => U): Result<U, E> => (result.ok ? ok(mapper(result.value)) : result);
export const mapError = <T, E, F>(result: Result<T, E>, mapper: (error: E) => F): Result<T, F> => (result.ok ? result : err(mapper(result.error)));
export const andThen = <T, E, U>(result: Result<T, E>, binder: (value: T) => Result<U, E>): Result<U, E> => (result.ok ? binder(result.value) : result);
export const tapError = <T, E>(result: Result<T, E>, sideEffect: (error: E) => void): Result<T, E> => {
if (!result.ok) sideEffect(result.error);
return result;
};
export const fromPromise = async <T, E>(promise: Promise<T>, onError: (cause: unknown) => E): Promise<Result<T, E>> => {
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<T, E> =
| { status: OutcomeStatus.Ok; data: T }
| { status: OutcomeStatus.Cancelled }
| { status: OutcomeStatus.Skipped }
| { status: OutcomeStatus.Error; error: E };
export const cancelled = (): Outcome<never, never> => ({ status: OutcomeStatus.Cancelled });
export const skipped = (): Outcome<never, never> => ({ status: OutcomeStatus.Skipped });
export const success = <T>(data: T): Outcome<T, never> => ({ status: OutcomeStatus.Ok, data });
export const failure = <E>(error: E): Outcome<never, E> => ({ status: OutcomeStatus.Error, error });

View file

@ -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

View file

@ -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<MediaTypeModel[]> {
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<MediaTypeModel | undefined> {
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<MediaTypeModel | undefined> {
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);
}
}

View file

@ -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<MediaTypeModel[]> {
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<MediaTypeModel> {
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;
}
}

View file

@ -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<MediaTypeModel[]> {
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<MediaTypeModel> {
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;
}
}

View file

@ -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<MediaTypeModel[]> {
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<paths>({ 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<MediaTypeModel> {
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<paths>({ 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;
}
}

View file

@ -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<string> {
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<MediaTypeModel[]> {
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<MediaTypeModel> {
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 ?? [];
}
}

View file

@ -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<string, string>;
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<string, string>();
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<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const client = createClient<paths>({ 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<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const client = createClient<paths>({ 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;
}
}

View file

@ -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<MediaTypeModel[]> {
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<MediaTypeModel> {
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;
}
}

View file

@ -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<MediaTypeModel[]> {
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<MediaTypeModel> {
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')}`;
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('movie', 'movie');
this.typeMappings.set('series', 'series');
this.typeMappings.set('game', 'game');
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
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<MediaTypeModel> {
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;
}
}

View file

@ -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<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const client = createClient<paths>({ 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<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const client = createClient<paths>({ 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;
}
}

View file

@ -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<MediaTypeModel[]> {
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<MediaTypeModel> {
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 ?? [];
}
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('movie', 'movie');
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
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<paths>({ 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<MediaTypeModel> {
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<paths>({ 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;
}
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('tv', 'season');
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
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<paths>({ 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<SeasonModel[]> {
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<paths>({ 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<MediaTypeModel> {
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 "<series_id>/season/<season_number>".`);
}
const tvId = Number.parseInt(m[1], 10);
const seasonNumber = Number.parseInt(m[2], 10);
const client = createClient<paths>({ 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 [];
}
}

View file

@ -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<string, string>;
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<string, string>();
this.typeMappings.set('tv', 'series');
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
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<paths>({ 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<MediaTypeModel> {
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<paths>({ 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;
}
}

View file

@ -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<unknown> {
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<VNJSONResponse> {
return this.postQuery('/vn', body) as Promise<VNJSONResponse>;
}
/**
* 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<ReleaseJSONResponse> {
return this.postQuery('/release', body) as Promise<ReleaseJSONResponse>;
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
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<MediaTypeModel> {
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;
}
}

Some files were not shown because too many files have changed in this diff Show more