Merge branch 'master' into feat/vndb
This commit is contained in:
commit
61789308e6
65 changed files with 51328 additions and 1050 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,3 +36,4 @@ exampleVault/.obsidian/plugins/obsidian-media-db-plugin/*
|
|||
!exampleVault/.obsidian/plugins/obsidian-media-db-plugin/.hotreload
|
||||
|
||||
exampleVault/Media DB/*
|
||||
meta.txt
|
||||
|
|
|
|||
88
README.md
88
README.md
|
|
@ -1,12 +1,16 @@
|
|||
## Obsidian Media DB Plugin
|
||||
|
||||
A plugin that can query multiple APIs for movies, series, anime, manga, games, music and wiki articles, and import them into your vault.
|
||||
A plugin that can query multiple APIs for movies, series, anime, manga, books, comics, games, music, and wiki articles, and import them into your vault.
|
||||
|
||||
> [!WARNING]
|
||||
> Please make sure you are looking at the README on the [release branch](https://github.com/mProjectsCode/obsidian-media-db-plugin/blob/release/README.md).
|
||||
> The README on the master branch refers to the current in-development version of the plugin.
|
||||
|
||||
### Features
|
||||
|
||||
#### Search by Title
|
||||
|
||||
Search a movie, series, anime, game, music release or wiki article by its name across multiple APIs.
|
||||
Search for movies, series, anime, manga, books, comics, games, music releases, or wiki articles by their name across multiple APIs.
|
||||
|
||||
#### Search by ID
|
||||
|
||||
|
|
@ -15,54 +19,54 @@ Allows you to search by an ID that varies from API to API. Concrete information
|
|||
#### Templates
|
||||
|
||||
The plugin allows you to set a template note that gets added to the end of any note created by this plugin.
|
||||
The plugin also offers simple "template tgs". E.g. if the template includes `{{ title }}`, it will be replaced by the title of the movie, show or game.
|
||||
Note that "template tags" are surrounded with two curly braces and that the spaces inside the curly braces are important.
|
||||
The plugin also offers simple template tags, for example `{{ title }}`, which will be replaced by the title of the media being imported.
|
||||
Note that template tags are surrounded by two curly braces and spaces. The spaces inside the curly braces are important!
|
||||
|
||||
For arrays there are two special ways of displaying them.
|
||||
For arrays, there are two special ways of displaying them:
|
||||
|
||||
- using `{{ LIST:variable_name }}` will result in
|
||||
- using `{{ LIST:variable_name }}` will result in:
|
||||
```
|
||||
- element 1
|
||||
- element 2
|
||||
- element 3
|
||||
- ...
|
||||
```
|
||||
- using `{{ ENUM:variable_name }}` will result in
|
||||
- using `{{ ENUM:variable_name }}` will result in:
|
||||
```
|
||||
element 1, element 2, element 3, ...
|
||||
```
|
||||
|
||||
Available variables that can be used in template tags are the same variables from the metadata of the note.
|
||||
Available variables that can be used in template tags are any front-matter properties.
|
||||
|
||||
I also published my own templates [here](https://github.com/mProjectsCode/obsidian-media-db-templates).
|
||||
|
||||
#### Download poster images
|
||||
|
||||
The plugin offers a setting to automatically download the poster images for a new media, ensuring offline access. The images are saved as `type_title (year)` e.g. `movie_The Perfect Storm (2000)`, in a user-chosen folder.
|
||||
|
||||
#### Metadata field customization
|
||||
|
||||
Allows you to rename the metadata fields this plugin generates through mappings.
|
||||
Allows you to rename the metadata fields this plugin generates through mappings. The mappings can be set in the plugin's settings.
|
||||
The three options for mapping are:
|
||||
|
||||
A mapping has to follow this syntax `[origional property name] -> [new property name]`.
|
||||
Multiple mappings are separated by a new line.
|
||||
So e.g.:
|
||||
|
||||
```
|
||||
title -> name
|
||||
year -> releaseYear
|
||||
```
|
||||
- `default`: Keep the original name
|
||||
- `remap`: Rename the property
|
||||
- `remove`: Removes the property entirely
|
||||
|
||||
#### Bulk Import
|
||||
|
||||
The plugin allows you to import your preexisting media collection and upgrade them to Media DB entries.
|
||||
The plugin allows you to import your preexisting media collection and upgrade it to Media DB entries.
|
||||
|
||||
##### Prerequisites
|
||||
|
||||
The preexisting media notes must be inside a folder in your vault.
|
||||
For the plugin to be able to query them they need one metadata field that is used as the title the piece of media is searched by.
|
||||
This can be achieved by for example using a `csv` import plugin to import an existing list from outside of obsidian.
|
||||
For the plugin to be able to query them, they need one metadata field that is used as the title the piece of media is searched by.
|
||||
This can be achieved by, for example, using a `csv` import plugin to import an existing list from outside of Obsidian.
|
||||
|
||||
##### Importing
|
||||
|
||||
To start the import process, right-click on the folder and select the `Import folder as Media DB entries` option.
|
||||
Then specify the API to search, if the current note content and metadata should be appended to the Media DB entry and the name of the metadata field that contains the title of the piece of media.
|
||||
Then specify the API to search, if the current note content and metadata should be appended to the Media DB entry, and the name of the metadata field that contains the title of the piece of media.
|
||||
|
||||
Then the plugin will go through every file in the folder and prompt you to select from the search results.
|
||||
|
||||
|
|
@ -72,7 +76,7 @@ After all files have been imported or the import was canceled, you will find the
|
|||
|
||||
### How to install
|
||||
|
||||
**The plugin is now released, so it can be installed directly through obsidian's plugin installer.**
|
||||
**The plugin is now released, so it can be installed directly through Obsidian's plugin installer.**
|
||||
|
||||
Alternatively, you can manually download the zip archive from the latest release here on GitHub.
|
||||
After downloading, extract the archive into the `.obsidian/plugins` folder in your vault.
|
||||
|
|
@ -91,20 +95,19 @@ The folder structure should look like this:
|
|||
|
||||
### How to use
|
||||
|
||||
(pictures are coming)
|
||||
|
||||
Once you have installed this plugin, you will find a database icon in the left ribbon.
|
||||
When using this or the `Add new Media DB entry` command, a popup will open.
|
||||
Here you can enter the title of what you want to search for and then select in which APIs to search.
|
||||
When using this or the `Add new Media DB entry` command, a pop-up will open.
|
||||
Here, you can enter the title of what you want to search for and then select which APIs to search.
|
||||
|
||||
After clicking search, a new popup will open prompting you to select from the search results.
|
||||
Now you select the result you want and the plugin will cast it's magic and create a new note in your vault, that contains the metadata of the selected search result.
|
||||
After clicking search, a new pop-up will open, prompting you to select from the search results.
|
||||
Now you select the result you want, and the plugin will cast its magic, creating a new note in your vault that contains the metadata of the selected search result.
|
||||
|
||||
### Currently supported media types
|
||||
|
||||
- movies (including specials)
|
||||
- series (including OVAs)
|
||||
- games
|
||||
- videogames
|
||||
- boardgames
|
||||
- music releases
|
||||
- wiki articles
|
||||
- books
|
||||
|
|
@ -116,21 +119,23 @@ Now you select the result you want and the plugin will cast it's magic and creat
|
|||
| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes |
|
||||
| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movie, series and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No |
|
||||
| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movies, series, and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No |
|
||||
| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Key` (**not** `API Read Access Token`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes |
|
||||
| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |
|
||||
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
|
||||
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No |
|
||||
| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID so there shouldn't be a rate limit. | No |
|
||||
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all Steam games. | games | No | 10000 per day | No |
|
||||
| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID, so there shouldn't be a rate limit. | No |
|
||||
| [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/). NOTE: As of September 2024 the API key is no longer free so consider using Giant Bomb or steam instead | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No |
|
||||
| [Giant Bomb](https://www.giantbomb.com) | The Giant Bomb API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.giantbomb.com/login-signup/) | API requests are limited to 200 requests per resource, per hour. In addition, they implement velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No |
|
||||
| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No |
|
||||
| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes |
|
||||
| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | No |
|
||||
|
||||
#### Notes
|
||||
|
||||
- [Jikan](https://jikan.moe/)
|
||||
- sometimes the api is very slow, this is normal
|
||||
- you need to use the title the anime has on [My Anime List](https://myanimelist.net), which is in most cases the japanese title
|
||||
- sometimes the api is very slow; this is normal
|
||||
- you need to use the title the anime has on [My Anime List](https://myanimelist.net), which is in most cases the Japanese title
|
||||
- e.g. instead of "Demon Slayer" you have to search "Kimetsu no Yaiba"
|
||||
|
||||
#### Search by ID
|
||||
|
|
@ -148,16 +153,22 @@ Now you select the result you want and the plugin will cast it's magic and creat
|
|||
- you can find this ID in the URL
|
||||
- e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528`
|
||||
- [MusicBrainz](https://musicbrainz.org/)
|
||||
- the id of a release is not easily accessible, you are better off just searching by title
|
||||
- the id of a release is not easily accessible; you are better off just searching by title
|
||||
- the search is generally for albums but you can have a more granular search like so:
|
||||
- search for albums by a specific `artist:"Lady Gaga" AND primarytype:"album"`
|
||||
- search for a specific album by a specific artist `artist:"Lady Gaga" AND primarytype:"album" AND releasegroup:"The Fame"`
|
||||
- search for a specific entry (song or album) by a specific `artist:"Lady Gaga" AND releasegroup:"Poker face"`
|
||||
- [Wikipedia](https://en.wikipedia.org/wiki/Main_Page)
|
||||
- [here](https://en.wikipedia.org/wiki/Wikipedia:Finding_a_Wikidata_ID) is a guide to finding the Wikipedia ID for an article
|
||||
- [Steam](https://store.steampowered.com/)
|
||||
- you can find this ID in the URL
|
||||
- e.g. for "Factorio" the URL looks like this `https://store.steampowered.com/app/427520/Factorio/` so the ID is `427520`
|
||||
- [Open Library](https://openlibrary.org)
|
||||
- The ID you need is the "work" ID and not the "book" ID, it needs to start with `/works/`. You can find this ID in the URL
|
||||
- e.g. for "Fantastic Mr. Fox" the URL looks like this `https://openlibrary.org/works/OL45804W` so the ID is `/works/OL45804W`
|
||||
- The ID can either be the "/work/" ID, the "/book/" ID, or the "/isbn/" ID it needs to start with `/works/`. You can find this ID in the URL
|
||||
- e.g. for "Fantastic Mr. Fox" the "/works/" URL looks like this `https://openlibrary.org/works/OL45804W` so the ID is `/works/OL45804W`
|
||||
- This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) `
|
||||
- For a specific edition of "Fantastic Mr. Fox" the "/books/" URL looks like this `https://openlibrary.org/books/OL3567303M/` so the ID is `/books/OL3567303M`
|
||||
- This URL is located in the editions section`
|
||||
- [Moby Games](https://www.mobygames.com)
|
||||
- you can find this ID in the URL
|
||||
- e.g. for "Bioshock 2" the URL looks like this `https://www.mobygames.com/game/45089/bioshock-2/` so the ID is `45089`
|
||||
|
|
@ -172,7 +183,7 @@ Now you select the result you want and the plugin will cast it's magic and creat
|
|||
- Located in the novel's VNDB URL path
|
||||
- e.g. The ID for [Katawa Shoujo](https://vndb.org/v945) (`https://vndb.org/v945`) is `v945`
|
||||
|
||||
### Problems, unexpected behavior or improvement suggestions?
|
||||
### Problems, unexpected behavior, or improvement suggestions?
|
||||
|
||||
You are more than welcome to open an issue on [GitHub](https://github.com/mProjectsCode/obsidian-media-db-plugin/issues).
|
||||
|
||||
|
|
@ -187,4 +198,3 @@ Contributions are always welcome. If you have an idea, feel free to open a featu
|
|||
Credits go to:
|
||||
|
||||
- https://github.com/anpigon/obsidian-book-search-plugin for some inspiration and the idea to make this plugin
|
||||
- https://github.com/liamcain/obsidian-periodic-notes for 99% of `Suggest.ts` and `FolderSuggest.ts`
|
||||
|
|
|
|||
20
automation/fetchSchemas.ts
Normal file
20
automation/fetchSchemas.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { $ } from 'utils/shellUtils';
|
||||
|
||||
async function fetchSchema() {
|
||||
// https://docs.api.jikan.moe/
|
||||
await $('bun openapi-typescript https://raw.githubusercontent.com/jikan-me/jikan-rest/master/storage/api-docs/api-docs.json -o ./src/api/schemas/MALAPI.ts');
|
||||
|
||||
// https://www.giantbomb.com/forums/api-developers-3017/giant-bomb-openapi-specification-1901269/
|
||||
await $('bun openapi-typescript ./src/api/schemas/GiantBomb.json -o ./src/api/schemas/GiantBomb.ts');
|
||||
|
||||
// https://www.omdbapi.com/swagger.json
|
||||
// await $('bun openapi-typescript ./src/api/schemas/OMDb.json -o ./src/api/schemas/OMDb.ts');
|
||||
|
||||
// https://github.com/internetarchive/openlibrary-api/blob/main/swagger.yaml
|
||||
await $('bun openapi-typescript ./src/api/schemas/OpenLibrary.json -o ./src/api/schemas/OpenLibrary.ts');
|
||||
|
||||
// https://developer.themoviedb.org/openapi
|
||||
await $('bun openapi-typescript https://developer.themoviedb.org/openapi/tmdb-api.json -o ./src/api/schemas/TMDB.ts');
|
||||
}
|
||||
|
||||
await fetchSchema();
|
||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "obsidian-media-db-plugin",
|
||||
"name": "Media DB",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.0-canary.20251229T143140",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.",
|
||||
"author": "Moritz Jung",
|
||||
|
|
|
|||
37
package.json
37
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "obsidian-media-db-plugin",
|
||||
"version": "0.8.0",
|
||||
"description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.",
|
||||
"description": "A plugin that can query multiple APIs for movies, series, anime, manga, books, comics, games, music and wiki articles, and import them into your vault.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "bun run automation/build/esbuild.dev.config.ts",
|
||||
|
|
@ -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,29 @@
|
|||
"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",
|
||||
"iso-639-2": "^3.0.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export abstract class APIModel {
|
|||
plugin!: MediaDbPlugin;
|
||||
|
||||
/**
|
||||
* This function should query the api and return a list of matches. The matches should be caped at 20.
|
||||
* This function should query the api and return a list of matches. The matches should be capped at 20.
|
||||
*
|
||||
* @param title the title to query for
|
||||
*/
|
||||
|
|
@ -18,14 +18,11 @@ export abstract class APIModel {
|
|||
|
||||
abstract getById(id: string): Promise<MediaTypeModel>;
|
||||
|
||||
abstract getDisabledMediaTypes(): MediaType[];
|
||||
|
||||
hasType(type: MediaType): boolean {
|
||||
// if (
|
||||
// this.types.contains(type) &&
|
||||
// (Boolean((this.plugin.settings.apiToggle as any)?.[this.apiName]?.[type]) === true || (this.plugin.settings.apiToggle as any)?.[this.apiName]?.[type] === undefined)
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
return this.types.contains(type);
|
||||
const disabledMediaTypes = this.getDisabledMediaTypes();
|
||||
return this.types.includes(type) && !disabledMediaTypes.includes(type);
|
||||
}
|
||||
|
||||
hasTypeOverlap(types: MediaType[]): boolean {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
this.plugin = plugin;
|
||||
this.apiName = 'BoardGameGeekAPI';
|
||||
this.apiDescription = 'A free API for BoardGameGeek things.';
|
||||
this.apiUrl = 'https://api.geekdo.com/xmlapi';
|
||||
this.apiUrl = 'https://boardgamegeek.com/xmlapi/';
|
||||
this.types = [MediaType.BoardGame];
|
||||
}
|
||||
|
||||
|
|
@ -24,8 +26,15 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (fetchData.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
|
||||
if (fetchData.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
|
@ -62,8 +71,15 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (fetchData.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
|
||||
if (fetchData.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
|
@ -117,4 +133,7 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
@ -95,4 +103,7 @@ export class ComicVineAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.ComicVineAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:${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,
|
||||
|
|
@ -106,4 +159,7 @@ export class GiantBombAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.GiantBombAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,53 @@ 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) ?? [],
|
||||
ageRating: result.rating,
|
||||
premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat),
|
||||
streamingServices: result.streaming?.map(x => x.name).filter(isTruthy),
|
||||
|
||||
userData: {
|
||||
watched: false,
|
||||
|
|
@ -135,24 +162,22 @@ 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) ?? [],
|
||||
ageRating: result.rating,
|
||||
premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat),
|
||||
streamingServices: result.streaming?.map(x => x.name).filter(isTruthy),
|
||||
|
||||
userData: {
|
||||
watched: false,
|
||||
|
|
@ -165,24 +190,24 @@ 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',
|
||||
ageRating: result.rating,
|
||||
airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat),
|
||||
airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat),
|
||||
airing: result.airing,
|
||||
|
||||
userData: {
|
||||
|
|
@ -195,4 +220,7 @@ export class MALAPI extends APIModel {
|
|||
|
||||
throw new Error(`MDB | Unknown media type for id ${id}`);
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.MALAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
@ -126,4 +155,7 @@ export class MALAPIManga extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.MALAPIManga_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
||||
|
|
@ -55,7 +61,7 @@ export class MobyGamesAPI extends APIModel {
|
|||
year: new Date(result.platforms[0].first_release_date).getFullYear().toString(),
|
||||
dataSource: this.apiName,
|
||||
id: result.game_id,
|
||||
} as GameModel),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -107,4 +113,7 @@ export class MobyGamesAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.MobyGamesAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,95 @@ import type MediaDbPlugin from '../../main';
|
|||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MusicReleaseModel } from '../../models/MusicReleaseModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils';
|
||||
import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
|
||||
// sadly no open api schema available
|
||||
|
||||
interface Tag {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
interface Genre {
|
||||
name: string;
|
||||
count: number;
|
||||
id: string;
|
||||
disambiguation: string;
|
||||
}
|
||||
interface Release {
|
||||
id: string;
|
||||
'status-id': string;
|
||||
title: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface ArtistCredit {
|
||||
name: string;
|
||||
artist: {
|
||||
tags: Tag[];
|
||||
type: string;
|
||||
id: string;
|
||||
name: string;
|
||||
'short-name': string;
|
||||
country: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
id: string;
|
||||
'type-id': string;
|
||||
score: number;
|
||||
'primary-type-id': string;
|
||||
'artists-credit-id': string;
|
||||
count: number;
|
||||
title: string;
|
||||
'first-release-date': string;
|
||||
'primary-type': string;
|
||||
'artist-credit': ArtistCredit[];
|
||||
releases: Release[];
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
interface IdResponse {
|
||||
id: string;
|
||||
tags: Tag[];
|
||||
'primary-type-id': string;
|
||||
'artist-credit': ArtistCredit[];
|
||||
title: string;
|
||||
genres: Genre[];
|
||||
'first-release-date': string;
|
||||
releases: Release[];
|
||||
'primary-type': string;
|
||||
rating: {
|
||||
value: number;
|
||||
'votes-count': number;
|
||||
};
|
||||
}
|
||||
|
||||
interface MediaResponse {
|
||||
media: {
|
||||
'track-count': number;
|
||||
tracks: {
|
||||
'artist-credit': ArtistCredit[];
|
||||
length: number | null;
|
||||
number: string;
|
||||
position: number;
|
||||
title: string;
|
||||
recording: {
|
||||
length: number;
|
||||
title: string;
|
||||
};
|
||||
}[];
|
||||
}[];
|
||||
'text-representation': {
|
||||
language: string;
|
||||
script: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class MusicBrainzAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
apiDateFormat: string = 'YYYY-MM-DD';
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
super();
|
||||
|
|
@ -37,7 +121,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[] = [];
|
||||
|
||||
|
|
@ -48,12 +134,13 @@ export class MusicBrainzAPI extends APIModel {
|
|||
title: result.title,
|
||||
englishTitle: result.title,
|
||||
year: new Date(result['first-release-date']).getFullYear().toString(),
|
||||
releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown',
|
||||
dataSource: this.apiName,
|
||||
url: 'https://musicbrainz.org/release-group/' + result.id,
|
||||
id: result.id,
|
||||
image: 'https://coverartarchive.org/release-group/' + result.id + '/front',
|
||||
image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg',
|
||||
|
||||
artists: result['artist-credit'].map((a: any) => a.name),
|
||||
artists: result['artist-credit'].map(a => a.name),
|
||||
subType: result['primary-type'],
|
||||
}),
|
||||
);
|
||||
|
|
@ -65,33 +152,73 @@ export class MusicBrainzAPI extends APIModel {
|
|||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
|
||||
const searchUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
// Fetch release group
|
||||
const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`;
|
||||
const groupResponse = await requestUrl({
|
||||
url: groupUrl,
|
||||
headers: {
|
||||
'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`,
|
||||
},
|
||||
});
|
||||
|
||||
if (fetchData.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
|
||||
if (groupResponse.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${groupResponse.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const result = await fetchData.json;
|
||||
const result = (await groupResponse.json) as IdResponse;
|
||||
|
||||
// Get ID of the first release
|
||||
const firstRelease = result.releases?.[0];
|
||||
if (!firstRelease) {
|
||||
throw Error('MDB | No releases found in release group.');
|
||||
}
|
||||
|
||||
// Fetch recordings for the first release
|
||||
const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`;
|
||||
console.log(`MDB | Fetching release recordings from: ${releaseUrl}`);
|
||||
|
||||
const releaseResponse = await requestUrl({
|
||||
url: releaseUrl,
|
||||
headers: {
|
||||
'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`,
|
||||
},
|
||||
});
|
||||
|
||||
if (releaseResponse.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const releaseData = (await releaseResponse.json) as MediaResponse;
|
||||
const tracks = extractTracksFromMedia(releaseData.media);
|
||||
|
||||
// Calculate total album length for the first release
|
||||
const totalrawLength =
|
||||
releaseData.media[0]?.tracks.reduce((sum, track) => {
|
||||
const len = track.length ?? track.recording?.length;
|
||||
return typeof len === 'number' && !isNaN(len) ? sum + len : sum;
|
||||
}, 0) ?? 0;
|
||||
const albumLengthCalc = millisecondsToMinutes(totalrawLength);
|
||||
|
||||
console.log(releaseData);
|
||||
|
||||
return new MusicReleaseModel({
|
||||
type: 'musicRelease',
|
||||
title: result.title,
|
||||
englishTitle: result.title,
|
||||
year: new Date(result['first-release-date']).getFullYear().toString(),
|
||||
releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown',
|
||||
dataSource: this.apiName,
|
||||
url: 'https://musicbrainz.org/release-group/' + result.id,
|
||||
id: result.id,
|
||||
image: 'https://coverartarchive.org/release-group/' + result.id + '/front',
|
||||
image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg',
|
||||
|
||||
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),
|
||||
language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown',
|
||||
genres: result.genres.map(g => g.name),
|
||||
subType: result['primary-type'],
|
||||
albumDuration: albumLengthCalc,
|
||||
trackCount: releaseData.media[0]?.['track-count'] ?? 0,
|
||||
tracks: tracks,
|
||||
rating: result.rating.value * 2,
|
||||
|
||||
userData: {
|
||||
|
|
@ -99,4 +226,36 @@ export class MusicBrainzAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
||||
function extractTracksFromMedia(media: MediaResponse['media']): {
|
||||
number: number;
|
||||
title: string;
|
||||
duration: string;
|
||||
featuredArtists: string[];
|
||||
}[] {
|
||||
if (!media || media.length === 0 || !media[0].tracks) return [];
|
||||
|
||||
return media[0].tracks.map((track, index) => {
|
||||
const title = track.title ?? track.recording?.title ?? 'Unknown Title';
|
||||
const rawLength = track.length ?? track.recording?.length;
|
||||
const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown';
|
||||
const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? [];
|
||||
|
||||
return {
|
||||
number: index + 1,
|
||||
title,
|
||||
duration,
|
||||
featuredArtists,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function millisecondsToMinutes(milliseconds: number): string {
|
||||
const minutes = Math.floor(milliseconds / 60000);
|
||||
const seconds = Math.floor((milliseconds % 60000) / 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { requestUrl } from 'obsidian';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { GameModel } from '../../models/GameModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
|
|
@ -7,6 +7,54 @@ import { SeriesModel } from '../../models/SeriesModel';
|
|||
import { MediaType } from '../../utils/MediaType';
|
||||
import { APIModel } from '../APIModel';
|
||||
|
||||
interface ErrorResponse {
|
||||
Response: 'False';
|
||||
Error: string;
|
||||
}
|
||||
|
||||
type SearchResponse =
|
||||
| {
|
||||
Response: 'True';
|
||||
totalResults: string;
|
||||
Search: {
|
||||
Title: string;
|
||||
Year: string;
|
||||
Poster: string;
|
||||
imdbID: string;
|
||||
Type: string;
|
||||
}[];
|
||||
}
|
||||
| ErrorResponse;
|
||||
|
||||
type IdResponse =
|
||||
| {
|
||||
Response: 'True';
|
||||
Title: string;
|
||||
Year: string;
|
||||
Rated: string;
|
||||
Released: string;
|
||||
Runtime: string;
|
||||
Genre: string;
|
||||
Director: string;
|
||||
Writer: string;
|
||||
Actors: string;
|
||||
Plot: string;
|
||||
Language: string;
|
||||
Country: string;
|
||||
Awards: string;
|
||||
Poster: string;
|
||||
Metascore: string;
|
||||
imdbRating: string;
|
||||
imdbVotes: string;
|
||||
imdbID: string;
|
||||
Type: string;
|
||||
DVD: string;
|
||||
BoxOffice: string;
|
||||
Production: string;
|
||||
Website: string;
|
||||
}
|
||||
| ErrorResponse;
|
||||
|
||||
export class OMDbAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
|
|
@ -33,24 +81,30 @@ 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 response = await requestUrl({
|
||||
url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (fetchData.status === 401) {
|
||||
if (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.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const data = await fetchData.json();
|
||||
const data = response.json as SearchResponse | undefined;
|
||||
|
||||
if (!data) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
if (data.Response === 'False') {
|
||||
if (data.Error === 'Movie not found!') {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`);
|
||||
throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`);
|
||||
}
|
||||
if (!data.Search) {
|
||||
return [];
|
||||
|
|
@ -111,18 +165,23 @@ 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 response = await requestUrl({
|
||||
url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${this.plugin.settings.OMDbKey}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (fetchData.status === 401) {
|
||||
if (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.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const result = await fetchData.json();
|
||||
// console.debug(result);
|
||||
const result = response.json as IdResponse | undefined;
|
||||
|
||||
if (!result) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
if (result.Response === 'False') {
|
||||
throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`);
|
||||
|
|
@ -130,7 +189,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 +202,20 @@ 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 ?? '',
|
||||
actors: result.Actors?.split(', '),
|
||||
image: result.Poster.replace('_SX300', '_SX600'),
|
||||
|
||||
released: true,
|
||||
streamingServices: [],
|
||||
premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat) ?? 'unknown',
|
||||
country: result.Country?.split(', '),
|
||||
boxOffice: result.BoxOffice,
|
||||
ageRating: result.Rated,
|
||||
premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat),
|
||||
|
||||
userData: {
|
||||
watched: false,
|
||||
|
|
@ -173,21 +233,20 @@ 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 ?? '',
|
||||
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',
|
||||
country: result.Country?.split(', '),
|
||||
ageRating: result.Rated,
|
||||
airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat),
|
||||
|
||||
userData: {
|
||||
watched: false,
|
||||
|
|
@ -205,14 +264,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 ?? '',
|
||||
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,
|
||||
|
|
@ -223,4 +280,8 @@ export class OMDbAPI extends APIModel {
|
|||
|
||||
throw new Error(`MDB | Unknown media type for id ${id}`);
|
||||
}
|
||||
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.OMDbAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,34 @@
|
|||
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 {
|
||||
editions: {
|
||||
docs: {
|
||||
key?: string;
|
||||
title?: string;
|
||||
cover_i?: number;
|
||||
isbn?: string[];
|
||||
}[];
|
||||
};
|
||||
cover_i?: number;
|
||||
has_fulltext?: boolean;
|
||||
edition_count?: number;
|
||||
title?: string;
|
||||
author_name?: string[];
|
||||
first_publish_year?: number;
|
||||
key: string;
|
||||
description?: string;
|
||||
|
||||
number_of_pages_median?: number;
|
||||
isbn?: string[];
|
||||
ratings_average?: number;
|
||||
}
|
||||
|
||||
export class OpenLibraryAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
@ -20,14 +46,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 +73,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() ?? 'unknown',
|
||||
dataSource: this.apiName,
|
||||
id: result.key,
|
||||
author: result.author_name ?? 'unknown',
|
||||
author: result.author_name?.join(', '),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -52,33 +88,63 @@ 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: `${id}`,
|
||||
fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description',
|
||||
},
|
||||
},
|
||||
fetch: obsidianFetch,
|
||||
});
|
||||
|
||||
if (response.error !== undefined) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const data = await fetchData.json();
|
||||
// console.debug(data);
|
||||
const data = response.data as {
|
||||
docs: SearchResponse[];
|
||||
q?: string;
|
||||
};
|
||||
|
||||
const result = data.docs[0];
|
||||
|
||||
return new BookModel({
|
||||
title: result.title,
|
||||
year: result.first_publish_year,
|
||||
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,
|
||||
let key = result.key;
|
||||
let title = result.title;
|
||||
let cover_i = result.cover_i;
|
||||
let isbnArr = result.isbn;
|
||||
|
||||
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`,
|
||||
// Check if the query is for /isbn/ or /books/ and extract from editions.docs if present
|
||||
const q = data.q ?? '';
|
||||
if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) {
|
||||
const edition = result.editions.docs[0];
|
||||
key = edition.key ?? key;
|
||||
title = edition.title ?? title;
|
||||
cover_i = edition.cover_i ?? cover_i;
|
||||
isbnArr = edition.isbn ?? isbnArr;
|
||||
}
|
||||
|
||||
const pages = Number(result.number_of_pages_median);
|
||||
const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10));
|
||||
const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13));
|
||||
|
||||
return new BookModel({
|
||||
title: title,
|
||||
year: result.first_publish_year?.toString() ?? 'unknown',
|
||||
dataSource: this.apiName,
|
||||
url: `https://openlibrary.org` + key,
|
||||
id: key,
|
||||
isbn: Number.isNaN(isbn) ? undefined : isbn,
|
||||
isbn13: Number.isNaN(isbn13) ? undefined : isbn13,
|
||||
englishTitle: title,
|
||||
|
||||
author: result.author_name?.join(', '),
|
||||
plot: result.description ?? undefined,
|
||||
pages: Number.isNaN(pages) ? undefined : pages,
|
||||
onlineRating: result.ratings_average,
|
||||
image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined,
|
||||
|
||||
released: true,
|
||||
|
||||
|
|
@ -89,4 +155,7 @@ export class OpenLibraryAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,129 @@ import type MediaDbPlugin from '../../main';
|
|||
import { GameModel } from '../../models/GameModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { imageUrlExists } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
|
||||
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;
|
||||
typeMappings: Map<string, string>;
|
||||
|
|
@ -34,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);
|
||||
|
||||
|
|
@ -69,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) {
|
||||
|
|
@ -85,6 +205,16 @@ export class SteamAPI extends APIModel {
|
|||
|
||||
// console.debug(result);
|
||||
|
||||
// Check if a poster version of the image exists, else use the header image
|
||||
const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`;
|
||||
const exists = await imageUrlExists(imageUrl);
|
||||
let finalimageurl;
|
||||
if (exists) {
|
||||
finalimageurl = imageUrl;
|
||||
} else {
|
||||
finalimageurl = result.header_image ?? '';
|
||||
}
|
||||
|
||||
return new GameModel({
|
||||
type: MediaType.Game,
|
||||
title: result.name,
|
||||
|
|
@ -92,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: result.header_image ?? '',
|
||||
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,
|
||||
|
|
@ -109,4 +239,7 @@ export class SteamAPI extends APIModel {
|
|||
},
|
||||
});
|
||||
}
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.SteamAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
158
src/api/apis/TMDBMovieAPI.ts
Normal file
158
src/api/apis/TMDBMovieAPI.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MovieModel } from '../../models/MovieModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/TMDB';
|
||||
|
||||
export class TMDBMovieAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
apiDateFormat: string = 'YYYY-MM-DD';
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
super();
|
||||
|
||||
this.plugin = plugin;
|
||||
this.apiName = 'TMDBMovieAPI';
|
||||
this.apiDescription = 'A community built Movie DB.';
|
||||
this.apiUrl = 'https://www.themoviedb.org/';
|
||||
this.types = [MediaType.Movie];
|
||||
this.typeMappings = new Map<string, string>();
|
||||
this.typeMappings.set('movie', 'movie');
|
||||
}
|
||||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const response = await client.GET('/3/search/movie', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
query: encodeURIComponent(title),
|
||||
include_adult: this.plugin.settings.sfwFilter ? false : true,
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (response.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
if (response.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const data = response.data;
|
||||
|
||||
if (!data) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
if (data.total_results === 0 || !data.results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// console.debug(data.results);
|
||||
|
||||
const ret: MediaTypeModel[] = [];
|
||||
|
||||
for (const result of data.results) {
|
||||
ret.push(
|
||||
new MovieModel({
|
||||
type: 'movie',
|
||||
title: result.original_title,
|
||||
englishTitle: result.title,
|
||||
year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
id: result.id.toString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const response = await client.GET('/3/movie/{movie_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
path: { movie_id: parseInt(id) },
|
||||
query: {
|
||||
append_to_response: 'credits',
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (response.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
if (response.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const result = response.data;
|
||||
|
||||
if (!result) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
// console.debug(result);
|
||||
|
||||
return new MovieModel({
|
||||
type: 'movie',
|
||||
title: result.title,
|
||||
englishTitle: result.title,
|
||||
year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown',
|
||||
premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown',
|
||||
dataSource: this.apiName,
|
||||
url: `https://www.themoviedb.org/movie/${result.id}`,
|
||||
id: result.id.toString(),
|
||||
|
||||
plot: result.overview ?? '',
|
||||
genres: result.genres?.map((g: any) => g.name) ?? [],
|
||||
// TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type
|
||||
// @ts-ignore
|
||||
writer: result.credits.crew?.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [],
|
||||
// @ts-ignore
|
||||
director: result.credits.crew?.filter((c: any) => c.job === 'Director').map((c: any) => c.name) ?? [],
|
||||
studio: result.production_companies?.map((s: any) => s.name) ?? [],
|
||||
|
||||
duration: result.runtime?.toString() ?? 'unknown',
|
||||
onlineRating: result.vote_average,
|
||||
// @ts-ignore
|
||||
actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [],
|
||||
image: `https://image.tmdb.org/t/p/w780${result.poster_path}`,
|
||||
|
||||
released: ['Released'].includes(result.status!),
|
||||
streamingServices: [],
|
||||
|
||||
userData: {
|
||||
watched: false,
|
||||
lastWatched: '',
|
||||
personalRating: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
269
src/api/apis/TMDBSeasonAPI.ts
Normal file
269
src/api/apis/TMDBSeasonAPI.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { APIModel } from '../APIModel';
|
||||
import { SeasonModel } from '../../models/SeasonModel';
|
||||
import type { paths } from '../schemas/TMDB';
|
||||
|
||||
export class TMDBSeasonAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
apiDateFormat: string = 'YYYY-MM-DD';
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
super();
|
||||
|
||||
this.plugin = plugin;
|
||||
this.apiName = 'TMDBSeasonAPI';
|
||||
this.apiDescription = 'A community built Series DB (seasons).';
|
||||
this.apiUrl = 'https://www.themoviedb.org/';
|
||||
this.types = [MediaType.Season];
|
||||
this.typeMappings = new Map<string, string>();
|
||||
this.typeMappings.set('tv', 'season');
|
||||
}
|
||||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const searchResponse = await client.GET('/3/search/tv', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
query: encodeURIComponent(title),
|
||||
include_adult: this.plugin.settings.sfwFilter ? false : true,
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (searchResponse.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
|
||||
if (searchResponse.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const searchData = searchResponse.data;
|
||||
|
||||
if (!searchData?.results || searchData.total_results === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ret: MediaTypeModel[] = [];
|
||||
|
||||
for (const result of searchData.results) {
|
||||
if (ret.length >= 20) break;
|
||||
|
||||
// Fetch series details to get the total number of seasons
|
||||
let totalSeasons = 0;
|
||||
try {
|
||||
const detailsResponse = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: result.id ?? 0 },
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (detailsResponse.response.status === 200 && detailsResponse.data) {
|
||||
const detailsData = detailsResponse.data;
|
||||
if (Array.isArray(detailsData.seasons)) {
|
||||
totalSeasons = detailsData.seasons.length;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
ret.push(
|
||||
new SeasonModel({
|
||||
title: `${result.name ?? result.original_name ?? ''}`,
|
||||
englishTitle: result.name ?? result.original_name ?? '',
|
||||
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
id: result.id?.toString() ?? '',
|
||||
seasonTitle: result.name ?? result.original_name ?? '',
|
||||
seasonNumber: totalSeasons,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Fetch all seasons for a given series
|
||||
async getSeasonsForSeries(tvId: string): Promise<SeasonModel[]> {
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const seriesResponse = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: parseInt(tvId) },
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (seriesResponse.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
|
||||
if (seriesResponse.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const seriesData = seriesResponse.data;
|
||||
const seriesName = seriesData?.name ?? '';
|
||||
|
||||
const ret: SeasonModel[] = [];
|
||||
|
||||
if (Array.isArray(seriesData?.seasons)) {
|
||||
for (const season of seriesData.seasons) {
|
||||
const seasonNumber = season.season_number ?? 0;
|
||||
const titleText = `${seriesName} - Season ${seasonNumber}`;
|
||||
|
||||
ret.push(
|
||||
new SeasonModel({
|
||||
title: titleText,
|
||||
englishTitle: titleText,
|
||||
year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
id: `${tvId}/season/${seasonNumber}`,
|
||||
seasonTitle: season.name ?? titleText,
|
||||
seasonNumber: seasonNumber,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
// Expect season ids like "12345/season/2"
|
||||
const m = /^(\d+)\/season\/(\d+)$/.exec(id);
|
||||
if (!m) {
|
||||
throw Error(`MDB | Invalid season id "${id}". Expected format "<series_id>/season/<season_number>".`);
|
||||
}
|
||||
|
||||
const tvId = m[1];
|
||||
const seasonNumber = m[2];
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
|
||||
// Fetch season details
|
||||
const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
path: {
|
||||
series_id: parseInt(tvId),
|
||||
season_number: parseInt(seasonNumber),
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (seasonResponse.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
|
||||
if (seasonResponse.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const seasonData = seasonResponse.data;
|
||||
if (!seasonData) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
// Fetch parent series to build consistent titles and inherit fields
|
||||
const seriesResponse = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: parseInt(tvId) },
|
||||
query: {
|
||||
append_to_response: 'credits',
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (seriesResponse.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
|
||||
if (seriesResponse.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const seriesData = seriesResponse.data;
|
||||
|
||||
if (!seriesData) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const seriesName = seriesData?.name ?? '';
|
||||
const airDate = seasonData.air_date ?? '';
|
||||
const titleText = `${seriesName} - Season ${seasonData.season_number}`;
|
||||
|
||||
// Get airedTo as the air_date of the last episode, if available
|
||||
let airedTo = 'unknown';
|
||||
if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) {
|
||||
const lastEp = seasonData.episodes[seasonData.episodes.length - 1];
|
||||
if (lastEp?.air_date) airedTo = lastEp.air_date;
|
||||
}
|
||||
|
||||
return new SeasonModel({
|
||||
title: titleText,
|
||||
englishTitle: titleText,
|
||||
year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
url: `https://www.themoviedb.org/tv/${tvId}/season/${seasonData.season_number}`,
|
||||
id: `${tvId}/season/${seasonData.season_number}`,
|
||||
seasonTitle: seasonData.name ?? titleText,
|
||||
seasonNumber: seasonData.season_number ?? Number(seasonNumber),
|
||||
episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0,
|
||||
airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown',
|
||||
airedTo: airedTo,
|
||||
plot: seasonData.overview ?? '',
|
||||
image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '',
|
||||
genres: seriesData.genres?.map(g => g.name ?? '').filter(name => name !== '') ?? [],
|
||||
writer: seriesData.created_by?.map(c => c.name ?? '').filter(name => name !== '') ?? [],
|
||||
studio: seriesData.production_companies?.map(s => s.name ?? '').filter(name => name !== '') ?? [],
|
||||
duration: seriesData.episode_run_time?.[0]?.toString() ?? '',
|
||||
onlineRating: seasonData.vote_average ?? 0,
|
||||
// @ts-ignore - append_to_response credits not reflected in base schema
|
||||
actors: seriesData.credits?.cast?.map((c: any) => c.name).slice(0, 5) ?? [],
|
||||
released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''),
|
||||
streamingServices: [],
|
||||
airing: ['Returning Series'].includes(seriesData.status ?? ''),
|
||||
userData: { watched: false, lastWatched: '', personalRating: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
157
src/api/apis/TMDBSeriesAPI.ts
Normal file
157
src/api/apis/TMDBSeriesAPI.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { SeriesModel } from '../../models/SeriesModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/TMDB';
|
||||
|
||||
export class TMDBSeriesAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
apiDateFormat: string = 'YYYY-MM-DD';
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
super();
|
||||
|
||||
this.plugin = plugin;
|
||||
this.apiName = 'TMDBSeriesAPI';
|
||||
this.apiDescription = 'A community built Series DB.';
|
||||
this.apiUrl = 'https://www.themoviedb.org/';
|
||||
this.types = [MediaType.Series];
|
||||
this.typeMappings = new Map<string, string>();
|
||||
this.typeMappings.set('tv', 'series');
|
||||
}
|
||||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const response = await client.GET('/3/search/tv', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
query: encodeURIComponent(title),
|
||||
include_adult: this.plugin.settings.sfwFilter ? false : true,
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (response.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
if (response.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const data = response.data;
|
||||
|
||||
if (!data) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
if (data.total_results === 0 || !data.results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// console.debug(data.results);
|
||||
|
||||
const ret: MediaTypeModel[] = [];
|
||||
|
||||
for (const result of data.results) {
|
||||
ret.push(
|
||||
new SeriesModel({
|
||||
type: 'series',
|
||||
title: result.original_name,
|
||||
englishTitle: result.name,
|
||||
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
id: result.id.toString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const response = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: parseInt(id) },
|
||||
query: {
|
||||
append_to_response: 'credits',
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (response.response.status === 401) {
|
||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||
}
|
||||
if (response.response.status !== 200) {
|
||||
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
|
||||
}
|
||||
|
||||
const result = response.data;
|
||||
|
||||
if (!result) {
|
||||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
// console.debug(result);
|
||||
|
||||
return new SeriesModel({
|
||||
type: 'series',
|
||||
title: result.original_name,
|
||||
englishTitle: result.name,
|
||||
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
|
||||
dataSource: this.apiName,
|
||||
url: `https://www.themoviedb.org/tv/${result.id}`,
|
||||
id: result.id.toString(),
|
||||
|
||||
plot: result.overview ?? '',
|
||||
genres: result.genres?.map((g: any) => g.name) ?? [],
|
||||
writer: result.created_by?.map((c: any) => c.name) ?? [],
|
||||
studio: result.production_companies?.map((s: any) => s.name) ?? [],
|
||||
episodes: result.number_of_episodes,
|
||||
duration: result.episode_run_time?.[0]?.toString() ?? 'unknown',
|
||||
onlineRating: result.vote_average,
|
||||
// TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type
|
||||
// @ts-ignore
|
||||
actors: result.credits?.cast.map((c: any) => c.name).slice(0, 5) ?? [],
|
||||
image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null,
|
||||
|
||||
released: ['Returning Series', 'Cancelled', 'Ended'].includes(result.status!),
|
||||
streamingServices: [],
|
||||
airing: ['Returning Series'].includes(result.status!),
|
||||
airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown',
|
||||
airedTo: ['Returning Series'].includes(result.status!) ? 'unknown' : (this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown'),
|
||||
|
||||
userData: {
|
||||
watched: false,
|
||||
lastWatched: '',
|
||||
personalRating: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getDisabledMediaTypes(): MediaType[] {
|
||||
return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,24 +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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11226
src/api/schemas/GiantBomb.json
Normal file
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
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
6761
src/api/schemas/MALAPI.ts
Normal file
File diff suppressed because it is too large
Load diff
602
src/api/schemas/OpenLibrary.json
Normal file
602
src/api/schemas/OpenLibrary.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
578
src/api/schemas/OpenLibrary.ts
Normal file
578
src/api/schemas/OpenLibrary.ts
Normal 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'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
22832
src/api/schemas/TMDB.ts
Normal file
22832
src/api/schemas/TMDB.ts
Normal file
File diff suppressed because it is too large
Load diff
355
src/main.ts
355
src/main.ts
|
|
@ -1,7 +1,10 @@
|
|||
import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder } from 'obsidian';
|
||||
import type { TFile } from 'obsidian';
|
||||
import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian';
|
||||
import { requestUrl, normalizePath } from 'obsidian';
|
||||
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';
|
||||
|
|
@ -10,21 +13,27 @@ import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI';
|
|||
import { OMDbAPI } from './api/apis/OMDbAPI';
|
||||
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
|
||||
import { SteamAPI } from './api/apis/SteamAPI';
|
||||
import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI';
|
||||
import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI';
|
||||
import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI';
|
||||
import { WikipediaAPI } from './api/apis/WikipediaAPI';
|
||||
import { ComicVineAPI } from './api/apis/ComicVineAPI';
|
||||
import { VNDBAPI } from './api/apis/VNDBAPI';
|
||||
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
|
||||
import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal';
|
||||
import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal';
|
||||
import type { MediaTypeModel } from './models/MediaTypeModel';
|
||||
import { PropertyMapper } from './settings/PropertyMapper';
|
||||
import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping';
|
||||
import type { MediaDbPluginSettings } from './settings/Settings';
|
||||
import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings';
|
||||
import { BulkImportHelper } from './utils/BulkImportHelper';
|
||||
import { DateFormatter } from './utils/DateFormatter';
|
||||
import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager';
|
||||
import type { SearchModalOptions } from './utils/ModalHelper';
|
||||
import { ModalHelper, ModalResultCode } from './utils/ModalHelper';
|
||||
import { ModalHelper } from './utils/ModalHelper';
|
||||
import type { CreateNoteOptions } from './utils/Utils';
|
||||
import { dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils';
|
||||
import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils';
|
||||
|
||||
export type Metadata = Record<string, unknown>;
|
||||
|
||||
|
|
@ -40,6 +49,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
mediaTypeManager!: MediaTypeManager;
|
||||
modelPropertyMapper!: PropertyMapper;
|
||||
modalHelper!: ModalHelper;
|
||||
bulkImportHelper!: BulkImportHelper;
|
||||
dateFormatter!: DateFormatter;
|
||||
|
||||
frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---';
|
||||
|
|
@ -53,17 +63,20 @@ export default class MediaDbPlugin extends Plugin {
|
|||
this.apiManager.registerAPI(new WikipediaAPI(this));
|
||||
this.apiManager.registerAPI(new MusicBrainzAPI(this));
|
||||
this.apiManager.registerAPI(new SteamAPI(this));
|
||||
this.apiManager.registerAPI(new TMDBSeriesAPI(this));
|
||||
this.apiManager.registerAPI(new TMDBSeasonAPI(this));
|
||||
this.apiManager.registerAPI(new TMDBMovieAPI(this));
|
||||
this.apiManager.registerAPI(new BoardGameGeekAPI(this));
|
||||
this.apiManager.registerAPI(new OpenLibraryAPI(this));
|
||||
this.apiManager.registerAPI(new ComicVineAPI(this));
|
||||
this.apiManager.registerAPI(new MobyGamesAPI(this));
|
||||
this.apiManager.registerAPI(new GiantBombAPI(this));
|
||||
this.apiManager.registerAPI(new VNDBAPI(this));
|
||||
// this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data
|
||||
|
||||
this.mediaTypeManager = new MediaTypeManager();
|
||||
this.modelPropertyMapper = new PropertyMapper(this);
|
||||
this.modalHelper = new ModalHelper(this);
|
||||
this.bulkImportHelper = new BulkImportHelper(this);
|
||||
this.dateFormatter = new DateFormatter();
|
||||
|
||||
await this.loadSettings();
|
||||
|
|
@ -84,7 +97,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
menu.addItem(item => {
|
||||
item.setTitle('Import folder as Media DB entries')
|
||||
.setIcon('database')
|
||||
.onClick(() => this.createEntriesFromFolder(file));
|
||||
.onClick(() => this.bulkImportHelper.import(file));
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
|
@ -123,7 +136,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
return false;
|
||||
}
|
||||
if (!checking) {
|
||||
this.updateActiveNote(false);
|
||||
void this.updateActiveNote(false);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -136,7 +149,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
return false;
|
||||
}
|
||||
if (!checking) {
|
||||
this.updateActiveNote(true);
|
||||
void this.updateActiveNote(true);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -150,19 +163,13 @@ export default class MediaDbPlugin extends Plugin {
|
|||
return false;
|
||||
}
|
||||
if (!checking) {
|
||||
this.createLinkWithSearchModal();
|
||||
void this.createLinkWithSearchModal();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* first very simple approach
|
||||
* TODO:
|
||||
* - replace the detail query
|
||||
* - maybe custom link syntax
|
||||
*/
|
||||
async createLinkWithSearchModal(): Promise<void> {
|
||||
const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => {
|
||||
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);
|
||||
|
|
@ -213,25 +220,110 @@ 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) {
|
||||
if (types.length === 1 && types[0] === 'season') {
|
||||
selectResults =
|
||||
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
|
||||
(await this.modalHelper.openSelectModal(
|
||||
{
|
||||
elements: apiSearchResults,
|
||||
description: 'Select one search result to proceed.',
|
||||
submitButtonText: 'Ok',
|
||||
},
|
||||
async selectModalData => {
|
||||
return selectModalData.selected;
|
||||
},
|
||||
)) ?? [];
|
||||
} else {
|
||||
selectResults =
|
||||
(await this.modalHelper.openSelectModal(
|
||||
{
|
||||
elements: apiSearchResults,
|
||||
},
|
||||
async selectModalData => {
|
||||
return await this.queryDetails(selectModalData.selected);
|
||||
})) ?? [];
|
||||
if (!selectResults) {
|
||||
},
|
||||
)) ?? [];
|
||||
}
|
||||
if (!selectResults || selectResults.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
proceed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => {
|
||||
// Only show the season select modal if the user searches for seasons
|
||||
if (await this.handleSeasonSelectModal(types, selectResults)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => {
|
||||
return previewModalData.confirmed;
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.createMediaDbNotes(selectResults!);
|
||||
}
|
||||
|
||||
// Season select modal
|
||||
private async handleSeasonSelectModal(types: string[], selectResults: MediaTypeModel[]): Promise<boolean> {
|
||||
if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') {
|
||||
// Use static import for the modal
|
||||
const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as import('./api/apis/TMDBSeasonAPI').TMDBSeasonAPI;
|
||||
if (!tmdbSeasonAPI) {
|
||||
new Notice('TMDBSeasonAPI not found.');
|
||||
return true;
|
||||
}
|
||||
// Fetch all seasons for the selected series
|
||||
const allSeasons = await tmdbSeasonAPI.getSeasonsForSeries(selectResults[0].id);
|
||||
if (!allSeasons || allSeasons.length === 0) {
|
||||
new Notice('No seasons found for this series.');
|
||||
return true;
|
||||
}
|
||||
// Pass the original series title from the search result
|
||||
const seriesName = selectResults[0]?.englishTitle || selectResults[0]?.title || '';
|
||||
const modal = new MediaDbSeasonSelectModal(
|
||||
this,
|
||||
allSeasons.map(s => ({
|
||||
season_number: s.seasonNumber,
|
||||
name: s.seasonTitle || s.title,
|
||||
episode_count: s.episodes || 0,
|
||||
air_date: s.year,
|
||||
poster_path: s.image,
|
||||
})),
|
||||
true,
|
||||
seriesName,
|
||||
);
|
||||
const selectedSeasons: any[] = await new Promise(resolve => {
|
||||
modal.setSubmitCallback(resolve);
|
||||
modal.open();
|
||||
});
|
||||
if (!selectedSeasons || selectedSeasons.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Fetch full metadata for each selected season and create the note
|
||||
await Promise.all(
|
||||
selectedSeasons.map(async season => {
|
||||
const orig = allSeasons.find(s => s.seasonNumber === season.season_number);
|
||||
if (orig) {
|
||||
// Fetch full metadata using getById
|
||||
const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as import('./api/apis/TMDBSeasonAPI').TMDBSeasonAPI;
|
||||
if (tmdbSeasonAPI) {
|
||||
const fullMeta = await tmdbSeasonAPI.getById(orig.id);
|
||||
await this.createMediaDbNotes([fullMeta]);
|
||||
} else {
|
||||
await this.createMediaDbNotes([orig]);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async createEntryWithAdvancedSearchModal(): Promise<void> {
|
||||
const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => {
|
||||
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);
|
||||
|
|
@ -243,20 +335,24 @@ export default class MediaDbPlugin extends Plugin {
|
|||
}
|
||||
|
||||
let selectResults: MediaTypeModel[];
|
||||
let proceed: boolean = false;
|
||||
const proceed: boolean = false;
|
||||
|
||||
while (!proceed) {
|
||||
selectResults =
|
||||
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
|
||||
return await this.queryDetails(selectModalData.selected);
|
||||
})) ?? [];
|
||||
if (!selectResults) {
|
||||
if (!selectResults || selectResults.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
proceed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => {
|
||||
const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => {
|
||||
return previewModalData.confirmed;
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.createMediaDbNotes(selectResults!);
|
||||
|
|
@ -308,11 +404,13 @@ export default class MediaDbPlugin extends Plugin {
|
|||
|
||||
options.openNote = this.settings.openNoteInNewTab;
|
||||
|
||||
if (this.settings.imageDownload) {
|
||||
await this.downloadImageForMediaModel(mediaTypeModel);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -325,6 +423,40 @@ export default class MediaDbPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to download the image for a media model.
|
||||
*
|
||||
* @param mediaTypeModel
|
||||
* @returns true if the image was downloaded, false otherwise
|
||||
*/
|
||||
private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise<boolean> {
|
||||
if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) {
|
||||
try {
|
||||
const imageUrl = mediaTypeModel.image;
|
||||
const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg';
|
||||
const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`;
|
||||
const imagePath = normalizePath(`${this.settings.imageFolder}/${imageFileName}`);
|
||||
|
||||
if (!this.app.vault.getAbstractFileByPath(this.settings.imageFolder)) {
|
||||
await this.app.vault.createFolder(this.settings.imageFolder);
|
||||
}
|
||||
|
||||
if (!this.app.vault.getAbstractFileByPath(imagePath)) {
|
||||
const response = await requestUrl({ url: imageUrl, method: 'GET' });
|
||||
await this.app.vault.createBinary(imagePath, response.arrayBuffer);
|
||||
}
|
||||
|
||||
// Update model to use local image path
|
||||
mediaTypeModel.image = `[[${imagePath}]]`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('MDB | Failed to download image:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string {
|
||||
const fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject());
|
||||
return stringifyYaml(fileMetadata);
|
||||
|
|
@ -337,18 +469,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
* @param options
|
||||
*/
|
||||
async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise<string> {
|
||||
const template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app);
|
||||
|
||||
return this.generateContentWithDefaultFrontMatter(mediaTypeModel, options, template);
|
||||
|
||||
// if (this.settings.useDefaultFrontMatter || !template) {
|
||||
// return this.generateContentWithDefaultFrontMatter(mediaTypeModel, options, template);
|
||||
// } else {
|
||||
// return this.generateContentWithCustomFrontMatter(mediaTypeModel, options, template);
|
||||
// }
|
||||
}
|
||||
|
||||
async generateContentWithDefaultFrontMatter(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions, template?: string): Promise<string> {
|
||||
let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app);
|
||||
let fileMetadata: Record<string, unknown>;
|
||||
|
||||
if (this.settings.useDefaultFrontMatter) {
|
||||
|
|
@ -377,62 +498,12 @@ export default class MediaDbPlugin extends Plugin {
|
|||
return fileContent;
|
||||
}
|
||||
|
||||
async generateContentWithCustomFrontMatter(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions, template: string): Promise<string> {
|
||||
const regExp = new RegExp(this.frontMatterRexExpPattern);
|
||||
|
||||
const frontMatter = this.getMetaDataFromFileContent(template);
|
||||
let fileContent: string = template.replace(regExp, '');
|
||||
|
||||
// Updating a previous file
|
||||
if (options.attachFile) {
|
||||
const previousMetadata = this.app.metadataCache.getFileCache(options.attachFile)?.frontmatter ?? {};
|
||||
|
||||
// Use contents (below front matter) from previous file
|
||||
fileContent = await this.app.vault.read(options.attachFile);
|
||||
|
||||
fileContent = fileContent.replace(regExp, '');
|
||||
fileContent = fileContent.startsWith('\n') ? fileContent.substring(1) : fileContent;
|
||||
|
||||
// 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];
|
||||
|
||||
if (!frontMatter[key] && value) {
|
||||
frontMatter[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) {
|
||||
// Only support stringifyYaml for templater plugin
|
||||
// Include the media variable in all templater commands by using a top level JavaScript execution command.
|
||||
fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(frontMatter)}---\n${fileContent}`;
|
||||
} else {
|
||||
fileContent = `---\n${stringifyYaml(frontMatter)}---\n${fileContent}`;
|
||||
}
|
||||
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> {
|
||||
if (!fileToAttach) {
|
||||
return { fileMetadata: fileMetadata, fileContent: fileContent };
|
||||
}
|
||||
|
||||
const attachFileMetadata: any = this.getMetadataFromFileCache(fileToAttach);
|
||||
const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach);
|
||||
// TODO: better object merging
|
||||
fileMetadata = Object.assign(attachFileMetadata, fileMetadata);
|
||||
|
||||
|
|
@ -450,7 +521,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);
|
||||
|
||||
|
|
@ -476,7 +547,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 = {};
|
||||
|
|
@ -510,9 +581,17 @@ export default class MediaDbPlugin extends Plugin {
|
|||
fileName = replaceIllegalFileNameCharactersInString(fileName);
|
||||
const filePath = `${folder.path}/${fileName}.md`;
|
||||
|
||||
// find and delete file with the same name
|
||||
// look if file already exists and ask if it should be overwritten
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (file) {
|
||||
const shouldOverwrite = await new Promise<boolean>(resolve => {
|
||||
new ConfirmOverwriteModal(this.app, fileName, resolve).open();
|
||||
});
|
||||
|
||||
if (!shouldOverwrite) {
|
||||
throw new Error('MDB | file creation cancelled by user');
|
||||
}
|
||||
|
||||
await this.app.vault.delete(file);
|
||||
}
|
||||
|
||||
|
|
@ -520,7 +599,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
const targetFile = await this.app.vault.create(filePath, fileContent);
|
||||
console.debug(`MDB | created new file at ${filePath}`);
|
||||
|
||||
// open newly crated file
|
||||
// open newly created file
|
||||
if (options.openNote) {
|
||||
const activeLeaf = this.app.workspace.getUnpinnedLeaf();
|
||||
if (!activeLeaf) {
|
||||
|
|
@ -573,95 +652,9 @@ export default class MediaDbPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async createEntriesFromFolder(folder: TFolder): Promise<void> {
|
||||
const erroredFiles: { filePath: string; error: string }[] = [];
|
||||
let canceled: boolean = false;
|
||||
|
||||
const { selectedAPI, titleFieldName, appendContent } = await new Promise<{ selectedAPI: string; titleFieldName: string; appendContent: boolean }>(resolve => {
|
||||
new MediaDbFolderImportModal(this.app, this, (selectedAPI: string, titleFieldName: string, appendContent: boolean) => {
|
||||
resolve({ selectedAPI, titleFieldName, appendContent });
|
||||
}).open();
|
||||
});
|
||||
|
||||
for (const child of folder.children) {
|
||||
if (child instanceof TFile) {
|
||||
const file: TFile = child;
|
||||
if (canceled) {
|
||||
erroredFiles.push({ filePath: file.path, error: 'user canceled' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata: any = this.getMetadataFromFileCache(file);
|
||||
|
||||
const title = metadata[titleFieldName];
|
||||
if (!title) {
|
||||
erroredFiles.push({ filePath: file.path, error: `metadata field '${titleFieldName}' not found or empty` });
|
||||
continue;
|
||||
}
|
||||
|
||||
let results: MediaTypeModel[] = [];
|
||||
try {
|
||||
results = await this.apiManager.query(title, [selectedAPI]);
|
||||
} catch (e) {
|
||||
erroredFiles.push({ filePath: file.path, error: `${e}` });
|
||||
continue;
|
||||
}
|
||||
if (!results || results.length === 0) {
|
||||
erroredFiles.push({ filePath: file.path, error: `no search results` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const { selectModalResult, selectModal } = await this.modalHelper.createSelectModal({ elements: results, skipButton: true, modalTitle: `Results for '${title}'` });
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.ERROR) {
|
||||
erroredFiles.push({ filePath: file.path, error: selectModalResult.error.message });
|
||||
selectModal.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.CLOSE) {
|
||||
erroredFiles.push({ filePath: file.path, error: 'user canceled' });
|
||||
selectModal.close();
|
||||
canceled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.SKIP) {
|
||||
erroredFiles.push({ filePath: file.path, error: 'user skipped' });
|
||||
selectModal.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selectModalResult.data.selected.length === 0) {
|
||||
erroredFiles.push({ filePath: file.path, error: `no search results selected` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const detailedResults = await this.queryDetails(selectModalResult.data.selected);
|
||||
await this.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
|
||||
|
||||
selectModal.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (erroredFiles.length > 0) {
|
||||
await this.createErroredFilesReport(erroredFiles);
|
||||
}
|
||||
}
|
||||
|
||||
async createErroredFilesReport(erroredFiles: { filePath: string; error: string }[]): Promise<void> {
|
||||
const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`;
|
||||
const filePath = `${title}.md`;
|
||||
|
||||
const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error]));
|
||||
|
||||
const fileContent = `# ${title}\n\n${markdownTable(table)}`;
|
||||
await this.app.vault.create(filePath, fileContent);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -683,7 +676,9 @@ export default class MediaDbPlugin extends Plugin {
|
|||
newProperties.push(defaultProperty);
|
||||
} else {
|
||||
// newProperty is just an object and take locked status from default property
|
||||
newProperties.push(new PropertyMapping(newProperty.property, newProperty.newProperty, newProperty.mapping, defaultProperty.locked));
|
||||
newProperties.push(
|
||||
new PropertyMapping(newProperty.property, newProperty.newProperty, newProperty.mapping, defaultProperty.locked, newProperty.wikilink ?? false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
44
src/modals/ConfirmOverwriteModal.ts
Normal file
44
src/modals/ConfirmOverwriteModal.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Modal, Setting } from 'obsidian';
|
||||
|
||||
export class ConfirmOverwriteModal extends Modal {
|
||||
result: boolean = false;
|
||||
onSubmit: (result: boolean) => void;
|
||||
fileName: string;
|
||||
|
||||
constructor(app: App, fileName: string, onSubmit: (result: boolean) => void) {
|
||||
super(app);
|
||||
this.fileName = fileName;
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
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?` });
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
|
||||
const bottomSettingRow = new Setting(contentEl);
|
||||
bottomSettingRow.addButton(btn => {
|
||||
btn.setButtonText('No');
|
||||
btn.onClick(() => this.close());
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
});
|
||||
bottomSettingRow.addButton(btn => {
|
||||
btn.setButtonText('Yes');
|
||||
btn.setCta();
|
||||
btn.onClick(() => {
|
||||
this.result = true;
|
||||
this.close();
|
||||
});
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onSubmit(this.result);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
134
src/modals/MediaDbBulkImportModal.ts
Normal file
134
src/modals/MediaDbBulkImportModal.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import type { ButtonComponent } from 'obsidian';
|
||||
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import type { APIModel } from 'src/api/APIModel';
|
||||
import { BulkImportLookupMethod } from 'src/utils/BulkImportHelper';
|
||||
import type MediaDbPlugin from '../main';
|
||||
|
||||
export class MediaDbBulkImportModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void;
|
||||
selectedApi: string;
|
||||
searchBtn?: ButtonComponent;
|
||||
lookupMethod: BulkImportLookupMethod;
|
||||
fieldName: string;
|
||||
appendContent: boolean;
|
||||
|
||||
constructor(plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.onSubmit = onSubmit;
|
||||
this.selectedApi = plugin.apiManager.apis[0].apiName;
|
||||
this.lookupMethod = BulkImportLookupMethod.TITLE;
|
||||
this.fieldName = '';
|
||||
this.appendContent = false;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
this.onSubmit(this.selectedApi, this.lookupMethod, this.fieldName, this.appendContent);
|
||||
this.close();
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl('h2', { text: 'Import folder as Media DB entries' });
|
||||
|
||||
this.createDropdownEl(
|
||||
contentEl,
|
||||
'API to search',
|
||||
(value: string) => {
|
||||
this.selectedApi = value;
|
||||
},
|
||||
this.plugin.apiManager.apis.map((api: APIModel) => {
|
||||
return { value: api.apiName, display: api.apiName };
|
||||
}),
|
||||
);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' });
|
||||
|
||||
const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
appendContentToggleTextWrapper.createEl('span', {
|
||||
text: 'If this is enabled, the plugin will override metadata fields with the same name.',
|
||||
cls: 'media-db-plugin-list-text',
|
||||
});
|
||||
|
||||
const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' });
|
||||
|
||||
const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper);
|
||||
appendContentToggle.setValue(false);
|
||||
appendContentToggle.onChange(value => (this.appendContent = value));
|
||||
appendContentToggleComponentWrapper.appendChild(appendContentToggle.toggleEl);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
contentEl.createEl('h3', { text: 'Media lookup method' });
|
||||
contentEl.createEl('p', {
|
||||
text: 'Choose whether to search the API by title (can return multiple results) or lookup directly using an ID (returns at most one result), and specify the name of the frontmatter property which contains the title or ID of the media.',
|
||||
});
|
||||
|
||||
this.createDropdownEl(
|
||||
contentEl,
|
||||
'Lookup media by',
|
||||
(value: string) => {
|
||||
this.lookupMethod = value as BulkImportLookupMethod;
|
||||
},
|
||||
[
|
||||
{ value: BulkImportLookupMethod.TITLE, display: 'Title' },
|
||||
{ value: BulkImportLookupMethod.ID, display: 'ID' },
|
||||
],
|
||||
);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
|
||||
const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const fieldNameComponent = new TextComponent(fieldNameWrapperEl);
|
||||
fieldNameComponent.setPlaceholder('title / id');
|
||||
fieldNameComponent.onChange(value => (this.fieldName = value));
|
||||
fieldNameComponent.inputEl.addEventListener('keydown', ke => {
|
||||
if (ke.key === 'Enter') {
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
contentEl.appendChild(fieldNameWrapperEl);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton(btn => {
|
||||
btn.setButtonText('Cancel');
|
||||
btn.onClick(() => this.close());
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
})
|
||||
.addButton(btn => {
|
||||
btn.setButtonText('Ok');
|
||||
btn.setCta();
|
||||
btn.onClick(() => {
|
||||
this.submit();
|
||||
});
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
this.searchBtn = btn;
|
||||
});
|
||||
}
|
||||
|
||||
createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void {
|
||||
const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const dropDownComponent = new DropdownComponent(wrapperEl);
|
||||
dropDownComponent.onChange(onChange);
|
||||
for (const option of options) {
|
||||
dropDownComponent.addOption(option.value, option.display);
|
||||
}
|
||||
wrapperEl.appendChild(dropDownComponent.selectEl);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import type { App, ButtonComponent } from 'obsidian';
|
||||
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import type MediaDbPlugin from '../main';
|
||||
|
||||
export class MediaDbFolderImportModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void;
|
||||
selectedApi: string;
|
||||
searchBtn?: ButtonComponent;
|
||||
titleFieldName: string;
|
||||
appendContent: boolean;
|
||||
|
||||
constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.onSubmit = onSubmit;
|
||||
this.selectedApi = plugin.apiManager.apis[0].apiName;
|
||||
this.titleFieldName = '';
|
||||
this.appendContent = false;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
this.onSubmit(this.selectedApi, this.titleFieldName, this.appendContent);
|
||||
this.close();
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl('h2', { text: 'Import folder as Media DB entries' });
|
||||
|
||||
const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const apiSelectorTextWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
apiSelectorTextWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' });
|
||||
|
||||
const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper);
|
||||
apiSelectorComponent.onChange((value: string) => {
|
||||
this.selectedApi = value;
|
||||
});
|
||||
for (const api of this.plugin.apiManager.apis) {
|
||||
apiSelectorComponent.addOption(api.apiName, api.apiName);
|
||||
}
|
||||
apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
contentEl.createEl('h3', { text: 'Append note content to Media DB entry.' });
|
||||
|
||||
const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
|
||||
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
|
||||
appendContentToggleTextWrapper.createEl('span', {
|
||||
text: 'If this is enabled, the plugin will override metadata fields with the same name.',
|
||||
cls: 'media-db-plugin-list-text',
|
||||
});
|
||||
|
||||
const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' });
|
||||
|
||||
const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper);
|
||||
appendContentToggle.setValue(false);
|
||||
appendContentToggle.onChange(value => (this.appendContent = value));
|
||||
appendContentToggleComponentWrapper.appendChild(appendContentToggle.toggleEl);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
contentEl.createEl('h3', { text: 'The name of the metadata field that should be used as the title to query.' });
|
||||
|
||||
const placeholder = 'title';
|
||||
const titleFieldNameComponent = new TextComponent(contentEl);
|
||||
titleFieldNameComponent.inputEl.style.width = '100%';
|
||||
titleFieldNameComponent.setPlaceholder(placeholder);
|
||||
titleFieldNameComponent.onChange(value => (this.titleFieldName = value));
|
||||
titleFieldNameComponent.inputEl.addEventListener('keydown', ke => {
|
||||
if (ke.key === 'Enter') {
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
contentEl.appendChild(titleFieldNameComponent.inputEl);
|
||||
|
||||
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton(btn => {
|
||||
btn.setButtonText('Cancel');
|
||||
btn.onClick(() => this.close());
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
})
|
||||
.addButton(btn => {
|
||||
btn.setButtonText('Ok');
|
||||
btn.setCta();
|
||||
btn.onClick(() => {
|
||||
this.submit();
|
||||
});
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
this.searchBtn = btn;
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper';
|
||||
import { SELECT_MODAL_OPTIONS_DEFAULT } from '../utils/ModalHelper';
|
||||
import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper';
|
||||
import { SelectModal } from './SelectModal';
|
||||
|
||||
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
|
||||
|
|
@ -13,18 +13,17 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
|
|||
submitCallback?: (res: SelectModalData) => void;
|
||||
closeCallback?: (err?: Error) => void;
|
||||
skipCallback?: () => void;
|
||||
submitButtonText: string;
|
||||
|
||||
constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) {
|
||||
selectModalOptions = Object.assign({}, SELECT_MODAL_OPTIONS_DEFAULT, selectModalOptions);
|
||||
selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions);
|
||||
super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect);
|
||||
this.plugin = plugin;
|
||||
|
||||
this.title = selectModalOptions.modalTitle ?? '';
|
||||
this.description = 'Select one or multiple search results.';
|
||||
this.description = selectModalOptions.description ?? 'Select one or multiple search results.';
|
||||
this.addSkipButton = selectModalOptions.skipButton ?? false;
|
||||
|
||||
this.submitButtonText = selectModalOptions.submitButtonText ?? 'Ok';
|
||||
this.busy = false;
|
||||
|
||||
this.sendCallback = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
50
src/modals/MediaDbSeasonSelectModal.ts
Normal file
50
src/modals/MediaDbSeasonSelectModal.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type MediaDbPlugin from '../main';
|
||||
import { SelectModal } from './SelectModal';
|
||||
|
||||
export interface SeasonSelectModalElement {
|
||||
season_number: number;
|
||||
name: string;
|
||||
air_date?: string;
|
||||
poster_path?: string;
|
||||
}
|
||||
|
||||
export class MediaDbSeasonSelectModal extends SelectModal<SeasonSelectModalElement> {
|
||||
plugin: MediaDbPlugin;
|
||||
submitCallback?: (selectedSeasons: SeasonSelectModalElement[]) => void;
|
||||
closeCallback?: (err?: Error) => void;
|
||||
seriesName?: string;
|
||||
|
||||
constructor(plugin: MediaDbPlugin, seasons: SeasonSelectModalElement[], multiSelect = true, seriesName?: string) {
|
||||
super(plugin.app, seasons, multiSelect);
|
||||
this.plugin = plugin;
|
||||
this.seriesName = seriesName;
|
||||
this.title = `Select seasons for${seriesName ? ` ${seriesName}` : ''}`;
|
||||
this.description = 'Select one or more seasons to create notes for.';
|
||||
this.submitButtonText = 'Create Entry';
|
||||
}
|
||||
|
||||
renderElement(season: SeasonSelectModalElement, el: HTMLElement): void {
|
||||
el.createEl('div', { text: `${season.name}` });
|
||||
if (season.air_date) {
|
||||
el.createEl('small', { text: `Air date: ${season.air_date}` });
|
||||
}
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value);
|
||||
this.submitCallback?.(selected);
|
||||
this.close();
|
||||
}
|
||||
|
||||
skip(): void {
|
||||
this.close();
|
||||
}
|
||||
|
||||
setSubmitCallback(cb: (selectedSeasons: SeasonSelectModalElement[]) => void): void {
|
||||
this.submitCallback = cb;
|
||||
}
|
||||
|
||||
setCloseCallback(cb: (err?: Error) => void): void {
|
||||
this.closeCallback = cb;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ export abstract class SelectModal<T> extends Modal {
|
|||
cancelButton?: ButtonComponent;
|
||||
skipButton?: ButtonComponent;
|
||||
submitButton?: ButtonComponent;
|
||||
submitButtonText: string;
|
||||
|
||||
elementWrapper?: HTMLDivElement;
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ export abstract class SelectModal<T> extends Modal {
|
|||
this.title = '';
|
||||
this.description = '';
|
||||
this.addSkipButton = false;
|
||||
this.submitButtonText = 'Ok';
|
||||
this.cancelButton = undefined;
|
||||
this.skipButton = undefined;
|
||||
this.submitButton = undefined;
|
||||
|
|
@ -54,7 +56,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 +78,7 @@ export abstract class SelectModal<T> extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
onOpen(): void {
|
||||
const { contentEl, titleEl } = this;
|
||||
|
||||
titleEl.createEl('h2', { text: this.title });
|
||||
|
|
@ -115,7 +117,7 @@ export abstract class SelectModal<T> extends Modal {
|
|||
});
|
||||
}
|
||||
bottomSettingRow.addButton(btn => {
|
||||
btn.setButtonText('Ok');
|
||||
btn.setButtonText(this.submitButtonText);
|
||||
btn.setCta();
|
||||
btn.onClick(() => this.submit());
|
||||
btn.buttonEl.addClass('media-db-plugin-button');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export abstract class MediaTypeModel {
|
|||
dataSource: string;
|
||||
url: string;
|
||||
id: string;
|
||||
image?: string;
|
||||
|
||||
userData: object;
|
||||
|
||||
|
|
@ -21,6 +22,8 @@ export abstract class MediaTypeModel {
|
|||
this.dataSource = '';
|
||||
this.url = '';
|
||||
this.id = '';
|
||||
this.image = '';
|
||||
|
||||
this.userData = {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export class MovieModel extends MediaTypeModel {
|
|||
image: string;
|
||||
|
||||
released: boolean;
|
||||
country: string[];
|
||||
boxOffice: string;
|
||||
ageRating: string;
|
||||
streamingServices: string[];
|
||||
premiere: string;
|
||||
|
||||
|
|
@ -40,6 +43,9 @@ export class MovieModel extends MediaTypeModel {
|
|||
this.image = '';
|
||||
|
||||
this.released = false;
|
||||
this.country = [];
|
||||
this.boxOffice = '';
|
||||
this.ageRating = '';
|
||||
this.streamingServices = [];
|
||||
this.premiere = '';
|
||||
|
||||
|
|
@ -51,7 +57,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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,18 @@ export type MusicReleaseData = ModelToData<MusicReleaseModel>;
|
|||
export class MusicReleaseModel extends MediaTypeModel {
|
||||
genres: string[];
|
||||
artists: string[];
|
||||
language: string;
|
||||
image: string;
|
||||
rating: number;
|
||||
releaseDate: string;
|
||||
albumDuration: string;
|
||||
trackCount: number;
|
||||
tracks: {
|
||||
number: number;
|
||||
title: string;
|
||||
duration: string;
|
||||
featuredArtists: string[];
|
||||
}[];
|
||||
|
||||
userData: {
|
||||
personalRating: number;
|
||||
|
|
@ -22,17 +32,23 @@ export class MusicReleaseModel extends MediaTypeModel {
|
|||
this.artists = [];
|
||||
this.image = '';
|
||||
this.rating = 0;
|
||||
this.releaseDate = '';
|
||||
|
||||
this.userData = {
|
||||
personalRating: 0,
|
||||
};
|
||||
|
||||
migrateObject(this, obj, this);
|
||||
|
||||
if (!obj.hasOwnProperty('userData')) {
|
||||
if (!Object.hasOwn(obj, 'userData')) {
|
||||
migrateObject(this.userData, obj, this.userData);
|
||||
}
|
||||
|
||||
this.type = this.getMediaType();
|
||||
this.albumDuration = obj.albumDuration ?? '0:00';
|
||||
this.trackCount = obj.trackCount ?? 0;
|
||||
this.tracks = obj.tracks ?? [];
|
||||
this.language = obj.language ?? '';
|
||||
}
|
||||
|
||||
getTags(): string[] {
|
||||
|
|
|
|||
80
src/models/SeasonModel.ts
Normal file
80
src/models/SeasonModel.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { MediaType } from '../utils/MediaType';
|
||||
import type { ModelToData } from '../utils/Utils';
|
||||
import { mediaDbTag, migrateObject } from '../utils/Utils';
|
||||
import { MediaTypeModel } from './MediaTypeModel';
|
||||
|
||||
export type SeasonData = ModelToData<SeasonModel>;
|
||||
|
||||
export class SeasonModel extends MediaTypeModel {
|
||||
seasonNumber: number;
|
||||
seasonTitle: string;
|
||||
episodes: number;
|
||||
|
||||
plot: string;
|
||||
genres: string[];
|
||||
writer: string[];
|
||||
studio: string[];
|
||||
duration: string;
|
||||
onlineRating: number;
|
||||
actors: string[];
|
||||
image: string;
|
||||
|
||||
released: boolean;
|
||||
streamingServices: string[];
|
||||
airing: boolean;
|
||||
airedFrom: string;
|
||||
airedTo: string;
|
||||
|
||||
userData: {
|
||||
watched: boolean;
|
||||
lastWatched: string;
|
||||
personalRating: number;
|
||||
};
|
||||
|
||||
constructor(obj: SeasonData) {
|
||||
super();
|
||||
this.seasonTitle = '';
|
||||
this.seasonNumber = 0;
|
||||
this.episodes = 0;
|
||||
this.plot = '';
|
||||
this.genres = [];
|
||||
this.writer = [];
|
||||
this.studio = [];
|
||||
this.duration = '';
|
||||
this.onlineRating = 0;
|
||||
this.actors = [];
|
||||
this.image = '';
|
||||
|
||||
this.released = false;
|
||||
this.streamingServices = [];
|
||||
this.airing = false;
|
||||
this.airedFrom = '';
|
||||
this.airedTo = '';
|
||||
|
||||
this.userData = {
|
||||
watched: false,
|
||||
lastWatched: '',
|
||||
personalRating: 0,
|
||||
};
|
||||
|
||||
migrateObject(this, obj, this);
|
||||
|
||||
if (!obj.hasOwnProperty('userData')) {
|
||||
migrateObject(this.userData, obj, this.userData);
|
||||
}
|
||||
|
||||
this.type = this.getMediaType();
|
||||
}
|
||||
|
||||
getTags(): string[] {
|
||||
return [mediaDbTag, 'tv', 'season'];
|
||||
}
|
||||
|
||||
getMediaType(): MediaType {
|
||||
return MediaType.Season;
|
||||
}
|
||||
|
||||
getSummary(): string {
|
||||
return this.seasonNumber + ' seasons';
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ export class SeriesModel extends MediaTypeModel {
|
|||
image: string;
|
||||
|
||||
released: boolean;
|
||||
country: string[];
|
||||
ageRating: string;
|
||||
streamingServices: string[];
|
||||
airing: boolean;
|
||||
airedFrom: string;
|
||||
|
|
@ -42,6 +44,8 @@ export class SeriesModel extends MediaTypeModel {
|
|||
this.image = '';
|
||||
|
||||
this.released = false;
|
||||
this.country = [];
|
||||
this.ageRating = '';
|
||||
this.streamingServices = [];
|
||||
this.airing = false;
|
||||
this.airedFrom = '';
|
||||
|
|
@ -55,7 +59,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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -34,14 +35,23 @@ export class PropertyMapper {
|
|||
for (const [key, value] of Object.entries(obj)) {
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.property === key) {
|
||||
let finalValue = value;
|
||||
if (propertyMapping.wikilink) {
|
||||
if (typeof value === 'string') {
|
||||
finalValue = `[[${value}]]`;
|
||||
} else if (Array.isArray(value)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
finalValue = value.map(v => (typeof v === 'string' ? `[[${v}]]` : v));
|
||||
}
|
||||
}
|
||||
if (propertyMapping.mapping === PropertyMappingOption.Map) {
|
||||
// @ts-ignore
|
||||
newObj[propertyMapping.newProperty] = value;
|
||||
newObj[propertyMapping.newProperty] = finalValue;
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Remove) {
|
||||
// do nothing
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Default) {
|
||||
// @ts-ignore
|
||||
newObj[key] = value;
|
||||
newObj[key] = finalValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -58,7 +68,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 +76,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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export class PropertyMappingModel {
|
|||
copy(): PropertyMappingModel {
|
||||
const copy = new PropertyMappingModel(this.type);
|
||||
for (const property of this.properties) {
|
||||
const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked);
|
||||
const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked, property.wikilink);
|
||||
copy.properties.push(propertyCopy);
|
||||
}
|
||||
return copy;
|
||||
|
|
@ -87,12 +87,14 @@ export class PropertyMapping {
|
|||
newProperty: string;
|
||||
locked: boolean;
|
||||
mapping: PropertyMappingOption;
|
||||
wikilink: boolean;
|
||||
|
||||
constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean) {
|
||||
constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean, wikilink?: boolean) {
|
||||
this.property = property;
|
||||
this.newProperty = newProperty;
|
||||
this.mapping = mapping;
|
||||
this.locked = locked ?? false;
|
||||
this.wikilink = wikilink ?? false;
|
||||
}
|
||||
|
||||
validate(): { res: boolean; err?: Error } {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -12,55 +10,152 @@
|
|||
|
||||
let { model, save }: Props = $props();
|
||||
|
||||
let validationResult: { res: boolean; err?: Error } | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
validationResult = model.validate();
|
||||
});
|
||||
let validationResult: { res: boolean; err?: Error } | undefined = $derived(model.validate());
|
||||
</script>
|
||||
|
||||
<div class="media-db-plugin-property-mappings-model-container">
|
||||
<div class="setting-item-name">{capitalizeFirstLetter(model.type)}</div>
|
||||
<div class="media-db-plugin-property-mappings-container">
|
||||
|
||||
<table class="media-db-plugin-property-mappings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-property">Property</th>
|
||||
<th class="col-mapping">Mapping</th>
|
||||
<th class="col-new-name">New name</th>
|
||||
<th class="col-wikilink">Wikilink</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each model.properties as property}
|
||||
<div class="media-db-plugin-property-mapping-element">
|
||||
<div class="media-db-plugin-property-mapping-element-property-name-wrapper">
|
||||
<pre class="media-db-plugin-property-mapping-element-property-name"><code>{property.property}</code></pre>
|
||||
</div>
|
||||
<tr>
|
||||
<td class="col-property">
|
||||
<code>{property.property}</code>
|
||||
</td>
|
||||
|
||||
{#if property.locked}
|
||||
<td class="col-locked" colspan="3">
|
||||
<div class="media-db-plugin-property-binding-text">property cannot be remapped</div>
|
||||
</td>
|
||||
{:else}
|
||||
<select class="dropdown" bind:value={property.mapping}>
|
||||
<td class="col-mapping">
|
||||
<select
|
||||
class="dropdown"
|
||||
bind:value={property.mapping}
|
||||
onchange={() => {
|
||||
model = model.copy();
|
||||
}}
|
||||
>
|
||||
{#each propertyMappingOptions as remappingOption}
|
||||
<option value={remappingOption}>
|
||||
{remappingOption}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<td class="col-new-name">
|
||||
{#if property.mapping === PropertyMappingOption.Map}
|
||||
<Icon iconName="arrow-right" />
|
||||
<div class="media-db-plugin-property-mapping-to">
|
||||
<input type="text" spellcheck="false" bind:value={property.newProperty} />
|
||||
<Icon iconName="arrow-right" />
|
||||
<input class="media-db-plugin-property-mapping-input" type="text" spellcheck="false" bind:value={property.newProperty} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="media-db-plugin-property-mapping-to-disabled">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<td class="col-wikilink">
|
||||
<label class="media-db-plugin-property-mapping-wikilink-label" title="Convert value to wikilink ([[value]])">
|
||||
<input type="checkbox" bind:checked={property.wikilink} />
|
||||
</label>
|
||||
</td>
|
||||
{/if}
|
||||
</div>
|
||||
</tr>
|
||||
{/each}
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{#if !validationResult?.res}
|
||||
<div class="media-db-plugin-property-mapping-validation">
|
||||
{validationResult?.err?.message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="media-db-plugin-property-mappings-save-button {validationResult?.res ? 'mod-cta' : 'mod-muted'}"
|
||||
onclick={() => {
|
||||
if (model.validate().res) save(model);
|
||||
}}
|
||||
>Save
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.media-db-plugin-property-mappings-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed; /* prevent overflow from wide cells */
|
||||
}
|
||||
|
||||
/* remove excessive left padding and keep within container */
|
||||
.media-db-plugin-property-mappings-table th,
|
||||
.media-db-plugin-property-mappings-table td {
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* column widths */
|
||||
.col-property {
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-mapping {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.col-new-name {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.col-wikilink {
|
||||
width: 15%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ensure inner controls don't push table wider than container */
|
||||
.media-db-plugin-property-mapping-to {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-input,
|
||||
.media-db-plugin-property-mappings-table select.dropdown {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-wikilink-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.95em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-db-plugin-property-mapping-to-disabled {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* avoid extra left indentation from <pre> */
|
||||
.media-db-plugin-property-mappings-table code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
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';
|
||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||
import { fragWithHTML } from '../utils/Utils';
|
||||
import { fragWithHTML, unCamelCase } from '../utils/Utils';
|
||||
import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping';
|
||||
import PropertyMappingModelsComponent from './PropertyMappingModelsComponent.svelte';
|
||||
import { FileSuggest } from './suggesters/FileSuggest';
|
||||
|
|
@ -12,35 +13,34 @@ import { FolderSuggest } from './suggesters/FolderSuggest';
|
|||
|
||||
export interface MediaDbPluginSettings {
|
||||
OMDbKey: string;
|
||||
TMDBKey: string;
|
||||
MobyGamesKey: string;
|
||||
GiantBombKey: string;
|
||||
ComicVineKey: string;
|
||||
BoardgameGeekKey: string;
|
||||
sfwFilter: boolean;
|
||||
templates: boolean;
|
||||
customDateFormat: string;
|
||||
openNoteInNewTab: boolean;
|
||||
useDefaultFrontMatter: boolean;
|
||||
enableTemplaterIntegration: boolean;
|
||||
// TODO: disabled for now, as i currently don't have the time to fix this from the original PR that introduced it (#133)
|
||||
// apiToggle: {
|
||||
// OMDbAPI: {
|
||||
// movie: boolean;
|
||||
// series: boolean;
|
||||
// game: boolean;
|
||||
// };
|
||||
// MALAPI: {
|
||||
// movie: boolean;
|
||||
// series: boolean;
|
||||
// };
|
||||
// SteamAPI: {
|
||||
// game: boolean;
|
||||
// };
|
||||
// MobyGamesAPI: {
|
||||
// game: boolean;
|
||||
// };
|
||||
// };
|
||||
OMDbAPI_disabledMediaTypes: MediaType[];
|
||||
TMDBSeriesAPI_disabledMediaTypes: MediaType[];
|
||||
TMDBSeasonAPI_disabledMediaTypes: MediaType[];
|
||||
TMDBMovieAPI_disabledMediaTypes: MediaType[];
|
||||
MALAPI_disabledMediaTypes: MediaType[];
|
||||
MALAPIManga_disabledMediaTypes: MediaType[];
|
||||
ComicVineAPI_disabledMediaTypes: MediaType[];
|
||||
SteamAPI_disabledMediaTypes: MediaType[];
|
||||
MobyGamesAPI_disabledMediaTypes: MediaType[];
|
||||
GiantBombAPI_disabledMediaTypes: MediaType[];
|
||||
WikipediaAPI_disabledMediaTypes: MediaType[];
|
||||
BoardgameGeekAPI_disabledMediaTypes: MediaType[];
|
||||
MusicBrainzAPI_disabledMediaTypes: MediaType[];
|
||||
OpenLibraryAPI_disabledMediaTypes: MediaType[];
|
||||
movieTemplate: string;
|
||||
seriesTemplate: string;
|
||||
seasonTemplate: string;
|
||||
mangaTemplate: string;
|
||||
gameTemplate: string;
|
||||
wikiTemplate: string;
|
||||
|
|
@ -50,6 +50,7 @@ export interface MediaDbPluginSettings {
|
|||
|
||||
movieFileNameTemplate: string;
|
||||
seriesFileNameTemplate: string;
|
||||
seasonFileNameTemplate: string;
|
||||
mangaFileNameTemplate: string;
|
||||
gameFileNameTemplate: string;
|
||||
wikiFileNameTemplate: string;
|
||||
|
|
@ -59,6 +60,7 @@ export interface MediaDbPluginSettings {
|
|||
|
||||
moviePropertyConversionRules: string;
|
||||
seriesPropertyConversionRules: string;
|
||||
seasonPropertyConversionRules: string;
|
||||
mangaPropertyConversionRules: string;
|
||||
gamePropertyConversionRules: string;
|
||||
wikiPropertyConversionRules: string;
|
||||
|
|
@ -68,6 +70,7 @@ export interface MediaDbPluginSettings {
|
|||
|
||||
movieFolder: string;
|
||||
seriesFolder: string;
|
||||
seasonFolder: string;
|
||||
mangaFolder: string;
|
||||
gameFolder: string;
|
||||
wikiFolder: string;
|
||||
|
|
@ -75,39 +78,41 @@ export interface MediaDbPluginSettings {
|
|||
boardgameFolder: string;
|
||||
bookFolder: string;
|
||||
|
||||
imageDownload: boolean;
|
||||
imageFolder: string;
|
||||
propertyMappingModels: PropertyMappingModel[];
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
||||
OMDbKey: '',
|
||||
TMDBKey: '',
|
||||
MobyGamesKey: '',
|
||||
GiantBombKey: '',
|
||||
ComicVineKey: '',
|
||||
BoardgameGeekKey: '',
|
||||
sfwFilter: true,
|
||||
templates: true,
|
||||
customDateFormat: 'L',
|
||||
openNoteInNewTab: true,
|
||||
useDefaultFrontMatter: true,
|
||||
enableTemplaterIntegration: false,
|
||||
// apiToggle: {
|
||||
// OMDbAPI: {
|
||||
// movie: true,
|
||||
// series: true,
|
||||
// game: true,
|
||||
// },
|
||||
// MALAPI: {
|
||||
// movie: true,
|
||||
// series: true,
|
||||
// },
|
||||
// SteamAPI: {
|
||||
// game: true,
|
||||
// },
|
||||
// MobyGamesAPI: {
|
||||
// game: true,
|
||||
// },
|
||||
// },
|
||||
OMDbAPI_disabledMediaTypes: [],
|
||||
TMDBSeriesAPI_disabledMediaTypes: [],
|
||||
TMDBSeasonAPI_disabledMediaTypes: [],
|
||||
TMDBMovieAPI_disabledMediaTypes: [],
|
||||
MALAPI_disabledMediaTypes: [],
|
||||
MALAPIManga_disabledMediaTypes: [],
|
||||
ComicVineAPI_disabledMediaTypes: [],
|
||||
SteamAPI_disabledMediaTypes: [],
|
||||
MobyGamesAPI_disabledMediaTypes: [],
|
||||
GiantBombAPI_disabledMediaTypes: [],
|
||||
WikipediaAPI_disabledMediaTypes: [],
|
||||
BoardgameGeekAPI_disabledMediaTypes: [],
|
||||
MusicBrainzAPI_disabledMediaTypes: [],
|
||||
OpenLibraryAPI_disabledMediaTypes: [],
|
||||
movieTemplate: '',
|
||||
seriesTemplate: '',
|
||||
seasonTemplate: '',
|
||||
mangaTemplate: '',
|
||||
gameTemplate: '',
|
||||
wikiTemplate: '',
|
||||
|
|
@ -117,6 +122,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
|||
|
||||
movieFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
seriesFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
seasonFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
mangaFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
gameFileNameTemplate: '{{ title }} ({{ year }})',
|
||||
wikiFileNameTemplate: '{{ title }}',
|
||||
|
|
@ -126,6 +132,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
|||
|
||||
moviePropertyConversionRules: '',
|
||||
seriesPropertyConversionRules: '',
|
||||
seasonPropertyConversionRules: '',
|
||||
mangaPropertyConversionRules: '',
|
||||
gamePropertyConversionRules: '',
|
||||
wikiPropertyConversionRules: '',
|
||||
|
|
@ -135,6 +142,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
|||
|
||||
movieFolder: 'Media DB/movies',
|
||||
seriesFolder: 'Media DB/series',
|
||||
seasonFolder: 'Media DB/series',
|
||||
mangaFolder: 'Media DB/comics',
|
||||
gameFolder: 'Media DB/games',
|
||||
wikiFolder: 'Media DB/wiki',
|
||||
|
|
@ -142,6 +150,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
|||
boardgameFolder: 'Media DB/boardgames',
|
||||
bookFolder: 'Media DB/books',
|
||||
|
||||
imageDownload: false,
|
||||
imageFolder: 'Media DB/images',
|
||||
propertyMappingModels: [],
|
||||
};
|
||||
|
||||
|
|
@ -161,16 +171,54 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings
|
|||
const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType);
|
||||
|
||||
for (const key of Object.keys(metadataObj)) {
|
||||
propertyMappingModel.properties.push(new PropertyMapping(key, '', PropertyMappingOption.Default, lockedPropertyMappings.contains(key)));
|
||||
propertyMappingModel.properties.push(
|
||||
new PropertyMapping(
|
||||
key,
|
||||
'',
|
||||
PropertyMappingOption.Default,
|
||||
lockedPropertyMappings.contains(key),
|
||||
false, // wikilink default
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
propertyMappingModels.push(propertyMappingModel);
|
||||
}
|
||||
|
||||
// MIGRATION: Ensure all property mappings have wikilink defined (for settings loaded from disk)
|
||||
if (defaultSettings.propertyMappingModels && Array.isArray(defaultSettings.propertyMappingModels)) {
|
||||
for (const model of defaultSettings.propertyMappingModels) {
|
||||
if (model.properties && Array.isArray(model.properties)) {
|
||||
for (const prop of model.properties) {
|
||||
if (typeof prop.wikilink === 'undefined') {
|
||||
prop.wikilink = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultSettings.propertyMappingModels = propertyMappingModels;
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all property mappings in loaded settings have the wikilink property defined.
|
||||
*/
|
||||
export function ensureWikilinkOnPropertyMappings(settings: MediaDbPluginSettings): void {
|
||||
if (settings.propertyMappingModels && Array.isArray(settings.propertyMappingModels)) {
|
||||
for (const model of settings.propertyMappingModels) {
|
||||
if (model.properties && Array.isArray(model.properties)) {
|
||||
for (const prop of model.properties) {
|
||||
if (typeof prop.wikilink === 'undefined') {
|
||||
prop.wikilink = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaDbSettingTab extends PluginSettingTab {
|
||||
plugin: MediaDbPlugin;
|
||||
|
||||
|
|
@ -195,6 +243,17 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName('TMDB API key')
|
||||
.setDesc('API key for "https://www.themoviedb.org".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.TMDBKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.TMDBKey = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Moby Games key')
|
||||
|
|
@ -230,6 +289,17 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName('Boardgame Geek Key')
|
||||
.setDesc('API key for "www.boardgamegeek.com".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.BoardgameGeekKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.BoardgameGeekKey = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('SFW filter')
|
||||
|
|
@ -310,89 +380,91 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
// containerEl.createEl('h3', { text: 'APIs per media type' });
|
||||
// containerEl.createEl('h5', { text: 'Movies' });
|
||||
// new Setting(containerEl)
|
||||
// .setName('OMDb API')
|
||||
// .setDesc('Use OMDb API for movies.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.OMDbAPI.movie).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.OMDbAPI.movie = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// new Setting(containerEl)
|
||||
// .setName('MAL API')
|
||||
// .setDesc('Use MAL API for movies.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.MALAPI.movie).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.MALAPI.movie = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// containerEl.createEl('h5', { text: 'Series' });
|
||||
// new Setting(containerEl)
|
||||
// .setName('OMDb API')
|
||||
// .setDesc('Use OMDb API for series.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.OMDbAPI.series).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.OMDbAPI.series = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// new Setting(containerEl)
|
||||
// .setName('MAL API')
|
||||
// .setDesc('Use MAL API for series.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.MALAPI.series).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.MALAPI.series = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// containerEl.createEl('h5', { text: 'Games' });
|
||||
// new Setting(containerEl)
|
||||
// .setName('OMDb API')
|
||||
// .setDesc('Use OMDb API for games.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.OMDbAPI.game).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.OMDbAPI.game = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// new Setting(containerEl)
|
||||
// .setName('Steam API')
|
||||
// .setDesc('Use OMDb API for games.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.SteamAPI.game).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.SteamAPI.game = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// new Setting(containerEl)
|
||||
// .setName('MobyGames API')
|
||||
// .setDesc('Use MobyGames API for games.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.MobyGamesAPI.game).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.MobyGamesAPI.game = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
// new Setting(containerEl)
|
||||
// .setName('Giantbomb API')
|
||||
// .setDesc('Use Giantbomb API for games.')
|
||||
// .addToggle(cb => {
|
||||
// cb.setValue(this.plugin.settings.apiToggle.GiantBombAPI.game).onChange(data => {
|
||||
// this.plugin.settings.apiToggle.GiantBombAPI.game = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
// });
|
||||
new Setting(containerEl)
|
||||
.setName('Download images')
|
||||
.setDesc('Downloads images for new notes in the folder below')
|
||||
.addToggle(cb => {
|
||||
cb.setValue(this.plugin.settings.imageDownload).onChange(data => {
|
||||
this.plugin.settings.imageDownload = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Image folder')
|
||||
.setDesc('Where downloaded images should be stored.')
|
||||
.addSearch(cb => {
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.imageFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.imageFolder)
|
||||
.setValue(this.plugin.settings.imageFolder)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.imageFolder = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Create a map to store APIs for each media type
|
||||
const mediaTypeApiMap = new Map<MediaType, string[]>();
|
||||
|
||||
// Populate the map with APIs for each media type dynamically
|
||||
for (const api of this.plugin.apiManager.apis) {
|
||||
for (const mediaType of api.types) {
|
||||
if (!mediaTypeApiMap.has(mediaType)) {
|
||||
mediaTypeApiMap.set(mediaType, []);
|
||||
}
|
||||
mediaTypeApiMap.get(mediaType)!.push(api.apiName);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out media types with only one API
|
||||
const filteredMediaTypes = Array.from(mediaTypeApiMap.entries()).filter(([_, apis]) => apis.length > 1);
|
||||
|
||||
// Dynamically create settings based on the filtered media types and their APIs
|
||||
for (const [mediaType, apis] of filteredMediaTypes) {
|
||||
new Setting(containerEl).setName(`Select APIs for ${unCamelCase(mediaType)}`).setHeading();
|
||||
for (const apiName of apis) {
|
||||
const api = this.plugin.apiManager.apis.find(api => api.apiName === apiName);
|
||||
if (api) {
|
||||
const disabledMediaTypes = api.getDisabledMediaTypes();
|
||||
new Setting(containerEl)
|
||||
.setName(apiName)
|
||||
.setDesc(`Use ${apiName} API for ${unCamelCase(mediaType)}.`)
|
||||
.addToggle(cb => {
|
||||
cb.setValue(!disabledMediaTypes.includes(mediaType)).onChange(data => {
|
||||
if (data) {
|
||||
const index = disabledMediaTypes.indexOf(mediaType);
|
||||
if (index > -1) {
|
||||
disabledMediaTypes.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
disabledMediaTypes.push(mediaType);
|
||||
}
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Setting(containerEl).setName('New file location').setHeading();
|
||||
// region new file location
|
||||
new Setting(containerEl)
|
||||
.setName('Movie folder')
|
||||
.setDesc('Where newly imported movies should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.movieFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.movieFolder)
|
||||
.setValue(this.plugin.settings.movieFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -405,7 +477,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Series folder')
|
||||
.setDesc('Where newly imported series should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.seriesFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.seriesFolder)
|
||||
.setValue(this.plugin.settings.seriesFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -414,11 +492,30 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Season folder')
|
||||
.setDesc('Where newly imported seasons should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.seasonFolder)
|
||||
.setValue(this.plugin.settings.seriesFolder)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.seasonFolder = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Comic and manga folder')
|
||||
.setDesc('Where newly imported comics and manga should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.mangaFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.mangaFolder)
|
||||
.setValue(this.plugin.settings.mangaFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -431,7 +528,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Game folder')
|
||||
.setDesc('Where newly imported games should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.gameFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.gameFolder)
|
||||
.setValue(this.plugin.settings.gameFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -444,7 +547,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Wiki folder')
|
||||
.setDesc('Where newly imported wiki articles should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.wikiFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.wikiFolder)
|
||||
.setValue(this.plugin.settings.wikiFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -457,7 +566,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Music folder')
|
||||
.setDesc('Where newly imported music should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.musicReleaseFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.musicReleaseFolder)
|
||||
.setValue(this.plugin.settings.musicReleaseFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -470,7 +585,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Board game folder')
|
||||
.setDesc('Where newly imported board games should be places.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.boardgameFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.boardgameFolder)
|
||||
.setValue(this.plugin.settings.boardgameFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -482,7 +603,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Book folder')
|
||||
.setDesc('Where newly imported books should be placed.')
|
||||
.addSearch(cb => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FolderSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(folder => {
|
||||
cb.setValue(folder.path);
|
||||
this.plugin.settings.bookFolder = folder.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder(DEFAULT_SETTINGS.bookFolder)
|
||||
.setValue(this.plugin.settings.bookFolder)
|
||||
.onChange(data => {
|
||||
|
|
@ -499,7 +626,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Movie template')
|
||||
.setDesc('Template file to be used when creating a new note for a movie.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.movieTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: movieTemplate.md')
|
||||
.setValue(this.plugin.settings.movieTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -512,7 +645,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Series template')
|
||||
.setDesc('Template file to be used when creating a new note for a series.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.seriesTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: seriesTemplate.md')
|
||||
.setValue(this.plugin.settings.seriesTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -521,11 +660,30 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Season template')
|
||||
.setDesc('Template file to be used when creating a new note for a season.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
cb.setPlaceholder('Example: seasonTemplate.md')
|
||||
.setValue(this.plugin.settings.seasonTemplate)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.seasonTemplate = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Manga and Comics template')
|
||||
.setDesc('Template file to be used when creating a new note for a manga or a comic.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.mangaTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: mangaTemplate.md')
|
||||
.setValue(this.plugin.settings.mangaTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -538,7 +696,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Game template')
|
||||
.setDesc('Template file to be used when creating a new note for a game.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.gameTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: gameTemplate.md')
|
||||
.setValue(this.plugin.settings.gameTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -551,7 +715,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Wiki template')
|
||||
.setDesc('Template file to be used when creating a new note for a wiki entry.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.wikiTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: wikiTemplate.md')
|
||||
.setValue(this.plugin.settings.wikiTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -564,7 +734,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Music release template')
|
||||
.setDesc('Template file to be used when creating a new note for a music release.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.musicReleaseTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: musicReleaseTemplate.md')
|
||||
.setValue(this.plugin.settings.musicReleaseTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -577,7 +753,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Board game template')
|
||||
.setDesc('Template file to be used when creating a new note for a boardgame.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.boardgameTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: boardgameTemplate.md')
|
||||
.setValue(this.plugin.settings.boardgameTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -590,7 +772,13 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
.setName('Book template')
|
||||
.setDesc('Template file to be used when creating a new note for a book.')
|
||||
.addSearch(cb => {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
const suggester = new FileSuggest(this.app, cb.inputEl);
|
||||
suggester.onSelect(file => {
|
||||
cb.setValue(file.path);
|
||||
this.plugin.settings.bookTemplate = file.path;
|
||||
void this.plugin.saveSettings();
|
||||
suggester.close();
|
||||
});
|
||||
cb.setPlaceholder('Example: bookTemplate.md')
|
||||
.setValue(this.plugin.settings.bookTemplate)
|
||||
.onChange(data => {
|
||||
|
|
@ -627,6 +815,18 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Season file name template')
|
||||
.setDesc('Template for the file name used when creating a new note for a season.')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.seasonFileNameTemplate}`)
|
||||
.setValue(this.plugin.settings.seasonFileNameTemplate)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.seasonFileNameTemplate = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Manga and comic file name template')
|
||||
.setDesc('Template for the file name used when creating a new note for a manga or comic.')
|
||||
|
|
|
|||
|
|
@ -1,29 +1,17 @@
|
|||
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[] {
|
||||
const abstractFiles = this.app.vault.getAllLoadedFiles();
|
||||
const files: TFile[] = [];
|
||||
const lowerCaseInputStr = inputStr.toLowerCase();
|
||||
export class FileSuggest extends AbstractInputSuggest<TFile> {
|
||||
protected getSuggestions(query: string): TFile[] | Promise<TFile[]> {
|
||||
const lowerCaseInputStr = query.toLowerCase();
|
||||
|
||||
abstractFiles.forEach((file: TAbstractFile) => {
|
||||
if (file instanceof TFile && file.name.toLowerCase().contains(lowerCaseInputStr)) {
|
||||
files.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
// we do two filters because otherwise TS type inference does convert the array to TFile[]
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(file => file instanceof TFile)
|
||||
.filter(file => file.path.toLowerCase().contains(lowerCaseInputStr));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,17 @@
|
|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||
import { AbstractInputSuggest, TFolder } from 'obsidian';
|
||||
|
||||
import type { TAbstractFile } from 'obsidian';
|
||||
import { TFolder } from 'obsidian';
|
||||
import { TextInputSuggest } from './Suggest';
|
||||
export class FolderSuggest extends AbstractInputSuggest<TFolder> {
|
||||
protected getSuggestions(query: string): TFolder[] | Promise<TFolder[]> {
|
||||
const lowerCaseInputStr = query.toLowerCase();
|
||||
|
||||
export class FolderSuggest extends TextInputSuggest<TFolder> {
|
||||
getSuggestions(inputStr: string): TFolder[] {
|
||||
const abstractFiles = this.app.vault.getAllLoadedFiles();
|
||||
const folders: TFolder[] = [];
|
||||
const lowerCaseInputStr = inputStr.toLowerCase();
|
||||
|
||||
abstractFiles.forEach((folder: TAbstractFile) => {
|
||||
if (folder instanceof TFolder && folder.path.toLowerCase().contains(lowerCaseInputStr)) {
|
||||
folders.push(folder);
|
||||
}
|
||||
});
|
||||
|
||||
return folders;
|
||||
// we do two filters because otherwise TS type inference does convert the array to TFolder[]
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(file => file instanceof TFolder)
|
||||
.filter(file => file.path.toLowerCase().contains(lowerCaseInputStr));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
150
src/utils/BulkImportHelper.ts
Normal file
150
src/utils/BulkImportHelper.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import type { TFolder } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
import type MediaDbPlugin from 'src/main';
|
||||
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal';
|
||||
import type { MediaTypeModel } from 'src/models/MediaTypeModel';
|
||||
import { ModalResultCode } from './ModalHelper';
|
||||
import { dateTimeToString, markdownTable } from './Utils';
|
||||
|
||||
export enum BulkImportLookupMethod {
|
||||
ID = 'id',
|
||||
TITLE = 'title',
|
||||
}
|
||||
|
||||
interface BulkImportError {
|
||||
filePath: string;
|
||||
error: string;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export class BulkImportHelper {
|
||||
readonly plugin: MediaDbPlugin;
|
||||
|
||||
constructor(plugin: MediaDbPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
async import(folder: TFolder): Promise<void> {
|
||||
const erroredFiles: BulkImportError[] = [];
|
||||
let canceled: boolean = false;
|
||||
|
||||
const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{
|
||||
selectedAPI: string;
|
||||
lookupMethod: BulkImportLookupMethod;
|
||||
fieldName: string;
|
||||
appendContent: boolean;
|
||||
}>(resolve => {
|
||||
new MediaDbBulkImportModal(this.plugin, (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => {
|
||||
resolve({ selectedAPI, lookupMethod, fieldName, appendContent });
|
||||
}).open();
|
||||
});
|
||||
|
||||
for (const child of folder.children) {
|
||||
if (!(child instanceof TFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const file: TFile = child;
|
||||
if (canceled) {
|
||||
erroredFiles.push({ filePath: file.path, error: 'user canceled' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata = this.plugin.getMetadataFromFileCache(file);
|
||||
const lookupValue = metadata[fieldName];
|
||||
|
||||
if (!lookupValue || typeof lookupValue !== 'string') {
|
||||
erroredFiles.push({ filePath: file.path, error: `metadata field '${fieldName}' not found, empty, or not a string` });
|
||||
continue;
|
||||
} else if (lookupMethod === BulkImportLookupMethod.ID) {
|
||||
const error = await this.importById(file, lookupValue, selectedAPI, appendContent);
|
||||
if (error) {
|
||||
erroredFiles.push(error);
|
||||
}
|
||||
} else if (lookupMethod === BulkImportLookupMethod.TITLE) {
|
||||
const error = await this.importByTitle(file, lookupValue, selectedAPI, appendContent);
|
||||
if (error) {
|
||||
if (error.canceled) {
|
||||
canceled = true;
|
||||
}
|
||||
erroredFiles.push(error);
|
||||
}
|
||||
} else {
|
||||
erroredFiles.push({ filePath: file.path, error: `invalid lookup type` });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (erroredFiles.length > 0) {
|
||||
await this.createErroredFilesReport(erroredFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> {
|
||||
try {
|
||||
const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI);
|
||||
if (model) {
|
||||
await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined);
|
||||
return undefined;
|
||||
} else {
|
||||
return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` };
|
||||
}
|
||||
} catch (e) {
|
||||
return { filePath: file.path, error: `${e}` };
|
||||
}
|
||||
}
|
||||
|
||||
private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise<BulkImportError | undefined> {
|
||||
let results: MediaTypeModel[] = [];
|
||||
try {
|
||||
results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]);
|
||||
} catch (e) {
|
||||
return { filePath: file.path, error: `${e}` };
|
||||
}
|
||||
if (!results || results.length === 0) {
|
||||
return { filePath: file.path, error: `no search results` };
|
||||
}
|
||||
|
||||
const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({
|
||||
elements: results,
|
||||
skipButton: true,
|
||||
modalTitle: `Results for '${lookupValue}'`,
|
||||
});
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.ERROR) {
|
||||
selectModal.close();
|
||||
return { filePath: file.path, error: selectModalResult.error.message };
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.CLOSE) {
|
||||
selectModal.close();
|
||||
return { filePath: file.path, error: 'user canceled', canceled: true };
|
||||
}
|
||||
|
||||
if (selectModalResult.code === ModalResultCode.SKIP) {
|
||||
selectModal.close();
|
||||
return { filePath: file.path, error: 'user skipped' };
|
||||
}
|
||||
|
||||
if (selectModalResult.data.selected.length === 0) {
|
||||
selectModal.close();
|
||||
return { filePath: file.path, error: `no search results selected` };
|
||||
}
|
||||
|
||||
const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected);
|
||||
await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
|
||||
|
||||
selectModal.close();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async createErroredFilesReport(erroredFiles: BulkImportError[]): Promise<void> {
|
||||
const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`;
|
||||
const filePath = `${title}.md`;
|
||||
|
||||
const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error]));
|
||||
|
||||
const fileContent = `# ${title}\n\n${markdownTable(table)}`;
|
||||
await this.plugin.app.vault.create(filePath, fileContent);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
16
src/utils/IllegalFilenameCharactersList.ts
Normal file
16
src/utils/IllegalFilenameCharactersList.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Illegal characters in the form `[illegal_character, replacement][]`
|
||||
export const ILLEGAL_FILENAME_CHARACTERS = [
|
||||
['/', '-'],
|
||||
['\\', '-'],
|
||||
['<', ''],
|
||||
['>', ''],
|
||||
[':', ' - '],
|
||||
['"', "'"],
|
||||
['|', ' - '],
|
||||
['?', ''],
|
||||
['*', ''],
|
||||
['[', '('],
|
||||
[']', ')'],
|
||||
['^', ''],
|
||||
['#', ''],
|
||||
];
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
export enum MediaType {
|
||||
Movie = 'movie',
|
||||
Series = 'series',
|
||||
Season = 'season',
|
||||
ComicManga = 'comicManga',
|
||||
Game = 'game',
|
||||
MusicRelease = 'musicRelease',
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
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';
|
||||
import { SeriesModel } from '../models/SeriesModel';
|
||||
import { SeasonModel } from '../models/SeasonModel';
|
||||
import { WikiModel } from '../models/WikiModel';
|
||||
import type { MediaDbPluginSettings } from '../settings/Settings';
|
||||
import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList';
|
||||
import { MediaType } from './MediaType';
|
||||
import { replaceTags } from './Utils';
|
||||
|
||||
export const MEDIA_TYPES: MediaType[] = [
|
||||
MediaType.Movie,
|
||||
MediaType.Series,
|
||||
MediaType.Season,
|
||||
MediaType.ComicManga,
|
||||
MediaType.Game,
|
||||
MediaType.Wiki,
|
||||
|
|
@ -39,6 +42,7 @@ export class MediaTypeManager {
|
|||
this.mediaFileNameTemplateMap = new Map<MediaType, string>();
|
||||
this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate);
|
||||
this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate);
|
||||
this.mediaFileNameTemplateMap.set(MediaType.Season, settings.seasonFileNameTemplate);
|
||||
this.mediaFileNameTemplateMap.set(MediaType.ComicManga, settings.mangaFileNameTemplate);
|
||||
this.mediaFileNameTemplateMap.set(MediaType.Game, settings.gameFileNameTemplate);
|
||||
this.mediaFileNameTemplateMap.set(MediaType.Wiki, settings.wikiFileNameTemplate);
|
||||
|
|
@ -49,6 +53,7 @@ export class MediaTypeManager {
|
|||
this.mediaTemplateMap = new Map<MediaType, string>();
|
||||
this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate);
|
||||
this.mediaTemplateMap.set(MediaType.Series, settings.seriesTemplate);
|
||||
this.mediaTemplateMap.set(MediaType.Season, settings.seasonTemplate);
|
||||
this.mediaTemplateMap.set(MediaType.ComicManga, settings.mangaTemplate);
|
||||
this.mediaTemplateMap.set(MediaType.Game, settings.gameTemplate);
|
||||
this.mediaTemplateMap.set(MediaType.Wiki, settings.wikiTemplate);
|
||||
|
|
@ -61,6 +66,7 @@ export class MediaTypeManager {
|
|||
this.mediaFolderMap = new Map<MediaType, string>();
|
||||
this.mediaFolderMap.set(MediaType.Movie, settings.movieFolder);
|
||||
this.mediaFolderMap.set(MediaType.Series, settings.seriesFolder);
|
||||
this.mediaFolderMap.set(MediaType.Season, settings.seasonFolder);
|
||||
this.mediaFolderMap.set(MediaType.ComicManga, settings.mangaFolder);
|
||||
this.mediaFolderMap.set(MediaType.Game, settings.gameFolder);
|
||||
this.mediaFolderMap.set(MediaType.Wiki, settings.wikiFolder);
|
||||
|
|
@ -71,7 +77,14 @@ export class MediaTypeManager {
|
|||
|
||||
getFileName(mediaTypeModel: MediaTypeModel): string {
|
||||
// Ignore undefined tags since some search APIs do not return all properties in the model and produce clean file names even if errors occur
|
||||
return replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true);
|
||||
const fileName = replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true);
|
||||
return this.cleanFileName(fileName);
|
||||
}
|
||||
|
||||
cleanFileName(fileName: string): string {
|
||||
const cleanedFileName = ILLEGAL_FILENAME_CHARACTERS.reduce((str, char) => str.replaceAll(char[0], char[1]), fileName);
|
||||
// Remove all duplicate whitespace in the file name
|
||||
return cleanedFileName.replaceAll(/ +/g, ' ');
|
||||
}
|
||||
|
||||
async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise<string> {
|
||||
|
|
@ -104,9 +117,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 +126,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,11 +138,13 @@ 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) {
|
||||
return new SeriesModel(obj);
|
||||
} else if (mediaType === MediaType.Season) {
|
||||
return new SeasonModel(obj);
|
||||
} else if (mediaType === MediaType.ComicManga) {
|
||||
return new ComicMangaModel(obj);
|
||||
} else if (mediaType === MediaType.Game) {
|
||||
|
|
|
|||
|
|
@ -159,12 +159,13 @@ export interface IdSearchModalOptions {
|
|||
* - skipButton: whether to add a skip button to the modal
|
||||
*/
|
||||
export interface SelectModalOptions {
|
||||
modalTitle?: string;
|
||||
elements?: MediaTypeModel[];
|
||||
multiSelect?: boolean;
|
||||
modalTitle?: string;
|
||||
skipButton?: boolean;
|
||||
description?: string; // Add this
|
||||
submitButtonText?: string; // Add this too
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the preview modal.
|
||||
* - modalTitle: the title of the modal
|
||||
|
|
@ -189,7 +190,7 @@ export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions =
|
|||
|
||||
export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = {
|
||||
modalTitle: 'Media DB Id Search',
|
||||
preselectedAPI: '',
|
||||
preselectedAPI: undefined,
|
||||
prefilledSearchString: '',
|
||||
};
|
||||
|
||||
|
|
@ -205,6 +206,15 @@ export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = {
|
|||
elements: [],
|
||||
};
|
||||
|
||||
export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = {
|
||||
elements: [],
|
||||
multiSelect: true,
|
||||
modalTitle: '',
|
||||
skipButton: false,
|
||||
description: 'Select one or multiple search results.',
|
||||
submitButtonText: 'Ok',
|
||||
};
|
||||
|
||||
/**
|
||||
* A class providing multiple usefull functions for dealing with the plugins modals.
|
||||
*/
|
||||
|
|
@ -505,12 +515,12 @@ export class ModalHelper {
|
|||
console.warn(previewModalResult.error);
|
||||
new Notice(previewModalResult.error.toString());
|
||||
previewModal.close();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previewModalResult.code === ModalResultCode.CLOSE) {
|
||||
// modal is already being closed
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
16
src/utils/SeasonModalHelper.ts
Normal file
16
src/utils/SeasonModalHelper.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { App } from 'obsidian';
|
||||
import type { SeasonSelectModalElement } from '../modals/MediaDbSeasonSelectModal';
|
||||
import { MediaDbSeasonSelectModal } from '../modals/MediaDbSeasonSelectModal';
|
||||
|
||||
export async function openSeasonSelectModal(app: App, plugin: any, seasons: SeasonSelectModalElement[]): Promise<SeasonSelectModalElement[] | undefined> {
|
||||
return new Promise(resolve => {
|
||||
const modal = new MediaDbSeasonSelectModal(plugin, seasons, true);
|
||||
modal.setSubmitCallback(selected => {
|
||||
resolve(selected);
|
||||
});
|
||||
modal.setCloseCallback(() => {
|
||||
resolve(undefined);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { iso6392 } from 'iso-639-2';
|
||||
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 +44,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 +61,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 +72,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 +90,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 +203,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 +223,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,13 +234,72 @@ 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.
|
||||
|
||||
export async function imageUrlExists(url: string): Promise<boolean> {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const response = await requestUrl({
|
||||
url,
|
||||
method: 'HEAD',
|
||||
throw: false,
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch {
|
||||
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;
|
||||
}
|
||||
|
||||
export function getLanguageName(code: string): string | null {
|
||||
const language = iso6392.find(lang => lang.iso6392B === code || lang.iso6392T === code);
|
||||
|
||||
return language?.name ?? null;
|
||||
}
|
||||
|
|
|
|||
5
test/placeholder.test.ts
Normal file
5
test/placeholder.test.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { test, expect } from 'bun:test';
|
||||
|
||||
test('placeholder test', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue