From eb28e96769bb1b680a7d4e81751bfa0094f89362 Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 21 Jul 2023 20:45:03 -0500 Subject: [PATCH] Added MAL Manga as a second Jikan API --- .gitignore | 3 + src/api/apis/MALAPIManga.ts | 199 ++++++++++++++++++++++++++++++++++ src/main.ts | 2 + src/models/MangaModel.ts | 77 +++++++++++++ src/settings/Settings.ts | 46 ++++++++ src/utils/MediaType.ts | 1 + src/utils/MediaTypeManager.ts | 8 +- 7 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 src/api/apis/MALAPIManga.ts create mode 100644 src/models/MangaModel.ts diff --git a/.gitignore b/.gitignore index e09a007..79c0350 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ data.json # Exclude macOS Finder (System Explorer) View States .DS_Store + +src/**/*.js +__mocks__/*.js \ No newline at end of file diff --git a/src/api/apis/MALAPIManga.ts b/src/api/apis/MALAPIManga.ts new file mode 100644 index 0000000..d44d727 --- /dev/null +++ b/src/api/apis/MALAPIManga.ts @@ -0,0 +1,199 @@ +import { APIModel } from '../APIModel'; +import { MediaTypeModel } from '../../models/MediaTypeModel'; +// import { MovieModel } from '../../models/MovieModel'; +import MediaDbPlugin from '../../main'; +import { MangaModel } from '../../models/MangaModel'; +import { MediaType } from '../../utils/MediaType'; + +export class MALAPIManga extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + 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.typeMappings = new Map(); + this.typeMappings.set('manga', 'manga'); + this.typeMappings.set('light novel', 'lnovel'); + } + + async searchByTitle(title: string): Promise { + 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 fetchData = await fetch(searchUrl); + console.debug(fetchData); + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from an API.`); + } + const data = await fetchData.json(); + + console.debug(data); + + const ret: MediaTypeModel[] = []; + + for (const result of data.data) { + const type = this.typeMappings.get(result.type?.toLowerCase()); + if (type === undefined) { + ret.push( + new MangaModel({ + subType: type, + title: result.title, + synopsis: result.synopsis, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map((x: any) => x.title) ?? [], + year: result.year ?? result.published?.prop?.from?.year ?? '', + dataSource: this.apiName, + url: result.url, + id: result.mal_id, + + genres: result.genres?.map((x: any) => x.name) ?? [], + authors: result.authors?.map((x: any) => x.name) ?? [], + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score ?? 0, + image: result.images?.jpg?.image_url ?? '', + + released: true, + publishedFrom: new Date(result.published?.from).toLocaleDateString() ?? 'unknown', + publishedTo: new Date(result.published?.to).toLocaleDateString() ?? 'unknown', + status: result.status, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + } as MangaModel) + ); + } + if (type === 'manga' || type === "lnovel") { + ret.push( + new MangaModel({ + subType: type, + title: result.title, + synopsis: result.synopsis, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map((x: any) => x.title) ?? [], + year: result.year ?? result.published?.prop?.from?.year ?? '', + dataSource: this.apiName, + url: result.url, + id: result.mal_id, + + genres: result.genres?.map((x: any) => x.name) ?? [], + authors: result.authors?.map((x: any) => x.name) ?? [], + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score ?? 0, + image: result.images?.jpg?.image_url ?? '', + + released: true, + publishedFrom: new Date(result.published?.from).toLocaleDateString() ?? 'unknown', + publishedTo: new Date(result.published?.to).toLocaleDateString() ?? 'unknown', + status: result.status, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + } as MangaModel) + ); + } + } + + return ret; + } + + async getById(id: string): Promise { + 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); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from an API.`); + } + + const data = await fetchData.json(); + console.debug(data); + const result = data.data; + + const type = this.typeMappings.get(result.type?.toLowerCase()); + if (type === undefined) { + const model = new MangaModel({ + subType: type, + title: result.title, + synopsis: result.synopsis, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map((x: any) => x.title) ?? [], + year: result.year ?? result.published?.prop?.from?.year ?? '', + dataSource: this.apiName, + url: result.url, + id: result.mal_id, + + genres: result.genres?.map((x: any) => x.name) ?? [], + authors: result.authors?.map((x: any) => x.name) ?? [], + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score ?? 0, + image: result.images?.jpg?.image_url ?? '', + + released: true, + publishedFrom: new Date(result.published?.from).toLocaleDateString() ?? 'unknown', + publishedTo: new Date(result.published?.to).toLocaleDateString() ?? 'unknown', + status: result.status, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + } as MangaModel); + + return model; + } + + if (type === 'manga' || type === 'lnovel') { + const model = new MangaModel({ + subType: type, + title: result.title, + synopsis: result.synopsis, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map((x: any) => x.title) ?? [], + year: result.year ?? result.published?.prop?.from?.year ?? '', + dataSource: this.apiName, + url: result.url, + id: result.mal_id, + + genres: result.genres?.map((x: any) => x.name) ?? [], + authors: result.authors?.map((x: any) => x.name) ?? [], + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score ?? 0, + image: result.images?.jpg?.image_url ?? '', + + released: true, + publishedFrom: new Date(result.published?.from).toLocaleDateString() ?? 'unknown', + publishedTo: new Date(result.published?.to).toLocaleDateString() ?? 'unknown', + status: result.status, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + } as MangaModel); + + return model; + } + + return; + } +} diff --git a/src/main.ts b/src/main.ts index 76abff4..e863d3e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { MediaTypeModel } from './models/MediaTypeModel'; import { CreateNoteOptions, dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, unCamelCase } from './utils/Utils'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { MALAPI } from './api/apis/MALAPI'; +import { MALAPIManga } from './api/apis/MALAPIManga'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; @@ -30,6 +31,7 @@ export default class MediaDbPlugin extends Plugin { // register APIs this.apiManager.registerAPI(new OMDbAPI(this)); this.apiManager.registerAPI(new MALAPI(this)); + this.apiManager.registerAPI(new MALAPIManga(this)); this.apiManager.registerAPI(new WikipediaAPI(this)); this.apiManager.registerAPI(new MusicBrainzAPI(this)); this.apiManager.registerAPI(new SteamAPI(this)); diff --git a/src/models/MangaModel.ts b/src/models/MangaModel.ts new file mode 100644 index 0000000..63c99ae --- /dev/null +++ b/src/models/MangaModel.ts @@ -0,0 +1,77 @@ +import { MediaTypeModel } from './MediaTypeModel'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaType } from '../utils/MediaType'; + +export class MangaModel extends MediaTypeModel { + type: string; + subType: string; + title: string; + synopsis: string; + englishTitle: string; + alternateTitles: string[]; + year: string; + dataSource: string; + url: string; + id: string; + + genres: string[]; + authors: string[]; + chapters: number; + volumes: number; + onlineRating: number; + image: string; + + released: boolean; + status: string; + publishedFrom: string; + publishedTo: string; + + userData: { + watched: boolean; + lastWatched: string; + personalRating: number; + }; + + constructor(obj: any = {}) { + super(); + + this.genres = undefined; + this.authors = undefined; + this.alternateTitles = undefined; + this.chapters = undefined; + this.volumes = undefined; + this.onlineRating = undefined; + this.image = undefined; + + this.released = undefined; + this.status = undefined; + this.publishedFrom = undefined; + this.publishedTo = undefined; + + this.userData = { + watched: undefined, + lastWatched: undefined, + personalRating: undefined, + }; + + migrateObject(this, obj, this); + + if (!obj.hasOwnProperty('userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'manga', 'lnovel']; + } + + getMediaType(): MediaType { + return MediaType.Manga; + } + + getSummary(): string { + return this.title + ' (' + this.year + ')'; + } +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 4c37060..c420084 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -16,6 +16,7 @@ export interface MediaDbPluginSettings { movieTemplate: string; seriesTemplate: string; + mangaTemplate: string; gameTemplate: string; wikiTemplate: string; musicReleaseTemplate: string; @@ -23,6 +24,7 @@ export interface MediaDbPluginSettings { movieFileNameTemplate: string; seriesFileNameTemplate: string; + mangaFileNameTemplate: string; gameFileNameTemplate: string; wikiFileNameTemplate: string; musicReleaseFileNameTemplate: string; @@ -30,6 +32,7 @@ export interface MediaDbPluginSettings { moviePropertyConversionRules: string; seriesPropertyConversionRules: string; + mangaPropertyConversionRules: string; gamePropertyConversionRules: string; wikiPropertyConversionRules: string; musicReleasePropertyConversionRules: string; @@ -37,6 +40,7 @@ export interface MediaDbPluginSettings { movieFolder: string; seriesFolder: string; + mangaFolder: string; gameFolder: string; wikiFolder: string; musicReleaseFolder: string; @@ -53,6 +57,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieTemplate: '', seriesTemplate: '', + mangaTemplate: '', gameTemplate: '', wikiTemplate: '', musicReleaseTemplate: '', @@ -60,6 +65,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieFileNameTemplate: '{{ title }} ({{ year }})', seriesFileNameTemplate: '{{ title }} ({{ year }})', + mangaFileNameTemplate: '{{ title }} ({{ year }})', gameFileNameTemplate: '{{ title }} ({{ year }})', wikiFileNameTemplate: '{{ title }}', musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})', @@ -67,6 +73,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { moviePropertyConversionRules: '', seriesPropertyConversionRules: '', + mangaPropertyConversionRules: '', gamePropertyConversionRules: '', wikiPropertyConversionRules: '', musicReleasePropertyConversionRules: '', @@ -74,6 +81,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieFolder: 'Media DB/movies', seriesFolder: 'Media DB/series', + mangaFolder: 'Media DB/manga', gameFolder: 'Media DB/games', wikiFolder: 'Media DB/wiki', musicReleaseFolder: 'Media DB/music', @@ -193,6 +201,19 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName('Manga Folder') + .setDesc('Where newly imported manga should be placed.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.mangaFolder) + .setValue(this.plugin.settings.mangaFolder) + .onChange(data => { + this.plugin.settings.mangaFolder = data; + this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('Game Folder') .setDesc('Where newly imported games should be places.') @@ -274,6 +295,19 @@ 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.') + .addSearch(cb => { + new FileSuggest(this.app, cb.inputEl); + cb.setPlaceholder('Example: mangaTemplate.md') + .setValue(this.plugin.settings.mangaTemplate) + .onChange(data => { + this.plugin.settings.mangaTemplate = data; + this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('Game template') .setDesc('Template file to be used when creating a new note for a game.') @@ -353,6 +387,18 @@ 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.') + .addText(cb => { + cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.mangaFileNameTemplate}`) + .setValue(this.plugin.settings.mangaFileNameTemplate) + .onChange(data => { + this.plugin.settings.mangaFileNameTemplate = data; + this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('Game file name template') .setDesc('Template for the file name used when creating a new note for a game.') diff --git a/src/utils/MediaType.ts b/src/utils/MediaType.ts index c36680c..4269f06 100644 --- a/src/utils/MediaType.ts +++ b/src/utils/MediaType.ts @@ -1,6 +1,7 @@ export enum MediaType { Movie = 'movie', Series = 'series', + Manga = 'manga', Game = 'game', MusicRelease = 'musicRelease', Wiki = 'wiki', diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 58a07ef..f1bf872 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -5,12 +5,13 @@ import { replaceTags } from './Utils'; import { App, TAbstractFile, TFile, TFolder } from 'obsidian'; import { MovieModel } from '../models/MovieModel'; import { SeriesModel } from '../models/SeriesModel'; +import { MangaModel } from '../models/MangaModel'; import { GameModel } from '../models/GameModel'; import { WikiModel } from '../models/WikiModel'; import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { BoardGameModel } from '../models/BoardGameModel'; -export const MEDIA_TYPES: MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Game, MediaType.Wiki, MediaType.MusicRelease, MediaType.BoardGame]; +export const MEDIA_TYPES: MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Manga, MediaType.Game, MediaType.Wiki, MediaType.MusicRelease, MediaType.BoardGame]; export class MediaTypeManager { mediaFileNameTemplateMap: Map; @@ -23,6 +24,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.Game, settings.gameFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Wiki, settings.wikiFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate); @@ -31,6 +33,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.Game, settings.gameTemplate); this.mediaTemplateMap.set(MediaType.Wiki, settings.wikiTemplate); this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate); @@ -41,6 +44,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.Game, settings.gameFolder); this.mediaFolderMap.set(MediaType.Wiki, settings.wikiFolder); this.mediaFolderMap.set(MediaType.MusicRelease, settings.musicReleaseFolder); @@ -103,6 +107,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.Game) { return new GameModel(obj); } else if (mediaType === MediaType.Wiki) {