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/README.md b/README.md index 7eadce2..f80649e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Obsidian Media DB Plugin -A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault. +A plugin that can query multiple APIs for movies, series, anime, manga, games, music and wiki articles, and import them into your vault. ### Features @@ -113,7 +113,7 @@ 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 | No | 60 per minute and 3 per second | Yes | +| [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 | | [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 | @@ -134,6 +134,10 @@ Now you select the result you want and the plugin will cast it's magic and creat - the ID you need is the ID of the anime on [My Anime List](https://myanimelist.net) - you can find this ID in the URL - e.g. for "Beyond the Boundary" the URL looks like this `https://myanimelist.net/anime/18153/Kyoukai_no_Kanata` so the ID is `18153` +- [Jikan Manga](https://jikan.moe/) + - the ID you need is the ID of the manga on [My Anime List](https://myanimelist.net) + - you can find this ID in the URL + - e.g. for "All You Need Is Kill" the URL looks like this `https://myanimelist.net/manga/62887/All_You_Need_Is_Kill` so the ID is `62887` - [OMDb](https://www.omdbapi.com/) - the ID you need is the ID of the movie or show on [IMDb](https://www.imdb.com) - you can find this ID in the URL diff --git a/src/api/apis/MALAPIManga.ts b/src/api/apis/MALAPIManga.ts new file mode 100644 index 0000000..45bc06d --- /dev/null +++ b/src/api/apis/MALAPIManga.ts @@ -0,0 +1,130 @@ +import { APIModel } from '../APIModel'; +import { MediaTypeModel } from '../../models/MediaTypeModel'; +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('manhwa', 'manhwa'); + this.typeMappings.set('doujinshi', 'doujin'); + this.typeMappings.set('one-shot', 'oneshot'); + this.typeMappings.set('manhua', 'manhua'); + this.typeMappings.set('light novel', 'light-novel'); + this.typeMappings.set('novel', 'novel'); + } + + 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()); + 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()); + 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; + } +} diff --git a/src/main.ts b/src/main.ts index a65015d..5dcf63d 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'; @@ -33,6 +34,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..c43f7be --- /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', 'light-novel']; + } + + 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 f7258e7..3cdf97c 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -18,6 +18,7 @@ export interface MediaDbPluginSettings { movieTemplate: string; seriesTemplate: string; + mangaTemplate: string; gameTemplate: string; wikiTemplate: string; musicReleaseTemplate: string; @@ -26,6 +27,7 @@ export interface MediaDbPluginSettings { movieFileNameTemplate: string; seriesFileNameTemplate: string; + mangaFileNameTemplate: string; gameFileNameTemplate: string; wikiFileNameTemplate: string; musicReleaseFileNameTemplate: string; @@ -34,6 +36,7 @@ export interface MediaDbPluginSettings { moviePropertyConversionRules: string; seriesPropertyConversionRules: string; + mangaPropertyConversionRules: string; gamePropertyConversionRules: string; wikiPropertyConversionRules: string; musicReleasePropertyConversionRules: string; @@ -42,6 +45,7 @@ export interface MediaDbPluginSettings { movieFolder: string; seriesFolder: string; + mangaFolder: string; gameFolder: string; wikiFolder: string; musicReleaseFolder: string; @@ -60,6 +64,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieTemplate: '', seriesTemplate: '', + mangaTemplate: '', gameTemplate: '', wikiTemplate: '', musicReleaseTemplate: '', @@ -68,6 +73,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 }})', @@ -76,6 +82,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { moviePropertyConversionRules: '', seriesPropertyConversionRules: '', + mangaPropertyConversionRules: '', gamePropertyConversionRules: '', wikiPropertyConversionRules: '', musicReleasePropertyConversionRules: '', @@ -84,6 +91,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', @@ -226,6 +234,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 placed.') @@ -319,6 +340,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.') @@ -411,6 +445,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 cae316a..dd39945 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 b898991..6d2c925 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -5,13 +5,14 @@ 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'; import { BookModel } from '../models/BookModel'; -export const MEDIA_TYPES: MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Game, MediaType.Wiki, MediaType.MusicRelease, MediaType.BoardGame, MediaType.Book]; +export const MEDIA_TYPES: MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Manga, MediaType.Game, MediaType.Wiki, MediaType.MusicRelease, MediaType.BoardGame, MediaType.Book]; export class MediaTypeManager { mediaFileNameTemplateMap: Map; @@ -24,6 +25,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); @@ -33,6 +35,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); @@ -44,6 +47,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); @@ -107,6 +111,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) {