diff --git a/README.md b/README.md index e0b04d8..5ad2f49 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,12 @@ Now you select the result you want and the plugin will cast it's magic and creat - music releases - wiki articles - books +- manga +- comics ### Currently supported APIs: + | Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes | @@ -121,6 +124,7 @@ Now you select the result you want and the plugin will cast it's magic and creat | [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 #### Notes @@ -160,6 +164,10 @@ Now you select the result you want and the plugin will cast it's magic and creat - [Giant Bomb](https://www.giantbomb.com) - you can find this ID in the URL - e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887` +- [Comic Vine](https://www.comicvine.gamespot.com) + - you can find this ID in the URL + - e.g. for "Boule & Bill" the URL looks like this `https://comicvine.gamespot.com/boule-bill/4050-70187/` so the ID is `4050-70187` + - Please note that only volumes can be added, not separate issues. ### Problems, unexpected behavior or improvement suggestions? diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts new file mode 100644 index 0000000..86c9bf8 --- /dev/null +++ b/src/api/apis/ComicVineAPI.ts @@ -0,0 +1,98 @@ +import { requestUrl } from 'obsidian'; +import { ComicMangaModel } from 'src/models/ComicMangaModel'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +export class ComicVineAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'ComicVineAPI'; + this.apiDescription = 'A free API for comic books.'; + this.apiUrl = 'https://comicvine.gamespot.com/api'; + this.types = [MediaType.ComicManga]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `${this.apiUrl}/search/?api_key=${this.plugin.settings.ComicVineKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + // console.debug(fetchData); + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const ret: MediaTypeModel[] = []; + for (const result of data.results) { + ret.push( + new ComicMangaModel({ + title: result.name, + englishTitle: result.name, + year: result.start_year, + dataSource: this.apiName, + id: `4050-${result.id}`, + publishers: result.publisher?.name ?? [], + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.ComicVineKey}&format=json`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + console.debug(fetchData); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const result = data.results; + + return new ComicMangaModel({ + type: MediaType.ComicManga, + title: result.name, + englishTitle: result.name, + alternateTitles: result.aliases, + plot: result.deck, + year: result.start_year ?? '', + dataSource: this.apiName, + url: result.site_detail_url, + id: `4050-${result.id}`, + + authors: result.people?.map((x: any) => x.name) ?? [], + chapters: result.count_of_issues, + image: result.image?.original_url ?? '', + + released: true, + publishers: result.publisher?.name ?? [], + publishedFrom: result.start_year ?? 'unknown', + publishedTo: 'unknown', + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }); + } +} diff --git a/src/api/apis/MALAPIManga.ts b/src/api/apis/MALAPIManga.ts index 770c273..bd99476 100644 --- a/src/api/apis/MALAPIManga.ts +++ b/src/api/apis/MALAPIManga.ts @@ -1,5 +1,5 @@ import type MediaDbPlugin from '../../main'; -import { MangaModel } from '../../models/MangaModel'; +import { ComicMangaModel } from '../../models/ComicMangaModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -15,7 +15,7 @@ export class MALAPIManga extends APIModel { this.apiName = 'MALAPI Manga'; this.apiDescription = 'A free API for Manga. Some results may take a long time to load.'; this.apiUrl = 'https://jikan.moe/'; - this.types = [MediaType.Manga]; + this.types = [MediaType.ComicManga]; this.typeMappings = new Map(); this.typeMappings.set('manga', 'manga'); this.typeMappings.set('manhwa', 'manhwa'); @@ -45,7 +45,7 @@ export class MALAPIManga extends APIModel { for (const result of data.data) { const type = this.typeMappings.get(result.type?.toLowerCase()); ret.push( - new MangaModel({ + new ComicMangaModel({ subType: type, title: result.title, plot: result.synopsis, @@ -69,8 +69,8 @@ export class MALAPIManga extends APIModel { status: result.status, userData: { - watched: false, - lastWatched: '', + read: false, + lastRead: '', personalRating: 0, }, }), @@ -95,7 +95,7 @@ export class MALAPIManga extends APIModel { const result = data.data; const type = this.typeMappings.get(result.type?.toLowerCase()); - return new MangaModel({ + return new ComicMangaModel({ subType: type, title: result.title, englishTitle: result.title_english ?? result.title, @@ -114,13 +114,14 @@ export class MALAPIManga extends APIModel { 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', status: result.status, userData: { - watched: false, - lastWatched: '', + read: false, + lastRead: '', personalRating: 0, }, }); diff --git a/src/main.ts b/src/main.ts index 625a1ed..bdb02bb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { SteamAPI } from './api/apis/SteamAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; +import { ComicVineAPI } from './api/apis/ComicVineAPI'; import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal'; import type { MediaTypeModel } from './models/MediaTypeModel'; import { PropertyMapper } from './settings/PropertyMapper'; @@ -53,6 +54,7 @@ export default class MediaDbPlugin extends Plugin { this.apiManager.registerAPI(new SteamAPI(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 LocGovAPI(this)); // TODO: parse data @@ -549,9 +551,10 @@ export default class MediaDbPlugin extends Plugin { } const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; + console.debug(`MDB | validOldMetadata`, validOldMetadata); const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); - // console.debug(oldMediaTypeModel); + console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); if (!newMediaTypeModel) { @@ -559,7 +562,7 @@ export default class MediaDbPlugin extends Plugin { } newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); - // console.debug(newMediaTypeModel); + console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); if (onlyMetadata) { await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); diff --git a/src/models/MangaModel.ts b/src/models/ComicMangaModel.ts similarity index 76% rename from src/models/MangaModel.ts rename to src/models/ComicMangaModel.ts index b89e963..5677d72 100644 --- a/src/models/MangaModel.ts +++ b/src/models/ComicMangaModel.ts @@ -3,9 +3,9 @@ import type { ModelToData } from '../utils/Utils'; import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaTypeModel } from './MediaTypeModel'; -export type MangaData = ModelToData; +export type ComicMangaData = ModelToData; -export class MangaModel extends MediaTypeModel { +export class ComicMangaModel extends MediaTypeModel { plot: string; alternateTitles: string[]; genres: string[]; @@ -17,16 +17,17 @@ export class MangaModel extends MediaTypeModel { released: boolean; status: string; + publishers: string[]; publishedFrom: string; publishedTo: string; userData: { - watched: boolean; - lastWatched: string; + read: boolean; + lastRead: string; personalRating: number; }; - constructor(obj: MangaData) { + constructor(obj: ComicMangaData) { super(); this.plot = ''; @@ -40,12 +41,13 @@ export class MangaModel extends MediaTypeModel { this.released = false; this.status = ''; + this.publishers = []; this.publishedFrom = ''; this.publishedTo = ''; this.userData = { - watched: false, - lastWatched: '', + read: false, + lastRead: '', personalRating: 0, }; @@ -59,11 +61,11 @@ export class MangaModel extends MediaTypeModel { } getTags(): string[] { - return [mediaDbTag, 'manga', 'light-novel']; + return [mediaDbTag, 'manga', 'light-novel', 'comicbook']; } getMediaType(): MediaType { - return MediaType.Manga; + return MediaType.ComicManga; } getSummary(): string { diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index b5644dc..e3be2c5 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -62,6 +62,10 @@ export class PropertyMapper { return obj; } + if (obj.type === 'manga') { + obj.type = 'comicManga'; + console.debug(`MDB | updated metadata type`, obj.type); + } if (MEDIA_TYPES.contains(obj.type as any)) { return obj; } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index f30b153..f880a35 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -14,6 +14,7 @@ export interface MediaDbPluginSettings { OMDbKey: string; MobyGamesKey: string; GiantBombKey: string; + ComicVineKey: string; sfwFilter: boolean; templates: boolean; customDateFormat: string; @@ -81,6 +82,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { OMDbKey: '', MobyGamesKey: '', GiantBombKey: '', + ComicVineKey: '', sfwFilter: true, templates: true, customDateFormat: 'L', @@ -133,7 +135,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieFolder: 'Media DB/movies', seriesFolder: 'Media DB/series', - mangaFolder: 'Media DB/manga', + mangaFolder: 'Media DB/comics', gameFolder: 'Media DB/games', wikiFolder: 'Media DB/wiki', musicReleaseFolder: 'Media DB/music', @@ -217,6 +219,17 @@ export class MediaDbSettingTab extends PluginSettingTab { void this.plugin.saveSettings(); }); }); + new Setting(containerEl) + .setName('Comic Vine Key') + .setDesc('API key for "www.comicvine.gamespot.com".') + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.ComicVineKey) + .onChange(data => { + this.plugin.settings.ComicVineKey = data; + void this.plugin.saveSettings(); + }); + }); new Setting(containerEl) .setName('SFW filter') @@ -364,7 +377,15 @@ export class MediaDbSettingTab extends PluginSettingTab { // 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('New file location').setHeading(); // region new file location new Setting(containerEl) @@ -394,8 +415,8 @@ export class MediaDbSettingTab extends PluginSettingTab { }); new Setting(containerEl) - .setName('Manga folder') - .setDesc('Where newly imported manga should be placed.') + .setName('Comic and manga folder') + .setDesc('Where newly imported comics and manga should be placed.') .addSearch(cb => { new FolderSuggest(this.app, cb.inputEl); cb.setPlaceholder(DEFAULT_SETTINGS.mangaFolder) @@ -469,6 +490,7 @@ export class MediaDbSettingTab extends PluginSettingTab { void this.plugin.saveSettings(); }); }); + // endregion new Setting(containerEl).setName('Template settings').setHeading(); @@ -500,8 +522,8 @@ export class MediaDbSettingTab extends PluginSettingTab { }); new Setting(containerEl) - .setName('Manga template') - .setDesc('Template file to be used when creating a new note for a manga.') + .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); cb.setPlaceholder('Example: mangaTemplate.md') @@ -576,6 +598,7 @@ export class MediaDbSettingTab extends PluginSettingTab { void this.plugin.saveSettings(); }); }); + // endregion new Setting(containerEl).setName('File name settings').setHeading(); @@ -605,8 +628,8 @@ export class MediaDbSettingTab extends PluginSettingTab { }); new Setting(containerEl) - .setName('Manga file name template') - .setDesc('Template for the file name used when creating a new note for a manga.') + .setName('Manga and comic file name template') + .setDesc('Template for the file name used when creating a new note for a manga or comic.') .addText(cb => { cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.mangaFileNameTemplate}`) .setValue(this.plugin.settings.mangaFileNameTemplate) @@ -675,6 +698,7 @@ export class MediaDbSettingTab extends PluginSettingTab { void this.plugin.saveSettings(); }); }); + // endregion // region Property Mappings diff --git a/src/utils/MediaType.ts b/src/utils/MediaType.ts index dd39945..02d1c2c 100644 --- a/src/utils/MediaType.ts +++ b/src/utils/MediaType.ts @@ -1,7 +1,7 @@ export enum MediaType { Movie = 'movie', Series = 'series', - Manga = 'manga', + ComicManga = 'comicManga', Game = 'game', MusicRelease = 'musicRelease', Wiki = 'wiki', diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 531da63..ec4a5ae 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -3,7 +3,7 @@ import { TFolder } from 'obsidian'; import { BoardGameModel } from '../models/BoardGameModel'; import { BookModel } from '../models/BookModel'; import { GameModel } from '../models/GameModel'; -import { MangaModel } from '../models/MangaModel'; +import { ComicMangaModel } from '../models/ComicMangaModel'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MovieModel } from '../models/MovieModel'; import { MusicReleaseModel } from '../models/MusicReleaseModel'; @@ -16,7 +16,7 @@ import { replaceTags } from './Utils'; export const MEDIA_TYPES: MediaType[] = [ MediaType.Movie, MediaType.Series, - MediaType.Manga, + MediaType.ComicManga, MediaType.Game, MediaType.Wiki, MediaType.MusicRelease, @@ -39,7 +39,7 @@ export class MediaTypeManager { this.mediaFileNameTemplateMap = new Map(); this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate); - this.mediaFileNameTemplateMap.set(MediaType.Manga, settings.mangaFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.ComicManga, settings.mangaFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Game, settings.gameFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Wiki, settings.wikiFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate); @@ -49,7 +49,7 @@ export class MediaTypeManager { this.mediaTemplateMap = new Map(); this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate); this.mediaTemplateMap.set(MediaType.Series, settings.seriesTemplate); - this.mediaTemplateMap.set(MediaType.Manga, settings.mangaTemplate); + this.mediaTemplateMap.set(MediaType.ComicManga, settings.mangaTemplate); this.mediaTemplateMap.set(MediaType.Game, settings.gameTemplate); this.mediaTemplateMap.set(MediaType.Wiki, settings.wikiTemplate); this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate); @@ -61,7 +61,7 @@ export class MediaTypeManager { this.mediaFolderMap = new Map(); this.mediaFolderMap.set(MediaType.Movie, settings.movieFolder); this.mediaFolderMap.set(MediaType.Series, settings.seriesFolder); - this.mediaFolderMap.set(MediaType.Manga, settings.mangaFolder); + this.mediaFolderMap.set(MediaType.ComicManga, settings.mangaFolder); this.mediaFolderMap.set(MediaType.Game, settings.gameFolder); this.mediaFolderMap.set(MediaType.Wiki, settings.wikiFolder); this.mediaFolderMap.set(MediaType.MusicRelease, settings.musicReleaseFolder); @@ -132,8 +132,8 @@ export class MediaTypeManager { return new MovieModel(obj); } else if (mediaType === MediaType.Series) { return new SeriesModel(obj); - } else if (mediaType === MediaType.Manga) { - return new MangaModel(obj); + } else if (mediaType === MediaType.ComicManga) { + return new ComicMangaModel(obj); } else if (mediaType === MediaType.Game) { return new GameModel(obj); } else if (mediaType === MediaType.Wiki) {