add obsidian eslint plugin, solve major issues
This commit is contained in:
parent
84214a1c3c
commit
498e2611ae
105 changed files with 5702 additions and 5308 deletions
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
|
|
@ -13,9 +13,14 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Determine prerelease status
|
||||
id: status
|
||||
|
|
@ -27,23 +32,32 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
run: |
|
||||
bun install
|
||||
bun install --frozen-lockfile
|
||||
bun run build
|
||||
mkdir ${{ env.PLUGIN_NAME }}
|
||||
cp dist/main.js dist/manifest.json dist/styles.css ${{ env.PLUGIN_NAME }}
|
||||
zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }}
|
||||
ls
|
||||
|
||||
- name: Attest build provenance
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: |
|
||||
dist/main.js
|
||||
dist/manifest.json
|
||||
dist/styles.css
|
||||
${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip
|
||||
|
||||
- name: Release
|
||||
id: release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
prerelease: ${{ steps.status.outputs.prerelease }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -117,7 +117,7 @@ Now you select the result you want, and the plugin will cast its magic, creating
|
|||
### Currently supported APIs:
|
||||
|
||||
| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
|
||||
| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes |
|
||||
| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movies, series, and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No |
|
||||
| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Read Access Token` (**not** `API Key`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes |
|
||||
|
|
@ -125,8 +125,6 @@ Now you select the result you want, and the plugin will cast its magic, creating
|
|||
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
|
||||
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all Steam games. | games | No | 10000 per day | No |
|
||||
| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID, so there shouldn't be a rate limit. | No |
|
||||
| [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/). NOTE: As of September 2024 the API key is no longer free so consider using Giant Bomb or steam instead | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No |
|
||||
| [Giant Bomb](https://www.giantbomb.com) | The Giant Bomb API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.giantbomb.com/login-signup/) | API requests are limited to 200 requests per resource, per hour. In addition, they implement velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No |
|
||||
| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No |
|
||||
| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes |
|
||||
| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | No |
|
||||
|
|
@ -137,6 +135,8 @@ Now you select the result you want, and the plugin will cast its magic, creating
|
|||
- sometimes the api is very slow; this is normal
|
||||
- you need to use the title the anime has on [My Anime List](https://myanimelist.net), which is in most cases the Japanese title
|
||||
- e.g. instead of "Demon Slayer" you have to search "Kimetsu no Yaiba"
|
||||
- Support for the [Moby Games](https://www.mobygames.com) API has been removed from the plugins as the API is no longer free to use.
|
||||
- Support for the [Giant Bomb](https://www.giantbomb.com) API has been removed from the plugin temporarily as their API is [currently non functional](https://giantbomb.com/api).
|
||||
|
||||
#### Search by ID
|
||||
|
||||
|
|
@ -174,12 +174,6 @@ Now you select the result you want, and the plugin will cast its magic, creating
|
|||
- This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) `
|
||||
- For a specific edition of "Fantastic Mr. Fox" the "/books/" URL looks like this `https://openlibrary.org/books/OL3567303M/` so the ID is `/books/OL3567303M`
|
||||
- This URL is located in the editions section`
|
||||
- [Moby Games](https://www.mobygames.com)
|
||||
- you can find this ID in the URL
|
||||
- e.g. for "Bioshock 2" the URL looks like this `https://www.mobygames.com/game/45089/bioshock-2/` so the ID is `45089`
|
||||
- [Giant Bomb](https://www.giantbomb.com)
|
||||
- you can find this ID in the URL
|
||||
- e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887`
|
||||
- [Comic Vine](https://www.comicvine.gamespot.com)
|
||||
- you can find this ID in the URL
|
||||
- e.g. for "Boule & Bill" the URL looks like this `https://comicvine.gamespot.com/boule-bill/4050-70187/` so the ID is `4050-70187`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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/>.
|
||||
*/
|
||||
`;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"devBranch": "master",
|
||||
"releaseBranch": "release",
|
||||
"github": "https://github.com/mProjectsCode/obsidian-media-db-plugin"
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export class UserError extends Error {}
|
||||
|
||||
export interface ProjectConfig {
|
||||
corePackages: string[];
|
||||
packages: string[];
|
||||
}
|
||||
|
|
@ -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
295
bun.lock
|
|
@ -5,33 +5,34 @@
|
|||
"": {
|
||||
"name": "obsidian-media-db-plugin",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "^20.8.9",
|
||||
"@lemons_dev/parsinom": "^0.1.0",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@happy-dom/global-registrator": "^20.9.0",
|
||||
"@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3",
|
||||
"@types/bun": "^1.3.13",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.6.1",
|
||||
"eslint-plugin-obsidianmd": "^0.2.9",
|
||||
"eslint-plugin-only-warn": "^1.2.1",
|
||||
"iso-639-2": "^3.0.2",
|
||||
"obsidian": "latest",
|
||||
"openapi-fetch": "^0.17.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier": "^3.8.3",
|
||||
"solid-js": "^1.9.12",
|
||||
"string-argv": "^0.3.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.5",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-banner": "^0.8.1",
|
||||
"vite-plugin-solid": "^2.11.12",
|
||||
"vite-plugin-static-copy": "^4.0.1",
|
||||
"vite-plugin-static-copy": "^4.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
|
|
@ -55,7 +56,7 @@
|
|||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
|
||||
|
||||
|
|
@ -69,9 +70,9 @@
|
|||
|
||||
"@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
|
|
@ -89,18 +90,24 @@
|
|||
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
||||
|
||||
"@eslint/json": ["@eslint/json@0.14.0", "", { "dependencies": { "@eslint/core": "^0.17.0", "@eslint/plugin-kit": "^0.4.1", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="],
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.9.0" } }, "sha512-lBW6/m5BIFl3pMuWPNN0lIOYw9LMCmPfix53ExS3FBi4E+NELEljQ3xH6aAV9IYiQRfn9YIIgzzMrD0vIcD7tw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
|
@ -113,55 +120,61 @@
|
|||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.1.0", "", {}, "sha512-Jek2Drc1n6lRbr3QCB9JzK4rId5/U3rMmywiE/Quqmvq+f4unVqLeHe6XVTm6DeKn/T4Udgx/pC4Okt8FHX2sw=="],
|
||||
"@lemons_dev/lemons-obsidian-plugin-automation": ["@lemons_dev/lemons-obsidian-plugin-automation@0.1.3", "", { "dependencies": { "@lemons_dev/parsinom": "^0.2.1", "moment": "^2.30.1", "string-argv": "^0.3.2" }, "peerDependencies": { "vite": "^8.0.9" }, "bin": { "lemons-automation": "bin/lemons-automation.js" } }, "sha512-Nv1eONy1Q9citS+nsMMasgL9ZfhKb57uinJmHRF+F1H+9HSiRkB5wlx9J/S1ou9cJL8IAhFvv72KGzE9HTIWtg=="],
|
||||
|
||||
"@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.2.1", "", {}, "sha512-ew+G3gm5aWJBnIhrysxI7k09FCAtAoj48wbTzr16NoX9ndhB8UnhmqjTxYVv46DsG88HQo+DTWPIFQW5qTAwwg=="],
|
||||
|
||||
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
|
||||
"@microsoft/eslint-plugin-sdl": ["@microsoft/eslint-plugin-sdl@1.1.0", "", { "dependencies": { "eslint-plugin-n": "17.10.3", "eslint-plugin-react": "7.37.3", "eslint-plugin-security": "1.4.0" }, "peerDependencies": { "eslint": "^9" } }, "sha512-dxdNHOemLnBhfY3eByrujX9KyLigcNtW8sU+axzWv5nLGcsSBeKW2YYyTpfPo1hV8YPOmIGnfA4fZHyKVtWqBQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="],
|
||||
|
||||
"@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
|
||||
|
||||
"@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
|
||||
|
||||
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.11", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-V09ayfnb5GyysmvARbt+voFZAjGcf7hSYxOYxSkCc4fbH/DTfq5YWoec8cflvmHHqyIFbqvmGKmYFzqhr9zxDg=="],
|
||||
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
|
|
@ -171,17 +184,19 @@
|
|||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||
|
||||
"@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="],
|
||||
|
||||
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
||||
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||
|
||||
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
|
||||
|
||||
|
|
@ -189,25 +204,25 @@
|
|||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
|
|
@ -215,7 +230,7 @@
|
|||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
|
|
@ -229,12 +244,16 @@
|
|||
|
||||
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
||||
|
||||
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
|
||||
|
||||
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
||||
|
||||
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
|
||||
|
||||
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
|
||||
|
||||
"array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
|
@ -247,19 +266,19 @@
|
|||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="],
|
||||
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
|
|
@ -267,7 +286,7 @@
|
|||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001786", "", {}, "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
|
|
@ -311,16 +330,22 @@
|
|||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.351", "", {}, "sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA=="],
|
||||
|
||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
||||
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
|
@ -335,14 +360,34 @@
|
|||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
|
||||
"eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="],
|
||||
|
||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
||||
|
||||
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
|
||||
|
||||
"eslint-plugin-depend": ["eslint-plugin-depend@1.3.1", "", { "dependencies": { "empathic": "^2.0.0", "module-replacements": "^2.8.0", "semver": "^7.6.3" } }, "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw=="],
|
||||
|
||||
"eslint-plugin-es-x": ["eslint-plugin-es-x@7.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", "@eslint-community/regexpp": "^4.11.0", "eslint-compat-utils": "^0.5.1" }, "peerDependencies": { "eslint": ">=8" } }, "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ=="],
|
||||
|
||||
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
|
||||
|
||||
"eslint-plugin-json-schema-validator": ["eslint-plugin-json-schema-validator@5.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", "ajv": "^8.0.0", "debug": "^4.3.1", "eslint-compat-utils": "^0.5.0", "json-schema-migrate": "^2.0.0", "jsonc-eslint-parser": "^2.0.0", "minimatch": "^8.0.0", "synckit": "^0.9.0", "toml-eslint-parser": "^0.9.0", "tunnel-agent": "^0.6.0", "yaml-eslint-parser": "^1.0.0" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-ZmVyxRIjm58oqe2kTuy90PpmZPrrKvOjRPXKzq8WCgRgAkidCgm5X8domL2KSfadZ3QFAmifMgGTcVNhZ5ez2g=="],
|
||||
|
||||
"eslint-plugin-n": ["eslint-plugin-n@17.10.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "enhanced-resolve": "^5.17.0", "eslint-plugin-es-x": "^7.5.0", "get-tsconfig": "^4.7.0", "globals": "^15.8.0", "ignore": "^5.2.4", "minimatch": "^9.0.5", "semver": "^7.5.3" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw=="],
|
||||
|
||||
"eslint-plugin-no-relative-import-paths": ["eslint-plugin-no-relative-import-paths@1.6.1", "", {}, "sha512-YZNeOnsOrJcwhFw0X29MXjIzu2P/f5X2BZDPWw1R3VUYBRFxNIh77lyoL/XrMU9ewZNQPcEvAgL/cBOT1P330A=="],
|
||||
|
||||
"eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="],
|
||||
|
||||
"eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="],
|
||||
|
||||
"eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="],
|
||||
|
||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.3", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA=="],
|
||||
|
||||
"eslint-plugin-security": ["eslint-plugin-security@2.1.1", "", { "dependencies": { "safe-regex": "^2.1.1" } }, "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
|
@ -363,6 +408,8 @@
|
|||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
|
@ -395,6 +442,8 @@
|
|||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
|
@ -403,7 +452,9 @@
|
|||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="],
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
|
|
@ -417,7 +468,7 @@
|
|||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||
|
||||
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
|
||||
|
||||
|
|
@ -445,7 +496,7 @@
|
|||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||
|
||||
|
|
@ -493,6 +544,8 @@
|
|||
|
||||
"iso-639-2": ["iso-639-2@3.0.2", "", {}, "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw=="],
|
||||
|
||||
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
||||
|
||||
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
|
@ -503,12 +556,18 @@
|
|||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-migrate": ["json-schema-migrate@2.0.0", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||
|
||||
"jsonc-eslint-parser": ["jsonc-eslint-parser@2.4.2", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", "espree": "^9.0.0", "semver": "^7.3.5" } }, "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA=="],
|
||||
|
||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
|
@ -541,6 +600,8 @@
|
|||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
|
@ -551,20 +612,24 @@
|
|||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
|
||||
"module-replacements": ["module-replacements@2.11.0", "", {}, "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA=="],
|
||||
|
||||
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||
"node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
|
@ -617,18 +682,24 @@
|
|||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
|
@ -637,19 +708,27 @@
|
|||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
"ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="],
|
||||
"seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="],
|
||||
"seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
|
|
@ -663,7 +742,7 @@
|
|||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
|
|
@ -679,6 +758,10 @@
|
|||
|
||||
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
|
||||
|
||||
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||
|
||||
"string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||
|
|
@ -695,16 +778,24 @@
|
|||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
"synckit": ["synckit@0.9.3", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"toml-eslint-parser": ["toml-eslint-parser@0.9.3", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0" } }, "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
|
@ -717,13 +808,13 @@
|
|||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
|
|
@ -731,13 +822,13 @@
|
|||
|
||||
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
|
||||
|
||||
"vite": ["vite@8.0.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="],
|
||||
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
|
||||
|
||||
"vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="],
|
||||
|
||||
"vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
||||
|
||||
"vite-plugin-static-copy": ["vite-plugin-static-copy@4.0.1", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-r3kQUrrimduikhyRm58ayemoxsgB8lZdn/JULLL4wpXHAZlYejtyZx7E/id7dwRtIOSYWu/tWvFjdEOTzso2MA=="],
|
||||
"vite-plugin-static-copy": ["vite-plugin-static-copy@4.1.0", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
|
|
@ -761,8 +852,12 @@
|
|||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="],
|
||||
|
||||
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
|
||||
|
||||
"yaml-eslint-parser": ["yaml-eslint-parser@1.3.2", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0", "yaml": "^2.0.0" } }, "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
|
@ -771,6 +866,8 @@
|
|||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@microsoft/eslint-plugin-sdl/eslint-plugin-security": ["eslint-plugin-security@1.4.0", "", { "dependencies": { "safe-regex": "^1.1.0" } }, "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA=="],
|
||||
|
||||
"@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||
|
|
@ -791,20 +888,66 @@
|
|||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"eslint-compat-utils/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-depend/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-json-schema-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"eslint-plugin-json-schema-validator/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||
|
||||
"eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||
|
||||
"eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||
|
||||
"eslint-plugin-n/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="],
|
||||
|
||||
"eslint-plugin-obsidianmd/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
||||
|
||||
"json-schema-migrate/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
|
||||
|
||||
"jsonc-eslint-parser/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="],
|
||||
"toml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"yaml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@microsoft/eslint-plugin-sdl/eslint-plugin-security/safe-regex": ["safe-regex@1.1.0", "", { "dependencies": { "ret": "~0.1.10" } }, "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg=="],
|
||||
|
||||
"@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||
|
||||
"eslint-plugin-json-schema-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
||||
|
||||
"eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
||||
|
||||
"eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import only_warn from 'eslint-plugin-only-warn';
|
||||
import * as plugin_import from 'eslint-plugin-import';
|
||||
import no_relative_import_paths from 'eslint-plugin-no-relative-import-paths';
|
||||
import obsidianmd from 'eslint-plugin-obsidianmd';
|
||||
|
||||
export default tseslint.config(
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css', 'src/api/schemas/'],
|
||||
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css'],
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked],
|
||||
files: ['packages/obsidian/src/**/*.ts'],
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
...obsidianmd.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
|
|
@ -21,7 +29,8 @@ export default tseslint.config(
|
|||
plugins: {
|
||||
// @ts-ignore
|
||||
'only-warn': only_warn,
|
||||
import: plugin_import,
|
||||
'no-relative-import-paths': no_relative_import_paths,
|
||||
obsidianmd: obsidianmd,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': ['warn'],
|
||||
|
|
@ -44,11 +53,15 @@ export default tseslint.config(
|
|||
'@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
|
||||
'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: false }],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': ['warn'],
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/no-deprecated': 'off',
|
||||
|
||||
'obsidianmd/ui/sentence-case': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "obsidian-media-db-plugin",
|
||||
"name": "Media DB",
|
||||
"version": "0.8.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"minAppVersion": "1.12.0",
|
||||
"description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.",
|
||||
"author": "Moritz Jung",
|
||||
"authorUrl": "https://www.moritzjung.dev",
|
||||
|
|
|
|||
30
package.json
30
package.json
|
|
@ -6,41 +6,41 @@
|
|||
"scripts": {
|
||||
"dev": "vite build --watch --mode development",
|
||||
"build": "bun run tsc && vite build --mode production",
|
||||
"tsc": "tsc -noEmit -skipLibCheck",
|
||||
"typecheck": "tsc -noEmit -skipLibCheck",
|
||||
"test": "bun test",
|
||||
"test:log": "LOG_TESTS=true bun test",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint --max-warnings=0 --no-warn-ignored src/**",
|
||||
"lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**",
|
||||
"check": "bun run format:check && bun run tsc && bun run lint",
|
||||
"check:fix": "bun run format && bun run tsc && bun run lint:fix",
|
||||
"release": "bun run automation/release.ts",
|
||||
"stats": "bun run automation/stats.ts"
|
||||
"lint": "eslint --max-warnings=0 --no-warn-ignored packages/obsidian/src/**",
|
||||
"lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored packages/obsidian/src/**",
|
||||
"check": "bun run format:check && bun run typecheck && bun run lint",
|
||||
"check:fix": "bun run format && bun run typecheck && bun run lint:fix",
|
||||
"release": "lemons-automation release"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Moritz Jung",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "^20.8.9",
|
||||
"@lemons_dev/parsinom": "^0.1.0",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@happy-dom/global-registrator": "^20.9.0",
|
||||
"@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3",
|
||||
"@types/bun": "^1.3.13",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.6.1",
|
||||
"eslint-plugin-obsidianmd": "^0.2.9",
|
||||
"eslint-plugin-only-warn": "^1.2.1",
|
||||
"iso-639-2": "^3.0.2",
|
||||
"obsidian": "latest",
|
||||
"openapi-fetch": "^0.17.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier": "^3.8.3",
|
||||
"solid-js": "^1.9.12",
|
||||
"string-argv": "^0.3.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.5",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-banner": "^0.8.1",
|
||||
"vite-plugin-solid": "^2.11.12",
|
||||
"vite-plugin-static-copy": "^4.0.1"
|
||||
"vite-plugin-static-copy": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
113
packages/obsidian/src/api/APIManager.ts
Normal file
113
packages/obsidian/src/api/APIManager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import type { MediaType } from '../utils/MediaType';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { AppError } from 'packages/obsidian/src/utils/AppError';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { Result } from 'packages/obsidian/src/utils/result';
|
||||
|
||||
export abstract class APIModel {
|
||||
apiName!: string;
|
||||
|
|
@ -14,9 +16,9 @@ export abstract class APIModel {
|
|||
*
|
||||
* @param title the title to query for
|
||||
*/
|
||||
abstract searchByTitle(title: string): Promise<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[];
|
||||
|
||||
217
packages/obsidian/src/api/apis/BoardGameGeekAPI.ts
Normal file
217
packages/obsidian/src/api/apis/BoardGameGeekAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
170
packages/obsidian/src/api/apis/ComicVineAPI.ts
Normal file
170
packages/obsidian/src/api/apis/ComicVineAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
169
packages/obsidian/src/api/apis/GiantBombAPI.ts
Normal file
169
packages/obsidian/src/api/apis/GiantBombAPI.ts
Normal 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;
|
||||
// }
|
||||
// }
|
||||
264
packages/obsidian/src/api/apis/IGDBAPI.ts
Normal file
264
packages/obsidian/src/api/apis/IGDBAPI.ts
Normal 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 ?? [];
|
||||
}
|
||||
}
|
||||
260
packages/obsidian/src/api/apis/MALAPI.ts
Normal file
260
packages/obsidian/src/api/apis/MALAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { ComicMangaModel } from '../../models/ComicMangaModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { isTruthy, obsidianFetch } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/MALAPI';
|
||||
import { APIModel } from 'packages/obsidian/src/api/APIModel';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { AppError } from 'packages/obsidian/src/utils/AppError';
|
||||
import { AppErrorKind } from 'packages/obsidian/src/utils/AppError';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { Result } from 'packages/obsidian/src/utils/result';
|
||||
import { err, ok } from 'packages/obsidian/src/utils/result';
|
||||
import { isTruthy, obsidianFetch } from 'packages/obsidian/src/utils/Utils';
|
||||
import type { paths } from 'packages/schemas/src/MALAPI';
|
||||
|
||||
export class MALAPIManga extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -30,8 +35,8 @@ export class MALAPIManga extends APIModel {
|
|||
this.typeMappings.set('novel', 'novel');
|
||||
}
|
||||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
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/' });
|
||||
|
||||
|
|
@ -47,7 +52,12 @@ export class MALAPIManga extends APIModel {
|
|||
});
|
||||
|
||||
if (response.error !== undefined) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
return err({
|
||||
kind: AppErrorKind.Api,
|
||||
message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`,
|
||||
userMessage: `Received status code ${response.response.status} from ${this.apiName}.`,
|
||||
context: { apiName: this.apiName, status: response.response.status },
|
||||
});
|
||||
}
|
||||
|
||||
const data = response.data?.data;
|
||||
|
|
@ -93,11 +103,11 @@ export class MALAPIManga extends APIModel {
|
|||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return ok(ret);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
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/' });
|
||||
|
||||
|
|
@ -111,13 +121,23 @@ export class MALAPIManga extends APIModel {
|
|||
});
|
||||
|
||||
if (response.error !== undefined) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
return err({
|
||||
kind: AppErrorKind.Api,
|
||||
message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`,
|
||||
userMessage: `Received status code ${response.response.status} from ${this.apiName}.`,
|
||||
context: { apiName: this.apiName, status: response.response.status, id },
|
||||
});
|
||||
}
|
||||
|
||||
const result = response.data?.data;
|
||||
|
||||
if (!result) {
|
||||
throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`);
|
||||
return err({
|
||||
kind: AppErrorKind.Api,
|
||||
message: `MDB | No data found for ID ${id} in ${this.apiName}.`,
|
||||
userMessage: `No data found for ID ${id} in ${this.apiName}.`,
|
||||
context: { apiName: this.apiName, id },
|
||||
});
|
||||
}
|
||||
|
||||
const resType = result.type?.toLowerCase();
|
||||
|
|
@ -125,7 +145,8 @@ export class MALAPIManga extends APIModel {
|
|||
const year = result.published?.prop?.from?.year?.toString() ?? '';
|
||||
const new_id = result.mal_id?.toString();
|
||||
|
||||
return new ComicMangaModel({
|
||||
return ok(
|
||||
new ComicMangaModel({
|
||||
subType: type,
|
||||
title: result.title,
|
||||
plot: result.synopsis ?? undefined,
|
||||
|
|
@ -153,7 +174,8 @@ export class MALAPIManga extends APIModel {
|
|||
lastRead: '',
|
||||
personalRating: 0,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.MALAPIManga_disabledMediaTypes;
|
||||
327
packages/obsidian/src/api/apis/MusicBrainzAPI.ts
Normal file
327
packages/obsidian/src/api/apis/MusicBrainzAPI.ts
Normal 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')}`;
|
||||
}
|
||||
388
packages/obsidian/src/api/apis/OMDbAPI.ts
Normal file
388
packages/obsidian/src/api/apis/OMDbAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
214
packages/obsidian/src/api/apis/OpenLibraryAPI.ts
Normal file
214
packages/obsidian/src/api/apis/OpenLibraryAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
161
packages/obsidian/src/api/apis/RAWGAPI.ts
Normal file
161
packages/obsidian/src/api/apis/RAWGAPI.ts
Normal 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 ?? [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import { requestUrl } from 'obsidian';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { GameModel } from '../../models/GameModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { imageUrlExists } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
import { APIModel } from 'packages/obsidian/src/api/APIModel';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { GameModel } from 'packages/obsidian/src/models/GameModel';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { AppError } from 'packages/obsidian/src/utils/AppError';
|
||||
import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { Result } from 'packages/obsidian/src/utils/result';
|
||||
import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result';
|
||||
import { imageUrlExists } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
interface SearchResponse {
|
||||
appid: string;
|
||||
|
|
@ -143,16 +148,34 @@ export class SteamAPI extends APIModel {
|
|||
this.typeMappings.set('game', 'game');
|
||||
}
|
||||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
async searchByTitle(title: string): Promise<Result<MediaTypeModel[], AppError>> {
|
||||
Logger.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
|
||||
const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`;
|
||||
const fetchData = await requestUrl({
|
||||
const fetchDataResult = await fromPromise(
|
||||
requestUrl({
|
||||
url: searchUrl,
|
||||
});
|
||||
}),
|
||||
cause =>
|
||||
toAppError(cause, {
|
||||
kind: AppErrorKind.Network,
|
||||
message: `MDB | Network error querying ${this.apiName}`,
|
||||
userMessage: `Network error querying ${this.apiName}`,
|
||||
context: { apiName: this.apiName, title },
|
||||
}),
|
||||
);
|
||||
if (!fetchDataResult.ok) {
|
||||
return err(fetchDataResult.error);
|
||||
}
|
||||
const fetchData = fetchDataResult.value;
|
||||
|
||||
if (fetchData.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
|
||||
return err({
|
||||
kind: AppErrorKind.Api,
|
||||
message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`,
|
||||
userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`,
|
||||
context: { apiName: this.apiName, status: fetchData.status },
|
||||
});
|
||||
}
|
||||
|
||||
const data = (await fetchData.json) as SearchResponse[];
|
||||
|
|
@ -174,19 +197,37 @@ export class SteamAPI extends APIModel {
|
|||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return ok(ret);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
async getById(id: string): Promise<Result<MediaTypeModel, AppError>> {
|
||||
Logger.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
|
||||
const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`;
|
||||
const fetchData = await requestUrl({
|
||||
const fetchDataResult = await fromPromise(
|
||||
requestUrl({
|
||||
url: searchUrl,
|
||||
});
|
||||
}),
|
||||
cause =>
|
||||
toAppError(cause, {
|
||||
kind: AppErrorKind.Network,
|
||||
message: `MDB | Network error querying ${this.apiName}`,
|
||||
userMessage: `Network error querying ${this.apiName}`,
|
||||
context: { apiName: this.apiName, id },
|
||||
}),
|
||||
);
|
||||
if (!fetchDataResult.ok) {
|
||||
return err(fetchDataResult.error);
|
||||
}
|
||||
const fetchData = fetchDataResult.value;
|
||||
|
||||
if (fetchData.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
|
||||
return err({
|
||||
kind: AppErrorKind.Api,
|
||||
message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`,
|
||||
userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`,
|
||||
context: { apiName: this.apiName, status: fetchData.status, id },
|
||||
});
|
||||
}
|
||||
|
||||
// console.debug(await fetchData.json);
|
||||
|
|
@ -200,14 +241,27 @@ export class SteamAPI extends APIModel {
|
|||
}
|
||||
}
|
||||
if (!result) {
|
||||
throw Error(`MDB | API returned invalid data.`);
|
||||
return err({
|
||||
kind: AppErrorKind.Api,
|
||||
message: 'MDB | API returned invalid data.',
|
||||
userMessage: 'Steam returned invalid data.',
|
||||
context: { apiName: this.apiName, id },
|
||||
});
|
||||
}
|
||||
|
||||
// console.debug(result);
|
||||
|
||||
// Check if a poster version of the image exists, else use the header image
|
||||
const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`;
|
||||
const exists = await imageUrlExists(imageUrl);
|
||||
const existsResult = await fromPromise(imageUrlExists(imageUrl), cause =>
|
||||
toAppError(cause, {
|
||||
kind: AppErrorKind.Network,
|
||||
message: `MDB | Failed to validate image URL for ${this.apiName}`,
|
||||
userMessage: `Failed to validate image URL for ${this.apiName}`,
|
||||
context: { apiName: this.apiName, id, imageUrl },
|
||||
}),
|
||||
);
|
||||
const exists = existsResult.ok ? existsResult.value : false;
|
||||
let finalimageurl;
|
||||
if (exists) {
|
||||
finalimageurl = imageUrl;
|
||||
|
|
@ -215,7 +269,8 @@ export class SteamAPI extends APIModel {
|
|||
finalimageurl = result.header_image ?? '';
|
||||
}
|
||||
|
||||
return new GameModel({
|
||||
return ok(
|
||||
new GameModel({
|
||||
type: MediaType.Game,
|
||||
title: result.name,
|
||||
englishTitle: result.name,
|
||||
|
|
@ -237,7 +292,8 @@ export class SteamAPI extends APIModel {
|
|||
played: false,
|
||||
personalRating: 0,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.SteamAPI_disabledMediaTypes;
|
||||
265
packages/obsidian/src/api/apis/TMDBMovieAPI.ts
Normal file
265
packages/obsidian/src/api/apis/TMDBMovieAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
438
packages/obsidian/src/api/apis/TMDBSeasonAPI.ts
Normal file
438
packages/obsidian/src/api/apis/TMDBSeasonAPI.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
255
packages/obsidian/src/api/apis/TMDBSeriesAPI.ts
Normal file
255
packages/obsidian/src/api/apis/TMDBSeriesAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
334
packages/obsidian/src/api/apis/VNDBAPI.ts
Normal file
334
packages/obsidian/src/api/apis/VNDBAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
170
packages/obsidian/src/api/apis/WikipediaAPI.ts
Normal file
170
packages/obsidian/src/api/apis/WikipediaAPI.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
209
packages/obsidian/src/main.ts
Normal file
209
packages/obsidian/src/main.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ButtonComponent } from 'obsidian';
|
||||
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper';
|
||||
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
|
||||
export class MediaDbAdvancedSearchModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -71,7 +71,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
|
|||
|
||||
const placeholder = 'Search by title';
|
||||
const searchComponent = new TextComponent(contentEl);
|
||||
searchComponent.inputEl.style.width = '100%';
|
||||
searchComponent.inputEl.addClass('media-db-plugin-search-input');
|
||||
searchComponent.setPlaceholder(placeholder);
|
||||
searchComponent.setValue(this.query);
|
||||
searchComponent.onChange(value => (this.query = value));
|
||||
|
|
@ -85,13 +85,13 @@ export class MediaDbAdvancedSearchModal extends Modal {
|
|||
|
||||
// const apiToggleComponents: Component[] = [];
|
||||
for (const api of this.plugin.apiManager.apis) {
|
||||
const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
|
||||
|
||||
const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiToggleTextWrapper.createEl('span', { text: api.apiName, cls: 'media-db-plugin-list-text' });
|
||||
const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiToggleTextWrapper.createSpan({ text: api.apiName, cls: 'media-db-plugin-list-text' });
|
||||
apiToggleTextWrapper.createEl('small', { text: api.apiDescription, cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' });
|
||||
const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' });
|
||||
|
||||
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
|
||||
apiToggleComponent.setTooltip(api.apiName);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ButtonComponent } from 'obsidian';
|
||||
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import type { APIModel } from '../api/APIModel';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import { BulkImportLookupMethod } from '../utils/BulkImportHelper';
|
||||
import type { APIModel } from 'packages/obsidian/src/api/APIModel';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { BulkImportLookupMethod } from 'packages/obsidian/src/utils/BulkImportHelper';
|
||||
|
||||
export class MediaDbBulkImportModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -47,14 +47,14 @@ export class MediaDbBulkImportModal extends Modal {
|
|||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' });
|
||||
|
||||
const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
appendContentToggleTextWrapper.createEl('span', {
|
||||
const appendContentToggleElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
|
||||
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
|
||||
appendContentToggleTextWrapper.createSpan({
|
||||
text: 'If this is enabled, the plugin will override metadata fields with the same name.',
|
||||
cls: 'media-db-plugin-list-text',
|
||||
});
|
||||
|
||||
const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' });
|
||||
const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' });
|
||||
|
||||
const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper);
|
||||
appendContentToggle.setValue(false);
|
||||
|
|
@ -81,9 +81,9 @@ export class MediaDbBulkImportModal extends Modal {
|
|||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
|
||||
const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' });
|
||||
const fieldNameWrapperEl = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
|
||||
const fieldNameLabelWrapperEl = fieldNameWrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
|
||||
fieldNameLabelWrapperEl.createSpan({ text: 'Using the property named', cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const fieldNameComponent = new TextComponent(fieldNameWrapperEl);
|
||||
fieldNameComponent.setPlaceholder('title / id');
|
||||
|
|
@ -115,9 +115,9 @@ export class MediaDbBulkImportModal extends Modal {
|
|||
}
|
||||
|
||||
createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void {
|
||||
const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' });
|
||||
const wrapperEl = parentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
|
||||
const labelWrapperEl = wrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
|
||||
labelWrapperEl.createSpan({ text: label, cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const dropDownComponent = new DropdownComponent(wrapperEl);
|
||||
dropDownComponent.onChange(onChange);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ButtonComponent } from 'obsidian';
|
||||
import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper';
|
||||
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { IdSearchModalData, IdSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
|
||||
export class MediaDbIdSearchModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -69,7 +69,7 @@ export class MediaDbIdSearchModal extends Modal {
|
|||
|
||||
const placeholder = 'Search by id';
|
||||
const searchComponent = new TextComponent(contentEl);
|
||||
searchComponent.inputEl.style.width = '100%';
|
||||
searchComponent.inputEl.addClass('media-db-plugin-search-input');
|
||||
searchComponent.setPlaceholder(placeholder);
|
||||
searchComponent.onChange(value => (this.query = value));
|
||||
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
|
||||
|
|
@ -79,9 +79,9 @@ export class MediaDbIdSearchModal extends Modal {
|
|||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
|
||||
const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiSelectorTExtWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' });
|
||||
const apiSelectorWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
|
||||
const apiSelectorTExtWrapper = apiSelectorWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiSelectorTExtWrapper.createSpan({ text: 'API to search', cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper);
|
||||
apiSelectorComponent.onChange((value: string) => {
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper';
|
||||
import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import type { PreviewModalData, PreviewModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
import { PREVIEW_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
|
||||
export class MediaDbPreviewModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -48,14 +49,14 @@ export class MediaDbPreviewModal extends Modal {
|
|||
previewWrapper.createEl('h3', { text: result.englishTitle });
|
||||
const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' });
|
||||
|
||||
let fileContent = this.plugin.generateMediaDbNoteFrontmatterPreview(result);
|
||||
let fileContent = this.plugin.fileHelper.generateMediaDbNoteFrontmatterPreview(result);
|
||||
fileContent = `\`\`\`yaml\n${fileContent}\`\`\``;
|
||||
|
||||
try {
|
||||
// TODO: fix this not rendering the frontmatter any more
|
||||
await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent);
|
||||
} catch (e) {
|
||||
console.warn(`mdb | error during rendering of preview`, e);
|
||||
Logger.warn(`mdb | error during rendering of preview`, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import type { ButtonComponent } from 'obsidian';
|
||||
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaType } from '../utils/MediaType';
|
||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||
import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper';
|
||||
import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
|
||||
import { unCamelCase } from '../utils/Utils';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
|
||||
import type { SearchModalData, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
import { SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
import { unCamelCase } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export class MediaDbSearchModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -76,7 +76,7 @@ export class MediaDbSearchModal extends Modal {
|
|||
const searchComponent = new TextComponent(contentEl);
|
||||
let currentToggle: ToggleComponent | undefined = undefined;
|
||||
|
||||
searchComponent.inputEl.style.width = '100%';
|
||||
searchComponent.inputEl.addClass('media-db-plugin-search-input');
|
||||
searchComponent.setPlaceholder(placeholder);
|
||||
searchComponent.setValue(this.query);
|
||||
searchComponent.onChange(value => (this.query = value));
|
||||
|
|
@ -89,12 +89,12 @@ export class MediaDbSearchModal extends Modal {
|
|||
contentEl.createEl('h3', { text: 'APIs to search' });
|
||||
|
||||
for (const mediaType of MEDIA_TYPES) {
|
||||
const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' });
|
||||
|
||||
const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiToggleTextWrapper.createEl('span', { text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' });
|
||||
const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiToggleTextWrapper.createSpan({ text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' });
|
||||
const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' });
|
||||
|
||||
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
|
||||
apiToggleComponent.setTooltip(unCamelCase(mediaType));
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper';
|
||||
import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper';
|
||||
import { SelectModal } from './SelectModal';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper';
|
||||
|
||||
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -41,7 +41,7 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
|
|||
|
||||
// Renders each suggestion item.
|
||||
renderElement(item: MediaTypeModel, el: HTMLElement): void {
|
||||
el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) });
|
||||
el.createDiv({ text: this.plugin.mediaTypeManager.getFileName(item) });
|
||||
el.createEl('small', { text: `${item.getSummary()}\n` });
|
||||
el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` });
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type MediaDbPlugin from '../main';
|
||||
import { SelectModal } from './SelectModal';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
|
||||
|
||||
export interface SeasonSelectModalElement {
|
||||
season_number: number;
|
||||
|
|
@ -24,7 +24,7 @@ export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalEleme
|
|||
}
|
||||
|
||||
renderElement(season: SeasonSelectModalElement, el: HTMLElement): void {
|
||||
el.createEl('div', { text: `${season.name}` });
|
||||
el.createDiv({ text: `${season.name}` });
|
||||
if (season.air_date) {
|
||||
el.createEl('small', { text: `Air date: ${season.air_date}` });
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { App, ButtonComponent } from 'obsidian';
|
||||
import { Modal, Setting } from 'obsidian';
|
||||
import { mod } from '../utils/Utils';
|
||||
import { SelectModalElement } from './SelectModalElement';
|
||||
import { SelectModalElement } from 'packages/obsidian/src/modals/SelectModalElement';
|
||||
import { mod } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export abstract class SelectModal<T> extends Modal {
|
||||
allowMultiSelect: boolean;
|
||||
|
|
@ -48,7 +48,7 @@ export abstract class SelectModal<T> extends Modal {
|
|||
this.activateHighlighted();
|
||||
});
|
||||
this.scope.register([], ' ', evt => {
|
||||
if (this.elementWrapper && this.elementWrapper === document.activeElement) {
|
||||
if (this.elementWrapper && this.elementWrapper === activeDocument.activeElement) {
|
||||
this.activateHighlighted();
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { SelectModal } from './SelectModal';
|
||||
import type { SelectModal } from 'packages/obsidian/src/modals/SelectModal';
|
||||
|
||||
export class SelectModalElement<T> {
|
||||
selectModal: SelectModal<T>;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type BoardGameData = ModelToData<BoardGameModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type BookData = ModelToData<BookModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type ComicMangaData = ModelToData<ComicMangaModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type GameData = ModelToData<GameModel>;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MediaType } from '../utils/MediaType';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
|
||||
export abstract class MediaTypeModel {
|
||||
type: string;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type MovieData = ModelToData<MovieModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type MusicReleaseData = ModelToData<MusicReleaseModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type SeasonData = ModelToData<SeasonModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type SeriesData = ModelToData<SeriesModel>;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export type WikiData = ModelToData<WikiModel>;
|
||||
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaType } from '../utils/MediaType';
|
||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||
import { PropertyMappingOption } from './PropertyMapping';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
|
||||
|
||||
export class PropertyMapper {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -75,7 +76,7 @@ export class PropertyMapper {
|
|||
|
||||
if (obj.type === 'manga') {
|
||||
obj.type = 'comicManga';
|
||||
console.debug(`MDB | updated metadata type`, obj.type);
|
||||
Logger.debug(`MDB | updated metadata type`, obj.type);
|
||||
}
|
||||
if (!MEDIA_TYPES.includes(obj.type as MediaType)) {
|
||||
return obj;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MediaType } from '../utils/MediaType';
|
||||
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils';
|
||||
import { Logger } from 'packages/obsidian/src/utils/Logger';
|
||||
import type { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
// Plain object interfaces for serialization
|
||||
export interface PropertyMappingData {
|
||||
|
|
@ -33,7 +34,7 @@ export class PropertyMappingModel {
|
|||
}
|
||||
|
||||
validate(): { res: boolean; err?: Error } {
|
||||
console.debug(`MDB | validated property mappings for ${this.type}`);
|
||||
Logger.debug(`MDB | validated property mappings for ${this.type}`);
|
||||
|
||||
// check properties
|
||||
for (const property of this.properties) {
|
||||
|
|
@ -1,16 +1,51 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Notice, PluginSettingTab, SecretComponent, SettingGroup } from 'obsidian';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping';
|
||||
import PropertyMappingModelsComponent from 'packages/obsidian/src/settings/PropertyMappingModelsComponent';
|
||||
import { FileSuggest } from 'packages/obsidian/src/settings/suggesters/FileSuggest';
|
||||
import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderSuggest';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
|
||||
import { unCamelCase } from 'packages/obsidian/src/utils/Utils';
|
||||
import { render } from 'solid-js/web';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import { MediaType } from '../utils/MediaType';
|
||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||
import { fragWithHTML, unCamelCase } from '../utils/Utils';
|
||||
import type { PropertyMappingModelData } from './PropertyMapping';
|
||||
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping';
|
||||
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent';
|
||||
import { FileSuggest } from './suggesters/FileSuggest';
|
||||
import { FolderSuggest } from './suggesters/FolderSuggest';
|
||||
|
||||
function createDateFormatDescription(preview: string): DocumentFragment {
|
||||
return createFragment(frag => {
|
||||
const container = frag.createDiv();
|
||||
container.appendText('Your custom date format. Use ');
|
||||
container.createEl('em', { text: "'YYYY-MM-DD'" });
|
||||
container.appendText(' for example.');
|
||||
container.createEl('br');
|
||||
container.appendText('For more syntax, refer to ');
|
||||
container.createEl('a', {
|
||||
href: 'https://momentjs.com/docs/#/displaying/format/',
|
||||
text: 'format reference',
|
||||
});
|
||||
container.appendText('.');
|
||||
container.createEl('br');
|
||||
container.appendText('Your current syntax looks like this: ');
|
||||
container.createEl('em', { text: preview });
|
||||
});
|
||||
}
|
||||
|
||||
function createPropertyMappingsDescription(): DocumentFragment {
|
||||
return createFragment(frag => {
|
||||
const container = frag.createDiv();
|
||||
container.createEl('p', {
|
||||
text: 'Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.',
|
||||
});
|
||||
container.createEl('p', {
|
||||
text: 'You can choose to keep the original name, rename the property, or remove it entirely.',
|
||||
});
|
||||
const paragraph = container.createEl('p');
|
||||
paragraph.createEl('strong', {
|
||||
text: 'Remember to save your changes using the save button for each individual category.',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
export interface MediaDbPluginSettings {
|
||||
|
|
@ -435,22 +470,14 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
setting =>
|
||||
void setting
|
||||
.setName('Date format')
|
||||
.setDesc(
|
||||
fragWithHTML(
|
||||
"Your custom date format. Use <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>',
|
||||
),
|
||||
)
|
||||
.setDesc(createDateFormatDescription(this.plugin.dateFormatter.getPreview()))
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat)
|
||||
.setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat)
|
||||
.onChange(data => {
|
||||
const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat;
|
||||
this.plugin.settings.customDateFormat = newDateFormat;
|
||||
const previewEl = document.getElementById('media-db-dateformat-preview');
|
||||
const previewEl = activeDocument.getElementById('media-db-dateformat-preview');
|
||||
if (previewEl) {
|
||||
previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview
|
||||
}
|
||||
|
|
@ -809,15 +836,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
const mappingGroup = new SettingGroup(containerEl);
|
||||
mappingGroup.setHeading('Property mappings');
|
||||
mappingGroup.addSetting(setting => {
|
||||
setting
|
||||
.setName('Property mappings explanation')
|
||||
.setDesc(
|
||||
fragWithHTML(
|
||||
'<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>',
|
||||
),
|
||||
);
|
||||
setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription());
|
||||
|
||||
render(
|
||||
() =>
|
||||
|
|
@ -64,6 +64,10 @@ small.media-db-plugin-list-text {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.media-db-plugin-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-db-plugin-button:focus {
|
||||
/*outline: 1px solid white;*/
|
||||
}
|
||||
24
packages/obsidian/src/utils/AppError.ts
Normal file
24
packages/obsidian/src/utils/AppError.ts
Normal 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 };
|
||||
};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import type { TFolder } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from '../modals/MediaDbBulkImportModal';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import { ModalResultCode } from './ModalHelper';
|
||||
import { dateTimeToString, markdownTable } from './Utils';
|
||||
import type MediaDbPlugin from 'packages/obsidian/src/main';
|
||||
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { OutcomeStatus } from 'packages/obsidian/src/utils/result';
|
||||
import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
export enum BulkImportLookupMethod {
|
||||
ID = 'id',
|
||||
|
|
@ -50,7 +50,7 @@ export class BulkImportHelper {
|
|||
continue;
|
||||
}
|
||||
|
||||
const metadata = this.plugin.getMetadataFromFileCache(file);
|
||||
const metadata = this.plugin.fileHelper.getMetadataFromFileCache(file);
|
||||
const lookupValue = metadata[fieldName];
|
||||
|
||||
if (!lookupValue || typeof lookupValue !== 'string') {
|
||||
|
|
@ -81,60 +81,54 @@ export class BulkImportHelper {
|
|||
}
|
||||
|
||||
private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> {
|
||||
try {
|
||||
const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI);
|
||||
if (model) {
|
||||
await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined);
|
||||
const modelResult = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI);
|
||||
if (!modelResult.ok) {
|
||||
return { filePath: file.path, error: modelResult.error.userMessage ?? modelResult.error.message };
|
||||
}
|
||||
|
||||
if (modelResult.value) {
|
||||
await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined);
|
||||
return undefined;
|
||||
} else {
|
||||
}
|
||||
|
||||
return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` };
|
||||
}
|
||||
} catch (e) {
|
||||
return { filePath: file.path, error: `${e}` };
|
||||
}
|
||||
}
|
||||
|
||||
private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> {
|
||||
let results: MediaTypeModel[] = [];
|
||||
try {
|
||||
results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]);
|
||||
} catch (e) {
|
||||
return { filePath: file.path, error: `${e}` };
|
||||
const resultsResult = await this.plugin.apiManager.query(lookupValue, [selectedAPI]);
|
||||
if (!resultsResult.ok) {
|
||||
return { filePath: file.path, error: resultsResult.error.userMessage ?? resultsResult.error.message };
|
||||
}
|
||||
|
||||
const results: MediaTypeModel[] = resultsResult.value.items;
|
||||
if (!results || results.length === 0) {
|
||||
return { filePath: file.path, error: `no search results` };
|
||||
}
|
||||
|
||||
const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({
|
||||
const selectModalResult = await this.plugin.modalHelper.createSelectModalOutcome({
|
||||
elements: results,
|
||||
skipButton: true,
|
||||
modalTitle: `Results for '${lookupValue}'`,
|
||||
});
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.ERROR) {
|
||||
selectModal.close();
|
||||
return { filePath: file.path, error: selectModalResult.error.message };
|
||||
if (selectModalResult.status === OutcomeStatus.Error) {
|
||||
return { filePath: file.path, error: selectModalResult.error.userMessage ?? selectModalResult.error.message };
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.CLOSE) {
|
||||
selectModal.close();
|
||||
if (selectModalResult.status === OutcomeStatus.Cancelled) {
|
||||
return { filePath: file.path, error: 'user canceled', canceled: true };
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.SKIP) {
|
||||
selectModal.close();
|
||||
if (selectModalResult.status === OutcomeStatus.Skipped) {
|
||||
return { filePath: file.path, error: 'user skipped' };
|
||||
}
|
||||
|
||||
if (selectModalResult.data.selected.length === 0) {
|
||||
selectModal.close();
|
||||
return { filePath: file.path, error: `no search results selected` };
|
||||
}
|
||||
|
||||
const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected);
|
||||
await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
|
||||
|
||||
selectModal.close();
|
||||
const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected);
|
||||
await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import type momentType from 'moment';
|
||||
import type { Moment } from 'moment';
|
||||
import { moment as obsidianMoment } from 'obsidian';
|
||||
import { moment } from 'obsidian';
|
||||
|
||||
const moment: typeof momentType = obsidianMoment as unknown as typeof momentType;
|
||||
const momentFn = 'default' in moment ? moment.default : moment;
|
||||
|
||||
export class DateFormatter {
|
||||
private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
|
||||
|
|
@ -21,7 +19,7 @@ export class DateFormatter {
|
|||
}
|
||||
|
||||
getPreview(format?: string): string {
|
||||
const today = moment();
|
||||
const today = momentFn();
|
||||
|
||||
format ??= this.toFormat;
|
||||
|
||||
|
|
@ -44,32 +42,32 @@ export class DateFormatter {
|
|||
return null;
|
||||
}
|
||||
|
||||
let date: Moment;
|
||||
let date: moment.Moment;
|
||||
|
||||
if (!dateFormat) {
|
||||
date = this.parseWithoutFormat(dateString);
|
||||
} else {
|
||||
date = moment(dateString, dateFormat, locale);
|
||||
date = momentFn(dateString, dateFormat, locale);
|
||||
}
|
||||
|
||||
// format date (if it is valid)
|
||||
return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null;
|
||||
}
|
||||
|
||||
private parseWithoutFormat(dateString: string): Moment {
|
||||
private parseWithoutFormat(dateString: string): moment.Moment {
|
||||
// reading date formats other than C2822 or ISO with moment is deprecated
|
||||
// see https://momentjs.com/docs/#/parsing/string/
|
||||
if (this.hasMomentFormat(dateString)) {
|
||||
// expect C2822 or ISO format
|
||||
return moment(dateString);
|
||||
return momentFn(dateString);
|
||||
}
|
||||
|
||||
// fall back to native Date parsing for unknown formats
|
||||
return moment(new Date(dateString));
|
||||
return momentFn(new Date(dateString));
|
||||
}
|
||||
|
||||
private hasMomentFormat(dateString: string): boolean {
|
||||
const date = moment(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode
|
||||
const date = momentFn(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode
|
||||
return date.isValid();
|
||||
}
|
||||
}
|
||||
23
packages/obsidian/src/utils/ErrorReporter.ts
Normal file
23
packages/obsidian/src/utils/ErrorReporter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
packages/obsidian/src/utils/Logger.ts
Normal file
38
packages/obsidian/src/utils/Logger.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
289
packages/obsidian/src/utils/MediaDbEntryHelper.ts
Normal file
289
packages/obsidian/src/utils/MediaDbEntryHelper.ts
Normal 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]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
325
packages/obsidian/src/utils/MediaDbFileHelper.ts
Normal file
325
packages/obsidian/src/utils/MediaDbFileHelper.ts
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
import type { App, TFile } from 'obsidian';
|
||||
import type { App } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
import { TFolder } from 'obsidian';
|
||||
import { BoardGameModel } from '../models/BoardGameModel';
|
||||
import { BookModel } from '../models/BookModel';
|
||||
import { ComicMangaModel } from '../models/ComicMangaModel';
|
||||
import { GameModel } from '../models/GameModel';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import { MovieModel } from '../models/MovieModel';
|
||||
import { MusicReleaseModel } from '../models/MusicReleaseModel';
|
||||
import { SeasonModel } from '../models/SeasonModel';
|
||||
import { SeriesModel } from '../models/SeriesModel';
|
||||
import { WikiModel } from '../models/WikiModel';
|
||||
import type { MediaDbPluginSettings } from '../settings/Settings';
|
||||
import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList';
|
||||
import { MediaType } from './MediaType';
|
||||
import { replaceTags } from './Utils';
|
||||
import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel';
|
||||
import { BookModel } from 'packages/obsidian/src/models/BookModel';
|
||||
import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel';
|
||||
import { GameModel } from 'packages/obsidian/src/models/GameModel';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
import { MovieModel } from 'packages/obsidian/src/models/MovieModel';
|
||||
import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel';
|
||||
import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
|
||||
import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel';
|
||||
import { WikiModel } from 'packages/obsidian/src/models/WikiModel';
|
||||
import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings';
|
||||
import { ILLEGAL_FILENAME_CHARACTERS } from 'packages/obsidian/src/utils/IllegalFilenameCharactersList';
|
||||
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
|
||||
import { replaceTags } from 'packages/obsidian/src/utils/Utils';
|
||||
|
||||
// All media types in alphabetical order
|
||||
export const MEDIA_TYPES: MediaType[] = [
|
||||
|
|
@ -110,16 +111,16 @@ export class MediaTypeManager {
|
|||
}
|
||||
}
|
||||
|
||||
const template = await app.vault.cachedRead(templateFile as TFile);
|
||||
// console.log(template);
|
||||
if (!(templateFile instanceof TFile)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const template = await app.vault.cachedRead(templateFile);
|
||||
return replaceTags(template, mediaTypeModel);
|
||||
}
|
||||
|
||||
async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise<TFolder> {
|
||||
let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType());
|
||||
|
||||
folderPath ??= `/`;
|
||||
// console.log(folderPath);
|
||||
let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()) ?? '/';
|
||||
|
||||
if (!(await app.vault.adapter.exists(folderPath))) {
|
||||
await app.vault.createFolder(folderPath);
|
||||
282
packages/obsidian/src/utils/ModalHelper.ts
Normal file
282
packages/obsidian/src/utils/ModalHelper.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import { iso6392 } from 'iso-639-2';
|
||||
import type { TFile, TFolder, App } from 'obsidian';
|
||||
import { requestUrl } from 'obsidian';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
|
||||
|
||||
export const pluginName: string = 'obsidian-media-db-plugin';
|
||||
export const contactEmail: string = 'm.projects.code@gmail.com';
|
||||
export const mediaDbTag: string = 'mediaDB';
|
||||
export const mediaDbVersion: string = '0.5.2';
|
||||
export const debug: boolean = true;
|
||||
export const mediaDbVersion: string = '0.8.0';
|
||||
|
||||
export function wrapAround(value: number, size: number): number {
|
||||
if (size <= 0) {
|
||||
|
|
@ -153,10 +152,6 @@ export function markdownTable(content: string[][]): string {
|
|||
return table;
|
||||
}
|
||||
|
||||
export function fragWithHTML(html: string): DocumentFragment {
|
||||
return createFragment(frag => (frag.createDiv().innerHTML = html));
|
||||
}
|
||||
|
||||
export function dateToString(date: Date): string {
|
||||
return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`;
|
||||
}
|
||||
|
|
@ -240,7 +235,7 @@ function getTemplaterPlugin(app: App): TemplaterPlugin | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
return candidate as TemplaterPlugin;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function hasTemplaterPlugin(app: App): boolean {
|
||||
52
packages/obsidian/src/utils/result.ts
Normal file
52
packages/obsidian/src/utils/result.ts
Normal 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 });
|
||||
15
packages/schemas/src/fetchSchemas.sh
Normal file
15
packages/schemas/src/fetchSchemas.sh
Normal 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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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')}`;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue