update deps; make apis more type safe

This commit is contained in:
Moritz Jung 2025-08-08 16:29:18 +02:00
parent 9518ede41c
commit 292dcf5b49
46 changed files with 26911 additions and 550 deletions

View file

@ -0,0 +1,17 @@
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');
}
await fetchSchema();

BIN
bun.lockb

Binary file not shown.

View file

@ -7,7 +7,7 @@ import * as plugin_import from 'eslint-plugin-import';
export default tseslint.config(
{
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.svelte'],
ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.svelte', 'src/api/schemas/'],
},
{
files: ['src/**/*.ts'],

View file

@ -14,8 +14,8 @@
"lint": "eslint --max-warnings=0 src/**",
"lint:fix": "eslint --max-warnings=0 --fix src/**",
"svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"",
"check": "bun run format:check && bun run tsc && bun run test",
"check:fix": "bun run format && bun run tsc && bun run test",
"check": "bun run format:check && bun run tsc && bun run lint && bun run svelte-check",
"check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run svelte-check",
"release": "bun run automation/release.ts",
"stats": "bun run automation/stats.ts"
},
@ -23,26 +23,28 @@
"author": "Moritz Jung",
"license": "GPL-3.0",
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@happy-dom/global-registrator": "^18.0.1",
"@lemons_dev/parsinom": "^0.0.12",
"@happy-dom/global-registrator": "^14.12.3",
"@types/bun": "^1.1.16",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.2",
"@popperjs/core": "^2.11.8",
"@types/bun": "^1.2.19",
"builtin-modules": "^5.0.0",
"esbuild": "^0.25.8",
"esbuild-plugin-copy-watch": "^2.3.1",
"esbuild-svelte": "^0.8.2",
"eslint": "^9.18.0",
"eslint-plugin-import": "^2.31.0",
"esbuild-svelte": "^0.9.3",
"eslint": "^9.32.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-only-warn": "^1.1.0",
"obsidian": "latest",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"openapi-fetch": "^0.14.0",
"openapi-typescript": "^7.8.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"string-argv": "^0.3.2",
"svelte": "^5.17.5",
"svelte-check": "^4.1.4",
"svelte": "^5.38.0",
"svelte-check": "^4.3.1",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0"
}
}

View file

@ -5,6 +5,8 @@ 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;
@ -118,6 +120,6 @@ export class BoardGameGeekAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes;
}
}

View file

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
import { requestUrl } from 'obsidian';
import { ComicMangaModel } from 'src/models/ComicMangaModel';
import type MediaDbPlugin from '../../main';
@ -5,6 +7,8 @@ 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;
@ -41,7 +45,7 @@ export class ComicVineAPI extends APIModel {
year: result.start_year,
dataSource: this.apiName,
id: `4050-${result.id}`,
publishers: result.publisher?.name ?? [],
publishers: result.publisher?.name,
}),
);
}
@ -64,28 +68,32 @@ export class ComicVineAPI extends APIModel {
}
const data = await fetchData.json;
// console.debug(data);
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 ?? '',
year: result.start_year,
dataSource: this.apiName,
url: result.site_detail_url,
id: `4050-${result.id}`,
authors: result.people?.map((x: any) => x.name) ?? [],
authors: authors?.map(x => x.name),
chapters: result.count_of_issues,
image: result.image?.original_url ?? '',
image: result.image?.original_url,
released: true,
publishers: result.publisher?.name ?? [],
publishedFrom: result.start_year ?? 'unknown',
publishedTo: 'unknown',
publishers: result.publisher?.name,
publishedFrom: result.start_year,
status: result.status,
userData: {
@ -96,6 +104,6 @@ export class ComicVineAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.ComicVineAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.ComicVineAPI_disabledMediaTypes;
}
}

View file

@ -1,9 +1,11 @@
import { requestUrl } from 'obsidian';
import createClient from 'openapi-fetch';
import { obsidianFetch } from 'src/utils/Utils';
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';
import type { paths } from '../schemas/GiantBomb';
export class GiantBombAPI extends APIModel {
plugin: MediaDbPlugin;
@ -18,6 +20,7 @@ export class GiantBombAPI extends APIModel {
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`);
@ -25,35 +28,42 @@ export class GiantBombAPI extends APIModel {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `${this.apiUrl}/games?api_key=${this.plugin.settings.GiantBombKey}&filter=name:${encodeURIComponent(title)}&format=json`;
const fetchData = await requestUrl({
url: searchUrl,
const client = createClient<paths>({ baseUrl: 'https://www.giantbomb.com/api/' });
const response = await client.GET('/games', {
params: {
query: {
api_key: this.plugin.settings.GiantBombKey,
filter: `name:${encodeURIComponent(title)}`,
format: 'json',
limit: 20,
},
},
fetch: obsidianFetch,
});
// console.debug(fetchData);
if (fetchData.status === 401) {
if (response.response.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (fetchData.status === 429) {
if (response.response.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}.`);
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}
const data = await fetchData.json;
// console.debug(data);
const data = response.data?.results;
const ret: MediaTypeModel[] = [];
for (const result of data.results) {
for (const result of data ?? []) {
const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined;
ret.push(
new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: new Date(result.original_release_date).getFullYear().toString(),
year: year,
dataSource: this.apiName,
id: result.guid,
id: result.guid?.toString(),
}),
);
}
@ -68,36 +78,79 @@ export class GiantBombAPI extends APIModel {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `${this.apiUrl}/game/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.GiantBombKey}&format=json`;
const fetchData = await requestUrl({
url: searchUrl,
const client = createClient<paths>({ baseUrl: 'https://www.giantbomb.com/api/' });
const response = await client.GET('/game/{guid}', {
params: {
path: {
guid: id,
},
query: {
api_key: this.plugin.settings.GiantBombKey,
format: 'json',
},
},
fetch: obsidianFetch,
});
console.debug(fetchData);
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
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 = await fetchData.json;
// console.debug(data);
const result = data.results;
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: new Date(result.original_release_date).getFullYear().toString(),
year: year,
dataSource: this.apiName,
url: result.site_detail_url,
id: result.guid,
developers: result.developers?.map((x: any) => x.name) ?? [],
publishers: result.publishers?.map((x: any) => x.name) ?? [],
genres: result.genres?.map((x: any) => x.name) ?? [],
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: result.image?.super_url ?? '',
image: image?.super_url,
released: true,
releaseDate: result.original_release_date ?? 'unknown',
releaseDate: result.original_release_date,
userData: {
played: false,
@ -107,6 +160,6 @@ export class GiantBombAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.GiantBombAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.GiantBombAPI_disabledMediaTypes;
}
}

View file

@ -1,9 +1,12 @@
import createClient from 'openapi-fetch';
import { isTruthy, obsidianFetch } from 'src/utils/Utils';
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 { APIModel } from '../APIModel';
import type { paths } from '../schemas/MALAPI';
export class MALAPI extends APIModel {
plugin: MediaDbPlugin;
@ -28,30 +31,42 @@ export class MALAPI extends APIModel {
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const searchUrl = `https://api.jikan.moe/v4/anime?q=${encodeURIComponent(title)}&limit=20${this.plugin.settings.sfwFilter ? '&sfw' : ''}`;
const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' });
const fetchData = await fetch(searchUrl);
// console.debug(fetchData);
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
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 = await fetchData.json();
// console.debug(data);
const data = response.data?.data;
const ret: MediaTypeModel[] = [];
for (const result of data.data) {
const type = this.typeMappings.get(result.type?.toLowerCase());
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: result.year ?? result.aired?.prop?.from?.year ?? '',
year,
dataSource: this.apiName,
id: result.mal_id,
id,
}),
);
}
@ -61,9 +76,9 @@ export class MALAPI extends APIModel {
subType: type,
title: result.title,
englishTitle: result.title_english ?? result.title,
year: result.year ?? result.aired?.prop?.from?.year ?? '',
year,
dataSource: this.apiName,
id: result.mal_id,
id,
}),
);
} else if (type === 'series' || type === 'ova') {
@ -72,9 +87,9 @@ export class MALAPI extends APIModel {
subType: type,
title: result.title,
englishTitle: result.title_english ?? result.title,
year: result.year ?? result.aired?.prop?.from?.year ?? '',
year,
dataSource: this.apiName,
id: result.mal_id,
id,
}),
);
}
@ -86,41 +101,52 @@ export class MALAPI extends APIModel {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://api.jikan.moe/v4/anime/${encodeURIComponent(id)}/full`;
const fetchData = await fetch(searchUrl);
const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' });
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
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 data = await fetchData.json();
// console.debug(data);
const result = data.data;
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();
const type = this.typeMappings.get(result.type?.toLowerCase());
if (type === undefined) {
return new MovieModel({
subType: '',
subType: undefined,
title: result.title,
englishTitle: result.title_english ?? result.title,
year: result.year ?? result.aired?.prop?.from?.year ?? '',
year: year,
dataSource: this.apiName,
url: result.url,
id: result.mal_id,
id: new_id,
plot: result.synopsis,
genres: result.genres?.map((x: any) => x.name) ?? [],
director: [],
writer: [],
studio: result.studios?.map((x: any) => x.name).join(', ') ?? 'unknown',
duration: result.duration ?? 'unknown',
onlineRating: result.score ?? 0,
actors: [],
image: result.images?.jpg?.image_url ?? '',
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,
premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat) ?? 'unknown',
streamingServices: result.streaming?.map((x: any) => x.name) ?? [],
premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat),
streamingServices: result.streaming?.map(x => x.name).filter(isTruthy),
userData: {
watched: false,
@ -135,24 +161,21 @@ export class MALAPI extends APIModel {
subType: type,
title: result.title,
englishTitle: result.title_english ?? result.title,
year: result.year ?? result.aired?.prop?.from?.year ?? '',
year: year,
dataSource: this.apiName,
url: result.url,
id: result.mal_id,
id: new_id,
plot: result.synopsis,
genres: result.genres?.map((x: any) => x.name) ?? [],
director: [],
writer: [],
studio: result.studios?.map((x: any) => x.name).join(', ') ?? 'unknown',
duration: result.duration ?? 'unknown',
onlineRating: result.score ?? 0,
actors: [],
image: result.images?.jpg?.image_url ?? '',
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,
premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat) ?? 'unknown',
streamingServices: result.streaming?.map((x: any) => x.name) ?? [],
premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat),
streamingServices: result.streaming?.map(x => x.name).filter(isTruthy),
userData: {
watched: false,
@ -165,24 +188,23 @@ export class MALAPI extends APIModel {
subType: type,
title: result.title,
englishTitle: result.title_english ?? result.title,
year: result.year ?? result.aired?.prop?.from?.year ?? '',
year: year,
dataSource: this.apiName,
url: result.url,
id: result.mal_id,
id: new_id,
plot: result.synopsis,
genres: result.genres?.map((x: any) => x.name) ?? [],
writer: [],
studio: result.studios?.map((x: any) => x.name) ?? [],
genres: result.genres?.map(x => x.name).filter(isTruthy),
studio: result.studios?.map(x => x.name).filter(isTruthy),
episodes: result.episodes,
duration: result.duration ?? 'unknown',
onlineRating: result.score ?? 0,
streamingServices: result.streaming?.map((x: any) => x.name) ?? [],
image: result.images?.jpg?.image_url ?? '',
duration: result.duration,
onlineRating: result.score,
streamingServices: result.streaming?.map(x => x.name).filter(isTruthy),
image: result.images?.jpg?.image_url,
released: true,
airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat) ?? 'unknown',
airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat) ?? 'unknown',
airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat),
airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat),
airing: result.airing,
userData: {
@ -196,6 +218,6 @@ export class MALAPI extends APIModel {
throw new Error(`MDB | Unknown media type for id ${id}`);
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.MALAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.MALAPI_disabledMediaTypes;
}
}

View file

@ -1,12 +1,16 @@
import createClient from 'openapi-fetch';
import { isTruthy, obsidianFetch } from 'src/utils/Utils';
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';
import type { paths } from '../schemas/MALAPI';
export class MALAPIManga extends APIModel {
plugin: MediaDbPlugin;
typeMappings: Map<string, string>;
apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO
constructor(plugin: MediaDbPlugin) {
super();
@ -29,43 +33,55 @@ export class MALAPIManga extends APIModel {
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const searchUrl = `https://api.jikan.moe/v4/manga?q=${encodeURIComponent(title)}&limit=20${this.plugin.settings.sfwFilter ? '&sfw' : ''}`;
const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' });
const fetchData = await fetch(searchUrl);
// console.debug(fetchData);
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
const response = await client.GET('/manga', {
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 = await fetchData.json();
// console.debug(data);
const data = response.data?.data;
const ret: MediaTypeModel[] = [];
for (const result of data.data) {
const type = this.typeMappings.get(result.type?.toLowerCase());
for (const result of data ?? []) {
const resType = result.type?.toLowerCase();
const type = resType ? this.typeMappings.get(resType) : undefined;
const year = result.published?.prop?.from?.year?.toString() ?? '';
const id = result.mal_id?.toString();
ret.push(
new ComicMangaModel({
subType: type,
title: result.title,
plot: result.synopsis,
plot: result.synopsis ?? undefined,
englishTitle: result.title_english ?? result.title,
alternateTitles: result.titles?.map((x: any) => x.title) ?? [],
year: result.year ?? result.published?.prop?.from?.year ?? '',
alternateTitles: result.titles?.map(x => x.title).filter(isTruthy),
year: year,
dataSource: this.apiName,
url: result.url,
id: result.mal_id,
id: id,
genres: result.genres?.map((x: any) => x.name) ?? [],
authors: result.authors?.map((x: any) => x.name) ?? [],
genres: result.genres?.map(x => x.name).filter(isTruthy),
authors: result.authors?.map(x => x.name).filter(isTruthy),
chapters: result.chapters,
volumes: result.volumes,
onlineRating: result.score ?? 0,
image: result.images?.jpg?.image_url ?? '',
onlineRating: result.score,
image: result.images?.jpg?.image_url,
released: true,
publishedFrom: new Date(result.published?.from).toLocaleDateString() ?? 'unknown',
publishedTo: new Date(result.published?.to).toLocaleDateString() ?? 'unknown',
publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat),
publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat),
status: result.status,
userData: {
@ -83,40 +99,53 @@ export class MALAPIManga extends APIModel {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://api.jikan.moe/v4/manga/${encodeURIComponent(id)}/full`;
const fetchData = await fetch(searchUrl);
const client = createClient<paths>({ baseUrl: 'https://api.jikan.moe/v4/' });
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
const response = await client.GET('/manga/{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 data = await fetchData.json();
// console.debug(data);
const result = data.data;
const result = response.data?.data;
if (!result) {
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.published?.prop?.from?.year?.toString() ?? '';
const new_id = result.mal_id?.toString();
const type = this.typeMappings.get(result.type?.toLowerCase());
return new ComicMangaModel({
subType: type,
title: result.title,
plot: result.synopsis ?? undefined,
englishTitle: result.title_english ?? result.title,
alternateTitles: result.titles?.map((x: any) => x.title) ?? [],
year: result.year ?? result.published?.prop?.from?.year ?? '',
alternateTitles: result.titles?.map(x => x.title).filter(isTruthy),
year: year,
dataSource: this.apiName,
url: result.url,
id: result.mal_id,
id: new_id,
plot: (result.synopsis ?? 'unknown').replace(/"/g, "'") ?? 'unknown',
genres: result.genres?.map((x: any) => x.name) ?? [],
authors: result.authors?.map((x: any) => x.name) ?? [],
genres: result.genres?.map(x => x.name).filter(isTruthy),
authors: result.authors?.map(x => x.name).filter(isTruthy),
chapters: result.chapters,
volumes: result.volumes,
onlineRating: result.score ?? 0,
image: result.images?.jpg?.image_url ?? '',
onlineRating: result.score,
image: result.images?.jpg?.image_url,
released: true,
publishers: result.serializations?.map((x: any) => x.name) ?? [],
publishedFrom: new Date(result.published?.from).toLocaleDateString() ?? 'unknown',
publishedTo: new Date(result.published?.to).toLocaleDateString() ?? 'unknown',
publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat),
publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat),
status: result.status,
userData: {
@ -127,6 +156,6 @@ export class MALAPIManga extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.MALAPIManga_disabledMediaTypes as MediaType[];
return this.plugin.settings.MALAPIManga_disabledMediaTypes;
}
}

View file

@ -1,4 +1,5 @@
import { Notice } from 'obsidian';
/* 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';
@ -6,6 +7,10 @@ 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';
@ -19,6 +24,7 @@ export class MobyGamesAPI extends APIModel {
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`);
@ -108,6 +114,6 @@ export class MobyGamesAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.MobyGamesAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.MobyGamesAPI_disabledMediaTypes;
}
}

View file

@ -6,6 +6,73 @@ import { MediaType } from '../../utils/MediaType';
import { contactEmail, 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 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': {
name: string;
artist: {
id: string;
name: string;
'short-name': string;
};
}[];
releases: Release[];
tags: Tag[];
}
interface IdResponse {
id: string;
tags: Tag[];
'primary-type-id': string;
'artist-credit': {
name: string;
artist: {
tags: Tag[];
type: string;
id: string;
name: string;
'short-name': string;
country: string;
};
}[];
title: string;
genres: Genre[];
'first-release-date': string;
releases: Release[];
'primary-type': string;
rating: {
value: number;
'votes-count': number;
};
}
export class MusicBrainzAPI extends APIModel {
plugin: MediaDbPlugin;
@ -37,7 +104,9 @@ export class MusicBrainzAPI extends APIModel {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const data = await fetchData.json;
const data = (await fetchData.json) as {
'release-groups': SearchResponse[];
};
// console.debug(data);
const ret: MediaTypeModel[] = [];
@ -53,7 +122,7 @@ export class MusicBrainzAPI extends APIModel {
id: result.id,
image: 'https://coverartarchive.org/release-group/' + result.id + '/front',
artists: result['artist-credit'].map((a: any) => a.name),
artists: result['artist-credit'].map(a => a.name),
subType: result['primary-type'],
}),
);
@ -77,7 +146,7 @@ export class MusicBrainzAPI extends APIModel {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const result = await fetchData.json;
const result = (await fetchData.json) as IdResponse;
return new MusicReleaseModel({
type: 'musicRelease',
@ -89,8 +158,8 @@ export class MusicBrainzAPI extends APIModel {
id: result.id,
image: 'https://coverartarchive.org/release-group/' + result.id + '/front',
artists: result['artist-credit'].map((a: any) => a.name),
genres: result.genres.map((g: any) => g.name),
artists: result['artist-credit'].map(a => a.name),
genres: result.genres.map(g => g.name),
subType: result['primary-type'],
rating: result.rating.value * 2,
@ -100,6 +169,6 @@ export class MusicBrainzAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes;
}
}

View file

@ -1,4 +1,5 @@
import { Notice } from 'obsidian';
import createClient from 'openapi-fetch';
import { obsidianFetch } from 'src/utils/Utils';
import type MediaDbPlugin from '../../main';
import { GameModel } from '../../models/GameModel';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
@ -6,6 +7,56 @@ import { MovieModel } from '../../models/MovieModel';
import { SeriesModel } from '../../models/SeriesModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
import type { paths } from '../schemas/OMDb';
type SearchResponse =
| {
Response: 'True';
totalResults: string;
Search: {
Title: string;
Year: string;
Poster: string;
imdbID: string;
Type: string;
}[];
}
| {
Response: 'False';
Error: string;
};
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;
}
| {
Response: 'False';
Error: string;
};
export class OMDbAPI extends APIModel {
plugin: MediaDbPlugin;
@ -33,24 +84,37 @@ export class OMDbAPI extends APIModel {
throw new Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`;
const fetchData = await fetch(searchUrl);
const client = createClient<paths>({ baseUrl: 'https://www.omdbapi.com/' });
if (fetchData.status === 401) {
const response = await client.GET('/?s', {
params: {
query: {
s: title,
apikey: this.plugin.settings.OMDbKey,
},
},
fetch: obsidianFetch,
});
if (response.response.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}.`);
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}
const data = await fetchData.json();
const data = response.data 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}: \n${JSON.stringify(data, undefined, 4)}`);
throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`);
}
if (!data.Search) {
return [];
@ -111,18 +175,30 @@ export class OMDbAPI extends APIModel {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const searchUrl = `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${this.plugin.settings.OMDbKey}`;
const fetchData = await fetch(searchUrl);
const client = createClient<paths>({ baseUrl: 'https://www.omdbapi.com/' });
if (fetchData.status === 401) {
const response = await client.GET('/?i', {
params: {
query: {
i: id,
apikey: this.plugin.settings.OMDbKey,
},
},
fetch: obsidianFetch,
});
if (response.response.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}.`);
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}
const result = await fetchData.json();
// console.debug(result);
const result = response.data 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}`);
@ -130,7 +206,7 @@ export class OMDbAPI extends APIModel {
const type = this.typeMappings.get(result.Type.toLowerCase());
if (type === undefined) {
throw Error(`${result.type.toLowerCase()} is an unsupported type.`);
throw Error(`${result.Type.toLowerCase()} is an unsupported type.`);
}
if (type === 'movie') {
@ -143,19 +219,17 @@ export class OMDbAPI extends APIModel {
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(', ') ?? [],
studio: ['N/A'],
duration: result.Runtime ?? 'unknown',
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 ? result.Poster.replace('_SX300', '_SX600') : '',
actors: result.Actors?.split(', '),
image: result.Poster.replace('_SX300', '_SX600'),
released: true,
streamingServices: [],
premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat) ?? 'unknown',
premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat),
userData: {
watched: false,
@ -173,21 +247,18 @@ export class OMDbAPI extends APIModel {
url: `https://www.imdb.com/title/${result.imdbID}/`,
id: result.imdbID,
plot: result.Plot ?? '',
genres: result.Genre?.split(', ') ?? [],
writer: result.Writer?.split(', ') ?? [],
plot: result.Plot,
genres: result.Genre?.split(', '),
writer: result.Writer?.split(', '),
studio: [],
episodes: 0,
duration: result.Runtime ?? 'unknown',
duration: result.Runtime,
onlineRating: Number.parseFloat(result.imdbRating ?? 0),
actors: result.Actors?.split(', ') ?? [],
image: result.Poster ? result.Poster.replace('_SX300', '_SX600') : '',
actors: result.Actors?.split(', '),
image: result.Poster.replace('_SX300', '_SX600'),
released: true,
streamingServices: [],
airing: false,
airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat) ?? 'unknown',
airedTo: 'unknown',
airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat),
userData: {
watched: false,
@ -205,14 +276,12 @@ export class OMDbAPI extends APIModel {
url: `https://www.imdb.com/title/${result.imdbID}/`,
id: result.imdbID,
developers: [],
publishers: [],
genres: result.Genre?.split(', ') ?? [],
genres: result.Genre?.split(', '),
onlineRating: Number.parseFloat(result.imdbRating ?? 0),
image: result.Poster ? result.Poster.replace('_SX300', '_SX600') : '',
image: result.Poster.replace('_SX300', '_SX600'),
released: true,
releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat) ?? 'unknown',
releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat),
userData: {
played: false,
@ -225,6 +294,6 @@ export class OMDbAPI extends APIModel {
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.OMDbAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.OMDbAPI_disabledMediaTypes;
}
}

View file

@ -1,8 +1,26 @@
import createClient from 'openapi-fetch';
import { BookModel } from 'src/models/BookModel';
import { obsidianFetch } from 'src/utils/Utils';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
import type { paths } from '../schemas/OpenLibrary';
interface SearchResponse {
cover_i: number;
has_fulltext: boolean;
edition_count: number;
title: string;
author_name: string[];
first_publish_year: number;
key: string;
number_of_pages_median?: number;
cover_edition_key?: string;
isbn?: string[];
ratings_average?: number;
}
export class OpenLibraryAPI extends APIModel {
plugin: MediaDbPlugin;
@ -20,14 +38,24 @@ export class OpenLibraryAPI extends APIModel {
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const searchUrl = `https://openlibrary.org/search.json?title=${encodeURIComponent(title)}`;
const client = createClient<paths>({ baseUrl: 'https://openlibrary.org/' });
const fetchData = await fetch(searchUrl);
// console.debug(fetchData);
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
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 = await fetchData.json();
const data = response.data as {
docs: SearchResponse[];
};
// console.debug(data);
@ -37,11 +65,11 @@ export class OpenLibraryAPI extends APIModel {
ret.push(
new BookModel({
title: result.title,
englishTitle: result.title_english ?? result.title,
year: result.first_publish_year,
englishTitle: result.title,
year: result.first_publish_year.toString(),
dataSource: this.apiName,
id: result.key,
author: result.author_name ?? 'unknown',
author: result.author_name.join(', '),
}),
);
}
@ -52,33 +80,47 @@ export class OpenLibraryAPI extends APIModel {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://openlibrary.org/search.json?q=key:${encodeURIComponent(id)}`;
const fetchData = await fetch(searchUrl);
// console.debug(fetchData);
const client = createClient<paths>({ baseUrl: 'https://openlibrary.org/' });
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
const response = await client.GET('/search.json', {
params: {
query: {
q: `key:${id}`,
fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover_edition_key',
},
},
fetch: obsidianFetch,
});
if (response.error !== undefined) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}
const data = await fetchData.json();
const data = response.data as {
docs: SearchResponse[];
};
// console.debug(data);
const result = data.docs[0];
const pages = Number(result.number_of_pages_median);
const isbn = Number((result.isbn ?? []).find((el: string) => el.length <= 10));
const isbn13 = Number((result.isbn ?? []).find((el: string) => el.length == 13));
return new BookModel({
title: result.title,
year: result.first_publish_year,
year: result.first_publish_year.toString(),
dataSource: this.apiName,
url: `https://openlibrary.org` + result.key,
id: result.key,
isbn: (result.isbn ?? []).find((el: string | any[]) => el.length <= 10) ?? 'unknown',
isbn13: (result.isbn ?? []).find((el: string | any[]) => el.length == 13) ?? 'unknown',
englishTitle: result.title_english ?? result.title,
isbn: Number.isNaN(isbn) ? undefined : isbn,
isbn13: Number.isNaN(isbn13) ? undefined : isbn13,
englishTitle: result.title,
author: result.author_name ?? 'unknown',
plot: result.description ?? 'unknown',
pages: result.number_of_pages_median ?? 'unknown',
onlineRating: Number.parseFloat(Number(result.ratings_average ?? 0).toFixed(2)),
image: `https://covers.openlibrary.org/b/OLID/` + result.cover_edition_key + `-L.jpg`,
author: result.author_name.join(', '),
pages: Number.isNaN(pages) ? undefined : pages,
onlineRating: result.ratings_average,
image: result.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/` + result.cover_edition_key + `-L.jpg` : undefined,
released: true,
@ -90,6 +132,6 @@ export class OpenLibraryAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes;
}
}

View file

@ -3,8 +3,128 @@ 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';
import { imageUrlExists } from '../../utils/Utils';
import { APIModel } from '../APIModel';
interface SearchResponse {
appid: string;
name: string;
icon: string;
logo: string;
}
type IdResponse = Record<
string,
{
success: boolean;
data: GameDetails;
}
>;
interface GameDetails {
type: string;
name: string;
steam_appid: number;
required_age: string;
is_free: boolean;
controller_support: string;
dlc: number[];
detailed_description: string;
about_the_game: string;
short_description: string;
supported_languages: string;
reviews: string;
header_image: string;
capsule_image: string;
capsule_imagev5: string;
website: string;
pc_requirements: Requirements;
mac_requirements: Requirements;
linux_requirements: Requirements;
legal_notice: string;
drm_notice: string;
developers: string[];
publishers: string[];
price_overview: PriceOverview;
packages: number[];
platforms: Platforms;
metacritic?: {
score: number;
url: string;
};
categories: Category[];
genres: Genre[];
recommendations: {
total: number;
};
achievements: {
total: number;
highlighted: Achievement[];
};
release_date: {
coming_soon: boolean;
date: string;
};
support_info: {
url: string;
email: string;
};
background: string;
background_raw: string;
content_descriptors: {
ids: number[];
notes: string;
};
ratings: Ratings;
}
interface Requirements {
minimum: string;
recommended: string;
}
interface PriceOverview {
currency: string;
initial: number;
final: number;
discount_percent: number;
initial_formatted: string;
final_formatted: string;
}
interface Platforms {
windows: boolean;
mac: boolean;
linux: boolean;
}
interface Category {
id: number;
description: string;
}
interface Genre {
id: string;
description: string;
}
interface Achievement {
name: string;
path: string;
}
type Ratings = Record<
string,
{
rating: string;
descriptors: string;
use_age_gate: string;
required_age: string;
rating_id?: string;
banned?: string;
rating_generated?: string;
}
>;
export class SteamAPI extends APIModel {
plugin: MediaDbPlugin;
@ -35,7 +155,7 @@ export class SteamAPI extends APIModel {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const data = await fetchData.json;
const data = (await fetchData.json) as SearchResponse[];
// console.debug(data);
@ -70,14 +190,13 @@ export class SteamAPI extends APIModel {
}
// console.debug(await fetchData.json);
const data = (await fetchData.json) as IdResponse;
let result: any;
for (const [key, value] of Object.entries(await fetchData.json)) {
// console.log(typeof key, key)
// console.log(typeof id, id)
let result: GameDetails | undefined = undefined;
for (const [key, value] of Object.entries(data)) {
// after some testing I found out that id is somehow a number despite that it's defined as string...
if (key === String(id)) {
result = (value as any).data;
result = value.data;
}
}
if (!result) {
@ -103,16 +222,16 @@ export class SteamAPI extends APIModel {
year: new Date(result.release_date.date).getFullYear().toString(),
dataSource: this.apiName,
url: `https://store.steampowered.com/app/${result.steam_appid}`,
id: result.steam_appid,
id: result.steam_appid.toString(),
developers: result.developers,
publishers: result.publishers,
genres: result.genres?.map((x: any) => x.description) ?? [],
onlineRating: Number.parseFloat(result.metacritic?.score ?? 0),
image: finalimageurl ?? '',
genres: result.genres?.map(x => x.description),
onlineRating: result.metacritic?.score,
image: finalimageurl,
released: !result.release_date?.coming_soon,
releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat) ?? 'unknown',
releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat),
userData: {
played: false,
@ -121,6 +240,6 @@ export class SteamAPI extends APIModel {
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.SteamAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.SteamAPI_disabledMediaTypes;
}
}

View file

@ -4,6 +4,35 @@ import { WikiModel } from '../../models/WikiModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
interface SearchResponse {
query: {
search: {
title: string;
pageid: number;
}[];
};
}
interface IdResponse {
query: {
pages: Record<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
@ -29,7 +58,7 @@ export class WikipediaAPI extends APIModel {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const data = await fetchData.json();
const data = (await fetchData.json()) as SearchResponse;
console.debug(data);
const ret: MediaTypeModel[] = [];
@ -41,7 +70,7 @@ export class WikipediaAPI extends APIModel {
englishTitle: result.title,
year: '',
dataSource: this.apiName,
id: result.pageid,
id: result.pageid.toString(),
}),
);
}
@ -59,27 +88,25 @@ export class WikipediaAPI extends APIModel {
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
}
const data = await fetchData.json();
const data = (await fetchData.json()) as IdResponse;
// console.debug(data);
const result: any = Object.entries(data?.query?.pages)[0][1];
const result = Object.values(data?.query?.pages)[0];
return new WikiModel({
type: 'wiki',
title: result.title,
englishTitle: result.title,
year: '',
dataSource: this.apiName,
url: result.fullurl,
id: result.pageid,
id: result.pageid.toString(),
wikiUrl: result.fullurl,
lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat) ?? undefined,
lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat),
length: result.length,
userData: {},
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.WikipediaAPI_disabledMediaTypes as MediaType[];
return this.plugin.settings.WikipediaAPI_disabledMediaTypes;
}
}

11226
src/api/schemas/GiantBomb.json Normal file

File diff suppressed because it is too large Load diff

6454
src/api/schemas/GiantBomb.ts Normal file

File diff suppressed because it is too large Load diff

6761
src/api/schemas/MALAPI.ts Normal file

File diff suppressed because it is too large Load diff

269
src/api/schemas/OMDb.json Normal file
View file

@ -0,0 +1,269 @@
{
"openapi": "3.0.1",
"info": {
"title": "OMDb API",
"description": "This API requires authorization, you can get a free key here: [http://omdbapi.com/apikey.aspx](http://omdbapi.com/apikey.aspx)",
"termsOfService": "http://omdbapi.com/legal.htm",
"contact": {
"email": "bfritz@fadingsignal.com"
},
"license": {
"name": "CC BY-NC 4.0",
"url": "https://creativecommons.org/licenses/by-nc/4.0/"
},
"version": "1.0"
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
},
"servers": [
{
"url": "http://omdbapi.com/"
},
{
"url": "https://omdbapi.com/"
}
],
"security": [
{
"APIKeyQueryParam": []
}
],
"tags": [
{
"name": "Title Parameter",
"description": "e.g. ?t=title"
},
{
"name": "ID Parameter",
"description": "e.g. ?i=tt0000001"
},
{
"name": "Search Parameter",
"description": "e.g. ?s=title"
}
],
"paths": {
"/?t": {
"get": {
"tags": ["Title Parameter"],
"summary": "Returns the most popular match for a given title",
"operationId": "getTitle",
"parameters": [
{
"name": "t",
"in": "query",
"description": "Title of movie or series",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "y",
"in": "query",
"description": "Year of release",
"schema": {
"type": "integer"
}
},
{
"name": "type",
"in": "query",
"description": "Return movie or series",
"schema": {
"type": "string",
"enum": ["movie", "series"]
}
},
{
"name": "plot",
"in": "query",
"description": "Return short or full plot",
"schema": {
"type": "string",
"enum": ["short", "full"]
}
},
{
"name": "r",
"in": "query",
"description": "The response type to return",
"schema": {
"type": "string",
"enum": ["json", "xml"]
}
},
{
"name": "callback",
"in": "query",
"description": "JSONP callback name",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {}
},
"401": {
"description": "Not authenticated",
"content": {}
}
},
"security": [
{
"APIKeyQueryParam": []
}
]
}
},
"/?i": {
"get": {
"tags": ["ID Parameter"],
"summary": "Returns a single result based on the ID provided",
"operationId": "getId",
"parameters": [
{
"name": "i",
"in": "query",
"description": "A valid IMDb ID (e.g. tt0000001)",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "plot",
"in": "query",
"description": "Return short or full plot",
"schema": {
"type": "string",
"enum": ["short", "full"]
}
},
{
"name": "r",
"in": "query",
"description": "The response type to return",
"schema": {
"type": "string",
"enum": ["json", "xml"]
}
},
{
"name": "callback",
"in": "query",
"description": "JSONP callback name",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {}
},
"401": {
"description": "Not authenticated",
"content": {}
}
},
"security": [
{
"APIKeyQueryParam": []
}
]
}
},
"/?s": {
"get": {
"tags": ["Search Parameter"],
"summary": "Returns an array of results for a given title",
"operationId": "titleSearch",
"parameters": [
{
"name": "s",
"in": "query",
"description": "Title of movie or series",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "y",
"in": "query",
"description": "Year of release",
"schema": {
"type": "integer"
}
},
{
"name": "type",
"in": "query",
"description": "Return movie or series",
"schema": {
"type": "string",
"enum": ["movie", "series"]
}
},
{
"name": "r",
"in": "query",
"description": "The response type to return",
"schema": {
"type": "string",
"enum": ["json", "xml"]
}
},
{
"name": "page",
"in": "query",
"description": "Page number to return",
"schema": {
"type": "integer"
}
},
{
"name": "callback",
"in": "query",
"description": "JSONP callback name",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {}
},
"401": {
"description": "Not authenticated",
"content": {}
}
},
"security": [
{
"APIKeyQueryParam": []
}
]
}
}
},
"components": {
"securitySchemes": {
"APIKeyQueryParam": {
"type": "apiKey",
"name": "apikey",
"in": "query"
}
}
},
"x-original-swagger-version": "2.0"
}

180
src/api/schemas/OMDb.ts Normal file
View file

@ -0,0 +1,180 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
'/?t': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Returns the most popular match for a given title */
get: operations['getTitle'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/?i': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Returns a single result based on the ID provided */
get: operations['getId'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/?s': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Returns an array of results for a given title */
get: operations['titleSearch'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: never;
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
getTitle: {
parameters: {
query: {
/** @description Title of movie or series */
t: string;
/** @description Year of release */
y?: number;
/** @description Return movie or series */
type?: 'movie' | 'series';
/** @description Return short or full plot */
plot?: 'short' | 'full';
/** @description The response type to return */
r?: 'json' | 'xml';
/** @description JSONP callback name */
callback?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful operation */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not authenticated */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getId: {
parameters: {
query: {
/** @description A valid IMDb ID (e.g. tt0000001) */
i: string;
/** @description Return short or full plot */
plot?: 'short' | 'full';
/** @description The response type to return */
r?: 'json' | 'xml';
/** @description JSONP callback name */
callback?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful operation */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not authenticated */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
titleSearch: {
parameters: {
query: {
/** @description Title of movie or series */
s: string;
/** @description Year of release */
y?: number;
/** @description Return movie or series */
type?: 'movie' | 'series';
/** @description The response type to return */
r?: 'json' | 'xml';
/** @description Page number to return */
page?: number;
/** @description JSONP callback name */
callback?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful operation */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not authenticated */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
}

View file

@ -0,0 +1,602 @@
{
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"title": "Detail",
"type": "array"
}
},
"title": "HTTPValidationError",
"type": "object"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"type": "string"
},
"title": "Location",
"type": "array"
},
"msg": {
"title": "Message",
"type": "string"
},
"type": {
"title": "Error Type",
"type": "string"
}
},
"required": ["loc", "msg", "type"],
"title": "ValidationError",
"type": "object"
}
}
},
"info": {
"description": "- These are still in development and may not be perfect\n- Contribute by proposing edits to [openapi.json](https://github.com/internetarchive/openlibrary/blob/master/static/openapi.json)\n- Please do not use our APIs for bulk downloads, see [dev center](https://openlibrary.org/developers/api)",
"title": "Open Library API",
"version": "0.1.0"
},
"openapi": "3.0.2",
"paths": {
"/api/books": {
"get": {
"operationId": "read_api_books_api_books_get",
"parameters": [
{
"examples": {
"isbn": {
"value": "ISBN:0201558025"
},
"multiple": {
"value": "ISBN:9781408113479,OCLC:420517"
},
"oclc": {
"value": "OCLC:263296519"
}
},
"in": "query",
"name": "bibkeys",
"required": true,
"schema": {
"title": "Bibkeys",
"type": "string"
}
},
{
"description": "Specifies the response format. Possible values are json and javascript. When not specified the format is javascript.",
"in": "query",
"name": "format",
"required": false,
"schema": {
"default": "json",
"title": "Format",
"type": "string"
}
},
{
"description": "The name of the JavaScript function to call with the result. This is considered only when the format is javascript.",
"in": "query",
"name": "callback",
"required": false,
"schema": {
"title": "Callback"
}
},
{
"description": "Decides what information to provide for each matched bib_key. Possible values are viewapi and data. The default value is viewapi.",
"in": "query",
"name": "jscmd",
"required": false,
"schema": {
"default": "viewapi",
"title": "Jscmd",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Api Books",
"tags": ["books"]
}
},
"/api/volumes/brief/{key_type}/{value}.json": {
"get": {
"operationId": "read_api_volumes_brief_api_volumes_brief__key_type___value__json_get",
"parameters": [
{
"in": "path",
"name": "key_type",
"required": true,
"schema": {
"title": "Key Type"
}
},
{
"in": "path",
"name": "value",
"required": true,
"schema": {
"title": "Value"
}
},
{
"in": "query",
"name": "callback",
"required": false,
"schema": {
"title": "Callback"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Api Volumes Brief",
"tags": ["books"]
}
},
"/authors/{olid}.json": {
"get": {
"operationId": "read_authors_authors__olid__json_get",
"parameters": [
{
"in": "path",
"name": "olid",
"required": true,
"schema": {
"title": "Olid"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Authors",
"tags": ["authors"]
}
},
"/authors/{olid}/works.json": {
"get": {
"operationId": "read_authors_works_authors__olid__works_json_get",
"parameters": [
{
"in": "path",
"name": "olid",
"required": true,
"schema": {
"title": "Olid"
}
},
{
"in": "query",
"name": "limit",
"required": false,
"schema": {
"title": "Limit",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Authors Works",
"tags": ["authors"]
}
},
"/books/{olid}": {
"get": {
"operationId": "read_books_books__olid__get",
"parameters": [
{
"in": "path",
"name": "olid",
"required": true,
"schema": {
"example": "OL53924W",
"title": "Olid"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Books",
"tags": ["books"]
}
},
"/covers/{key_type}/{value}-{size}.jpg": {
"get": {
"operationId": "read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get",
"parameters": [
{
"in": "path",
"name": "key_type",
"required": true,
"schema": {
"title": "Key Type"
}
},
{
"in": "path",
"name": "value",
"required": true,
"schema": {
"title": "Value"
}
},
{
"in": "path",
"name": "size",
"required": true,
"schema": {
"title": "Size"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Covers Key Type Value Size Jpeg",
"tags": ["covers"]
}
},
"/isbn/{isbn}": {
"get": {
"operationId": "read_isbn_isbn__isbn__get",
"parameters": [
{
"in": "path",
"name": "isbn",
"required": true,
"schema": {
"title": "Isbn"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Isbn",
"tags": ["books"]
}
},
"/search.json": {
"get": {
"operationId": "read_search_json_search_json_get",
"parameters": [
{
"in": "query",
"name": "q",
"required": true,
"schema": {
"title": "Q"
}
},
{
"in": "query",
"name": "page",
"required": false,
"schema": {
"title": "Page",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Search Json",
"tags": ["search"]
}
},
"/search/authors.json": {
"get": {
"operationId": "read_search_authors_json_search_authors_json_get",
"parameters": [
{
"in": "query",
"name": "q",
"required": true,
"schema": {
"title": "Q"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Search Authors Json",
"tags": ["search"]
}
},
"/subjects/{subject}.json": {
"get": {
"operationId": "read_subjects_subjects__subject__json_get",
"parameters": [
{
"in": "path",
"name": "subject",
"required": true,
"schema": {
"title": "Subject"
}
},
{
"in": "query",
"name": "details",
"required": false,
"schema": {
"default": false,
"title": "Details",
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Subjects",
"tags": ["subjects"]
}
},
"/works/{olid}": {
"get": {
"operationId": "read_works_works__olid__get",
"parameters": [
{
"in": "path",
"name": "olid",
"required": true,
"schema": {
"title": "Olid"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Read Works",
"tags": ["books"]
}
}
},
"tags": [
{
"description": "Retrieve a specific work or edition by identifier",
"externalDocs": {
"description": "Find out more",
"url": "https://openlibrary.org/dev/docs/api/books"
},
"name": "books"
},
{
"description": "Retrieve an author and their works by author identifier",
"externalDocs": {
"description": "Find out more",
"url": "https://openlibrary.org/dev/docs/api/authors"
},
"name": "authors"
},
{
"description": "Search results for books, authors, and more",
"externalDocs": {
"description": "Find out more",
"url": "https://openlibrary.org/dev/docs/api/search"
},
"name": "search"
},
{
"description": "Fetch book covers by ISBN or Open Library identifier",
"externalDocs": {
"description": "Find out more",
"url": "https://openlibrary.org/dev/docs/api/covers"
},
"name": "covers"
},
{
"description": "Fetch books by subject name ",
"externalDocs": {
"description": "Find out more",
"url": "https://openlibrary.org/dev/docs/api/subjects"
},
"name": "subjects"
}
]
}

View file

@ -0,0 +1,578 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
'/api/books': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Api Books */
get: operations['read_api_books_api_books_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/api/volumes/brief/{key_type}/{value}.json': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Api Volumes Brief */
get: operations['read_api_volumes_brief_api_volumes_brief__key_type___value__json_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/authors/{olid}.json': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Authors */
get: operations['read_authors_authors__olid__json_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/authors/{olid}/works.json': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Authors Works */
get: operations['read_authors_works_authors__olid__works_json_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/books/{olid}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Books */
get: operations['read_books_books__olid__get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/covers/{key_type}/{value}-{size}.jpg': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Covers Key Type Value Size Jpeg */
get: operations['read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/isbn/{isbn}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Isbn */
get: operations['read_isbn_isbn__isbn__get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/search.json': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Search Json */
get: operations['read_search_json_search_json_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/search/authors.json': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Search Authors Json */
get: operations['read_search_authors_json_search_authors_json_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/subjects/{subject}.json': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Subjects */
get: operations['read_subjects_subjects__subject__json_get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/works/{olid}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Read Works */
get: operations['read_works_works__olid__get'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
detail?: components['schemas']['ValidationError'][];
};
/** ValidationError */
ValidationError: {
/** Location */
loc: string[];
/** Message */
msg: string;
/** Error Type */
type: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
read_api_books_api_books_get: {
parameters: {
query: {
bibkeys: string;
/** @description Specifies the response format. Possible values are json and javascript. When not specified the format is javascript. */
format?: string;
/** @description The name of the JavaScript function to call with the result. This is considered only when the format is javascript. */
callback?: unknown;
/** @description Decides what information to provide for each matched bib_key. Possible values are viewapi and data. The default value is viewapi. */
jscmd?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_api_volumes_brief_api_volumes_brief__key_type___value__json_get: {
parameters: {
query?: {
callback?: unknown;
};
header?: never;
path: {
key_type: unknown;
value: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_authors_authors__olid__json_get: {
parameters: {
query?: never;
header?: never;
path: {
olid: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_authors_works_authors__olid__works_json_get: {
parameters: {
query?: {
limit?: number;
};
header?: never;
path: {
olid: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_books_books__olid__get: {
parameters: {
query?: never;
header?: never;
path: {
olid: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get: {
parameters: {
query?: never;
header?: never;
path: {
key_type: unknown;
value: unknown;
size: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_isbn_isbn__isbn__get: {
parameters: {
query?: never;
header?: never;
path: {
isbn: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_search_json_search_json_get: {
parameters: {
query: {
q: unknown;
page?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_search_authors_json_search_authors_json_get: {
parameters: {
query: {
q: unknown;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_subjects_subjects__subject__json_get: {
parameters: {
query?: {
details?: boolean;
};
header?: never;
path: {
subject: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
read_works_works__olid__get: {
parameters: {
query?: never;
header?: never;
path: {
olid: unknown;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['HTTPValidationError'];
};
};
};
};
}

View file

@ -3,6 +3,7 @@ import { requestUrl, normalizePath } from 'obsidian'; // Add requestUrl import
import type { MediaType } from 'src/utils/MediaType';
import { APIManager } from './api/APIManager';
import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI';
import { ComicVineAPI } from './api/apis/ComicVineAPI';
import { GiantBombAPI } from './api/apis/GiantBombAPI';
import { MALAPI } from './api/apis/MALAPI';
import { MALAPIManga } from './api/apis/MALAPIManga';
@ -12,9 +13,8 @@ import { OMDbAPI } from './api/apis/OMDbAPI';
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
import { SteamAPI } from './api/apis/SteamAPI';
import { WikipediaAPI } from './api/apis/WikipediaAPI';
import { ComicVineAPI } from './api/apis/ComicVineAPI';
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal';
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
import type { MediaTypeModel } from './models/MediaTypeModel';
import { PropertyMapper } from './settings/PropertyMapper';
import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping';
@ -123,7 +123,7 @@ export default class MediaDbPlugin extends Plugin {
return false;
}
if (!checking) {
this.updateActiveNote(false);
void this.updateActiveNote(false);
}
return true;
},
@ -136,7 +136,7 @@ export default class MediaDbPlugin extends Plugin {
return false;
}
if (!checking) {
this.updateActiveNote(true);
void this.updateActiveNote(true);
}
return true;
},
@ -150,7 +150,7 @@ export default class MediaDbPlugin extends Plugin {
return false;
}
if (!checking) {
this.createLinkWithSearchModal();
void this.createLinkWithSearchModal();
}
return true;
},
@ -213,7 +213,7 @@ export default class MediaDbPlugin extends Plugin {
apiSearchResults = apiSearchResults.filter(x => types.contains(x.type));
let selectResults: MediaTypeModel[];
let proceed: boolean = false;
const proceed: boolean = false;
while (!proceed) {
selectResults =
@ -247,7 +247,7 @@ export default class MediaDbPlugin extends Plugin {
}
let selectResults: MediaTypeModel[];
let proceed: boolean = false;
const proceed: boolean = false;
while (!proceed) {
selectResults =
@ -322,9 +322,7 @@ export default class MediaDbPlugin extends Plugin {
const fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options);
if (!options.folder) {
options.folder = await this.mediaTypeManager.getFolder(mediaTypeModel, this.app);
}
options.folder ??= await this.mediaTypeManager.getFolder(mediaTypeModel, this.app);
const targetFile = await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options);
@ -347,7 +345,7 @@ export default class MediaDbPlugin extends Plugin {
if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) {
try {
const imageUrl = mediaTypeModel.image;
const imageExt = imageUrl.split('.').pop()?.split(/\#|\?/)[0] || 'jpg';
const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg';
const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`;
const imagePath = normalizePath(`${this.settings.imageFolder}/${imageFileName}`);
@ -441,7 +439,7 @@ export default class MediaDbPlugin extends Plugin {
// Update updated front matter with entries from the old front matter, if it isn't defined in the new front matter
Object.keys(previousMetadata).forEach(key => {
const value = previousMetadata[key];
const value: unknown = previousMetadata[key];
if (!frontMatter[key] && value) {
frontMatter[key] = value;
@ -450,17 +448,9 @@ export default class MediaDbPlugin extends Plugin {
}
// Ensure that id, type, and dataSource are defined
if (!frontMatter.id) {
frontMatter.id = mediaTypeModel.id;
}
if (!frontMatter.type) {
frontMatter.type = mediaTypeModel.type;
}
if (!frontMatter.dataSource) {
frontMatter.dataSource = mediaTypeModel.dataSource;
}
frontMatter.id ??= mediaTypeModel.id;
frontMatter.type ??= mediaTypeModel.type;
frontMatter.dataSource ??= mediaTypeModel.dataSource;
if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) {
// Only support stringifyYaml for templater plugin
@ -478,7 +468,7 @@ export default class MediaDbPlugin extends Plugin {
return { fileMetadata: fileMetadata, fileContent: fileContent };
}
const attachFileMetadata: any = this.getMetadataFromFileCache(fileToAttach);
const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach);
// TODO: better object merging
fileMetadata = Object.assign(attachFileMetadata, fileMetadata);
@ -496,7 +486,7 @@ export default class MediaDbPlugin extends Plugin {
return { fileMetadata: fileMetadata, fileContent: fileContent };
}
const templateMetadata: Metadata = this.getMetaDataFromFileContent(template);
const templateMetadata = this.getMetaDataFromFileContent(template);
// TODO: better object merging
fileMetadata = Object.assign(templateMetadata, fileMetadata);
@ -522,7 +512,7 @@ export default class MediaDbPlugin extends Plugin {
frontMatter = frontMatter.substring(4);
frontMatter = frontMatter.substring(0, frontMatter.length - 3);
metadata = parseYaml(frontMatter);
metadata = parseYaml(frontMatter) as Metadata;
if (!metadata) {
metadata = {};
@ -645,11 +635,11 @@ export default class MediaDbPlugin extends Plugin {
continue;
}
const metadata: any = this.getMetadataFromFileCache(file);
const metadata = this.getMetadataFromFileCache(file);
const title = metadata[titleFieldName];
if (!title) {
erroredFiles.push({ filePath: file.path, error: `metadata field '${titleFieldName}' not found or empty` });
if (!title || typeof title !== 'string') {
erroredFiles.push({ filePath: file.path, error: `metadata field '${titleFieldName}' not found, empty, or not a string` });
continue;
}
@ -715,7 +705,7 @@ export default class MediaDbPlugin extends Plugin {
async loadSettings(): Promise<void> {
// console.log(DEFAULT_SETTINGS);
const diskSettings: MediaDbPluginSettings = await this.loadData();
const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings;
const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this);
const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings);

View file

@ -12,7 +12,7 @@ export class ConfirmOverwriteModal extends Modal {
this.onSubmit = onSubmit;
}
onOpen() {
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'File already exists' });
contentEl.createEl('p', { text: `The file "${this.fileName}" already exists. Do you want to overwrite it?` });
@ -36,7 +36,7 @@ export class ConfirmOverwriteModal extends Modal {
});
}
onClose() {
onClose(): void {
const { contentEl } = this;
contentEl.empty();
this.onSubmit(this.result);

View file

@ -1,7 +1,6 @@
import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper';
import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
@ -39,7 +38,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
keyPressCallback(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.search();
void this.search();
}
}
@ -119,7 +118,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
this.search();
void this.search();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;

View file

@ -1,7 +1,6 @@
import type { ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper';
import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
@ -24,7 +23,7 @@ export class MediaDbIdSearchModal extends Modal {
this.plugin = plugin;
this.title = idSearchModalOptions.modalTitle ?? '';
this.selectedApi = idSearchModalOptions.preselectedAPI || plugin.apiManager.apis[0].apiName;
this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName;
this.query = '';
this.isBusy = false;
}
@ -39,7 +38,7 @@ export class MediaDbIdSearchModal extends Modal {
keyPressCallback(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.search();
void this.search();
}
}
@ -105,7 +104,7 @@ export class MediaDbIdSearchModal extends Modal {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
this.search();
void this.search();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;

View file

@ -1,4 +1,3 @@
import type { ButtonComponent } from 'obsidian';
import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian';
import type MediaDbPlugin from 'src/main';
import type { MediaTypeModel } from 'src/models/MediaTypeModel';

View file

@ -1,7 +1,6 @@
import type { ButtonComponent } from 'obsidian';
import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { MediaType } from '../utils/MediaType';
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper';
@ -42,7 +41,7 @@ export class MediaDbSearchModal extends Modal {
keyPressCallback(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.search();
void this.search();
}
}
@ -131,7 +130,7 @@ export class MediaDbSearchModal extends Modal {
btn.setButtonText('Ok');
btn.setCta();
btn.onClick(() => {
this.search();
void this.search();
});
btn.buttonEl.addClass('media-db-plugin-button');
this.searchBtn = btn;

View file

@ -54,7 +54,7 @@ export abstract class SelectModal<T> extends Modal {
this.scope.register([], 'Enter', () => this.submit());
}
abstract renderElement(value: T, el: HTMLElement): any;
abstract renderElement(value: T, el: HTMLElement): void;
abstract submit(): void;
@ -76,7 +76,7 @@ export abstract class SelectModal<T> extends Modal {
}
}
async onOpen(): Promise<void> {
onOpen(): void {
const { contentEl, titleEl } = this;
titleEl.createEl('h2', { text: this.title });

View file

@ -43,7 +43,7 @@ export class BoardGameModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -43,7 +43,7 @@ export class BookModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -53,7 +53,7 @@ export class ComicMangaModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -39,7 +39,7 @@ export class GameModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -51,7 +51,7 @@ export class MovieModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -28,7 +28,7 @@ export class MusicReleaseModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -55,7 +55,7 @@ export class SeriesModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -24,7 +24,7 @@ export class WikiModel extends MediaTypeModel {
migrateObject(this, obj, this);
if (!obj.hasOwnProperty('userData')) {
if (!Object.hasOwn(obj, 'userData')) {
migrateObject(this.userData, obj, this.userData);
}

View file

@ -1,3 +1,4 @@
import type { MediaType } from 'src/utils/MediaType';
import type MediaDbPlugin from '../main';
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
import { PropertyMappingOption } from './PropertyMapping';
@ -16,7 +17,7 @@ export class PropertyMapper {
* @param obj
*/
convertObject(obj: Record<string, unknown>): Record<string, unknown> {
if (!obj.hasOwnProperty('type')) {
if (!Object.hasOwn(obj, 'type')) {
return obj;
}
@ -58,7 +59,7 @@ export class PropertyMapper {
* @param obj
*/
convertObjectBack(obj: Record<string, unknown>): Record<string, unknown> {
if (!obj.hasOwnProperty('type')) {
if (!Object.hasOwn(obj, 'type')) {
return obj;
}
@ -66,7 +67,7 @@ export class PropertyMapper {
obj.type = 'comicManga';
console.debug(`MDB | updated metadata type`, obj.type);
}
if (MEDIA_TYPES.contains(obj.type as any)) {
if (MEDIA_TYPES.contains(obj.type as MediaType)) {
return obj;
}

View file

@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions } from './PropertyMapping';
import { capitalizeFirstLetter } from '../utils/Utils';
import Icon from './Icon.svelte';

View file

@ -1,5 +1,6 @@
import type { App } from 'obsidian';
import { Notice, PluginSettingTab, Setting } from 'obsidian';
import type { MediaType } from 'src/utils/MediaType';
import { mount } from 'svelte';
import type MediaDbPlugin from '../main';
import type { MediaTypeModel } from '../models/MediaTypeModel';
@ -9,7 +10,6 @@ import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte';
import { FileSuggest } from './suggesters/FileSuggest';
import { FolderSuggest } from './suggesters/FolderSuggest';
import type { MediaType } from 'src/utils/MediaType';
export interface MediaDbPluginSettings {
OMDbKey: string;

View file

@ -1,12 +1,11 @@
import type { TAbstractFile } from 'obsidian';
import { TFile } from 'obsidian';
import { TextInputSuggest } from './Suggest';
import { AbstractInputSuggest, TFile } from 'obsidian';
export class FileSuggest extends TextInputSuggest<TFile> {
getSuggestions(inputStr: string): TFile[] {
export class FileSuggest extends AbstractInputSuggest<TFile> {
protected getSuggestions(query: string): TFile[] | Promise<TFile[]> {
const abstractFiles = this.app.vault.getAllLoadedFiles();
const files: TFile[] = [];
const lowerCaseInputStr = inputStr.toLowerCase();
const lowerCaseInputStr = query.toLowerCase();
abstractFiles.forEach((file: TAbstractFile) => {
if (file instanceof TFile && file.name.toLowerCase().contains(lowerCaseInputStr)) {
@ -17,13 +16,7 @@ export class FileSuggest extends TextInputSuggest<TFile> {
return files;
}
renderSuggestion(file: TFile, el: HTMLElement): void {
el.setText(file.path);
}
selectSuggestion(file: TFile): void {
this.inputEl.value = file.path;
this.inputEl.trigger('input');
this.close();
renderSuggestion(value: TFile, el: HTMLElement): void {
el.setText(value.path);
}
}

View file

@ -1,14 +1,11 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import type { TAbstractFile } from 'obsidian';
import { TFolder } from 'obsidian';
import { TextInputSuggest } from './Suggest';
import { AbstractInputSuggest, TFolder } from 'obsidian';
export class FolderSuggest extends TextInputSuggest<TFolder> {
getSuggestions(inputStr: string): TFolder[] {
export class FolderSuggest extends AbstractInputSuggest<TFolder> {
protected getSuggestions(query: string): TFolder[] | Promise<TFolder[]> {
const abstractFiles = this.app.vault.getAllLoadedFiles();
const folders: TFolder[] = [];
const lowerCaseInputStr = inputStr.toLowerCase();
const lowerCaseInputStr = query.toLowerCase();
abstractFiles.forEach((folder: TAbstractFile) => {
if (folder instanceof TFolder && folder.path.toLowerCase().contains(lowerCaseInputStr)) {
@ -19,13 +16,7 @@ export class FolderSuggest extends TextInputSuggest<TFolder> {
return folders;
}
renderSuggestion(file: TFolder, el: HTMLElement): void {
el.setText(file.path);
}
selectSuggestion(file: TFolder): void {
this.inputEl.value = file.path;
this.inputEl.trigger('input');
this.close();
renderSuggestion(value: TFolder, el: HTMLElement): void {
el.setText(value.path);
}
}

View file

@ -1,185 +0,0 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import type { Instance as PopperInstance } from '@popperjs/core';
import { createPopper } from '@popperjs/core';
import type { App, ISuggestOwner } from 'obsidian';
import { Scope } from 'obsidian';
import { wrapAround } from 'src/utils/Utils';
export class Suggest<T> {
private owner: ISuggestOwner<T>;
private values: T[];
private suggestions: HTMLElement[];
private selectedItem: number;
private containerEl: HTMLElement;
constructor(owner: ISuggestOwner<T>, containerEl: HTMLElement, scope: Scope) {
this.owner = owner;
this.containerEl = containerEl;
this.values = [];
this.suggestions = [];
this.selectedItem = 0;
containerEl.on('click', '.suggestion-item', (e, el) => this.onSuggestionClick(e, el));
containerEl.on('mousemove', '.suggestion-item', (e, el) => this.onSuggestionMouseover(e, el));
scope.register([], 'ArrowUp', event => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
}
return undefined;
});
scope.register([], 'ArrowDown', event => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
}
return undefined;
});
scope.register([], 'Enter', event => {
if (!event.isComposing) {
this.useSelectedItem(event);
return false;
}
return undefined;
});
}
onSuggestionClick(event: MouseEvent, el: HTMLElement): void {
event.preventDefault();
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
this.useSelectedItem(event);
}
onSuggestionMouseover(_event: MouseEvent, el: HTMLElement): void {
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
}
setSuggestions(values: T[]): void {
this.containerEl.empty();
const suggestionEls: HTMLDivElement[] = [];
values.forEach(value => {
const suggestionEl = this.containerEl.createDiv('suggestion-item');
this.owner.renderSuggestion(value, suggestionEl);
suggestionEls.push(suggestionEl);
});
this.values = values;
this.suggestions = suggestionEls;
this.setSelectedItem(0, false);
}
useSelectedItem(event: MouseEvent | KeyboardEvent): void {
const currentValue = this.values[this.selectedItem];
if (currentValue) {
this.owner.selectSuggestion(currentValue, event);
}
}
setSelectedItem(selectedIndex: number, scrollIntoView: boolean): void {
const normalizedIndex = this.suggestions.length > 0 ? wrapAround(selectedIndex, this.suggestions.length) : 0;
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
const selectedSuggestion = this.suggestions[normalizedIndex];
prevSelectedSuggestion?.removeClass('is-selected');
selectedSuggestion?.addClass('is-selected');
this.selectedItem = normalizedIndex;
if (scrollIntoView) {
selectedSuggestion.scrollIntoView(false);
}
}
}
export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
protected app: App;
protected inputEl: HTMLInputElement;
private popper?: PopperInstance;
private scope: Scope;
private suggestEl: HTMLElement;
private suggest: Suggest<T>;
constructor(app: App, inputEl: HTMLInputElement) {
this.app = app;
this.inputEl = inputEl;
this.scope = new Scope();
this.suggestEl = createDiv('suggestion-container');
const suggestion = this.suggestEl.createDiv('suggestion');
this.suggest = new Suggest(this, suggestion, this.scope);
this.scope.register([], 'Escape', this.close.bind(this));
this.inputEl.addEventListener('input', this.onInputChanged.bind(this));
this.inputEl.addEventListener('focus', this.onInputChanged.bind(this));
this.inputEl.addEventListener('blur', this.close.bind(this));
this.suggestEl.on('mousedown', '.suggestion-container', (event: MouseEvent) => {
event.preventDefault();
});
}
onInputChanged(): void {
const inputStr = this.inputEl.value;
const suggestions = this.getSuggestions(inputStr);
if (suggestions.length > 0) {
this.suggest.setSuggestions(suggestions);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.open((this.app as any).dom.appContainerEl, this.inputEl);
}
}
open(container: HTMLElement, inputEl: HTMLElement): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.app as any).keymap.pushScope(this.scope);
container.appendChild(this.suggestEl);
this.popper = createPopper(inputEl, this.suggestEl, {
placement: 'bottom-start',
modifiers: [
{
name: 'sameWidth',
enabled: true,
fn: ({ state, instance }): void => {
// Note: positioning needs to be calculated twice -
// first pass - positioning it according to the width of the popper
// second pass - position it with the width bound to the reference element
// we need to early exit to avoid an infinite loop
const targetWidth = `${state.rects.reference.width}px`;
if (state.styles.popper.width === targetWidth) {
return;
}
state.styles.popper.width = targetWidth;
instance.update();
},
phase: 'beforeWrite',
requires: ['computeStyles'],
},
],
});
}
close(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.app as any).keymap.popScope(this.scope);
this.suggest.setSuggestions([]);
this.popper?.destroy();
this.suggestEl.detach();
}
abstract getSuggestions(inputStr: string): T[];
abstract renderSuggestion(item: T, el: HTMLElement): void;
abstract selectSuggestion(item: T): void;
}

View file

@ -17,9 +17,7 @@ export class DateFormatter {
getPreview(format?: string): string {
const today = moment();
if (!format) {
format = this.toFormat;
}
format ??= this.toFormat;
return today.locale(this.locale).format(format);
}
@ -35,7 +33,7 @@ export class DateFormatter {
* from the locale of this machine.
* @returns formatted date string or null if `dateString` is not a valid date
*/
format(dateString: string, dateFormat?: string, locale: string = 'en'): string | null {
format(dateString: string | null | undefined, dateFormat?: string, locale: string = 'en'): string | null {
if (!dateString) {
return null;
}

View file

@ -1,9 +1,9 @@
import type { App, TAbstractFile, TFile } from 'obsidian';
import type { App, TFile } from 'obsidian';
import { TFolder } from 'obsidian';
import { BoardGameModel } from '../models/BoardGameModel';
import { BookModel } from '../models/BookModel';
import { GameModel } from '../models/GameModel';
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';
@ -104,9 +104,7 @@ export class MediaTypeManager {
async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise<TFolder> {
let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType());
if (!folderPath) {
folderPath = `/`;
}
folderPath ??= `/`;
// console.log(folderPath);
if (!(await app.vault.adapter.exists(folderPath))) {
@ -115,7 +113,7 @@ export class MediaTypeManager {
const folder = app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) {
throw Error(`Expected ${folder} to be instance of TFolder`);
throw Error(`Expected ${folder?.path} to be instance of TFolder`);
}
return folder;
@ -127,7 +125,7 @@ export class MediaTypeManager {
* @param obj
* @param mediaType
*/
createMediaTypeModelFromMediaType(obj: any, mediaType: MediaType): MediaTypeModel {
createMediaTypeModelFromMediaType(obj: object, mediaType: MediaType): MediaTypeModel {
if (mediaType === MediaType.Movie) {
return new MovieModel(obj);
} else if (mediaType === MediaType.Series) {

View file

@ -1,4 +1,5 @@
import type { TFile, TFolder, App } from 'obsidian';
import { requestUrl } from 'obsidian';
import type { MediaTypeModel } from '../models/MediaTypeModel';
export const pluginName: string = 'obsidian-media-db-plugin';
@ -42,7 +43,8 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin
return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}';
}
return obj;
// eslint-disable-next-line @typescript-eslint/no-base-to-string
return obj?.toString() ?? 'null';
} else if (parts.length === 2) {
const operator = parts[0];
@ -58,7 +60,8 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin
if (!Array.isArray(obj)) {
return '{{ INVALID TEMPLATE TAG - operator LIST is only applicable on an array }}';
}
return obj.map((e: any) => `- ${e}`).join('\n');
return obj.map((e: unknown) => `- ${e}`).join('\n');
} else if (operator === 'ENUM') {
if (!Array.isArray(obj)) {
return '{{ INVALID TEMPLATE TAG - operator ENUM is only applicable on an array }}';
@ -68,12 +71,16 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin
if (!Array.isArray(obj)) {
return '{{ INVALID TEMPLATE TAG - operator FIRST is only applicable on an array }}';
}
return obj[0];
const first = obj[0] as unknown;
return first?.toString() ?? 'null';
} else if (operator === 'LAST') {
if (!Array.isArray(obj)) {
return '{{ INVALID TEMPLATE TAG - operator LAST is only applicable on an array }}';
}
return obj[obj.length - 1];
const last = obj[obj.length - 1] as unknown;
return last?.toString() ?? 'null';
}
return `{{ INVALID TEMPLATE TAG - unknown operator ${operator} }}`;
@ -82,12 +89,12 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin
return '{{ INVALID TEMPLATE TAG }}';
}
function traverseMetaData(path: string[], mediaTypeModel: MediaTypeModel): any {
let o: any = mediaTypeModel;
function traverseMetaData(path: string[], mediaTypeModel: MediaTypeModel): unknown {
let o: unknown = mediaTypeModel;
for (const part of path) {
if (o !== undefined) {
o = o[part];
o = (o as Record<string, unknown>)[part];
}
}
@ -195,9 +202,9 @@ export interface CreateNoteOptions {
folder?: TFolder;
}
export function migrateObject<T extends object>(object: T, oldData: any, defaultData: T): void {
export function migrateObject<T extends object>(object: T, oldData: Record<string, unknown>, defaultData: T): void {
for (const key in object) {
object[key] = oldData.hasOwnProperty(key) ? oldData[key] : defaultData[key];
object[key] = Object.hasOwn(oldData, key) && oldData[key] !== undefined && oldData[key] !== null ? (oldData[key] as T[typeof key]) : defaultData[key];
}
}
@ -215,6 +222,8 @@ export function unCamelCase(str: string): string {
);
}
/* eslint-disable */
export function hasTemplaterPlugin(app: App): boolean {
const templater = (app as any).plugins.plugins['templater-obsidian'];
@ -224,15 +233,17 @@ export function hasTemplaterPlugin(app: App): boolean {
// Copied from https://github.com/anpigon/obsidian-book-search-plugin
// Licensed under the MIT license. Copyright (c) 2020 Jake Runzer
export async function useTemplaterPluginInFile(app: App, file: TFile): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const templater = (app as any).plugins.plugins['templater-obsidian'];
if (templater && !templater?.settings.trigger_on_file_creation) {
await templater.templater.overwrite_file_commands(file);
}
}
/* eslint-enable */
export type ModelToData<T> = {
[K in keyof T as T[K] extends Function ? never : K]?: T[K];
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
[K in keyof T as T[K] extends Function ? never : K]?: T[K] | null;
};
// Checks if a given URL points to an existing image (status 200), or returns false for 404/other errors.
@ -250,3 +261,38 @@ export async function imageUrlExists(url: string): Promise<boolean> {
return false;
}
}
export function isTruthy<T>(value: T): value is Exclude<T, false | 0 | '' | null | undefined> {
return Boolean(value);
}
/**
* Wraps Obsidians `requestUrl` in a fetch like API.
*/
export async function obsidianFetch(input: Request): Promise<Response> {
const obs_headers: Record<string, string> = {};
input.headers.forEach((header, value) => {
obs_headers[header] = value;
});
const res = await requestUrl({
url: input.url,
method: input.method,
headers: obs_headers,
throw: false, // Do not throw on error, handle it manually
});
const responseHeaders: Headers = new Headers();
for (const [key, value] of Object.entries(res.headers)) {
responseHeaders.append(key, value);
}
return {
ok: res.status >= 200 && res.status < 300,
status: res.status,
headers: responseHeaders,
// eslint-disable-next-line
json: async () => res.json,
text: async () => res.text,
} as Response;
}