Merge pull request #176 from ltctceplrm/comicvine

Added support for comics through Comic Vine API
This commit is contained in:
Moritz Jung 2025-02-19 15:44:18 +01:00 committed by GitHub
commit d54822444a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 175 additions and 35 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@ import type { ModelToData } from '../utils/Utils';
import { mediaDbTag, migrateObject } from '../utils/Utils';
import { MediaTypeModel } from './MediaTypeModel';
export type MangaData = ModelToData<MangaModel>;
export type ComicMangaData = ModelToData<ComicMangaModel>;
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 {

View file

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

View file

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

View file

@ -1,7 +1,7 @@
export enum MediaType {
Movie = 'movie',
Series = 'series',
Manga = 'manga',
ComicManga = 'comicManga',
Game = 'game',
MusicRelease = 'musicRelease',
Wiki = 'wiki',

View file

@ -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<MediaType, string>();
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<MediaType, string>();
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<MediaType, string>();
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) {