fix merge

This commit is contained in:
Moritz Jung 2026-04-06 17:14:06 +02:00
commit ece0ddf00c
9 changed files with 347 additions and 3 deletions

View file

@ -120,7 +120,7 @@ Now you select the result you want, and the plugin will cast its magic, creating
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| [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 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 |
| [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 Read Access Token` (**not** `API Key`) [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 |
@ -152,6 +152,11 @@ Now you select the result you want, and the plugin will cast its magic, creating
- 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
- e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528`
- [TMDB](https://www.themoviedb.org/)
- the ID you need is the numeric value in the URL directly following `/movie/` or `/tv/`
- e.g. for "Stargate" the URL looks like this `https://www.themoviedb.org/movie/2164-stargate` so the ID is `2164`
- When searching by ID you need to select `TMDBSeriesAPI`, `TMDBSeasonAPI`, or `TMDBMovieAPI` for series, seasons, and movies respectively.
- Season ID searches use the format `<series_id>/season/<season_number>` - season 1 of The Expanse expects `63639/season/1`
- [MusicBrainz](https://musicbrainz.org/)
- 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:

159
src/api/apis/IGDBAPI.ts Normal file
View file

@ -0,0 +1,159 @@
import { requestUrl } from 'obsidian';
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';
interface IGDBCover {
url: string;
}
interface IGDBGenre {
name: string;
}
interface IGDBCompany {
name: string;
}
interface IGDBInvolvedCompany {
company: IGDBCompany;
developer: boolean;
publisher: boolean;
}
interface IGDBGame {
id: number;
name: string;
cover?: IGDBCover;
first_release_date?: number;
summary?: string;
total_rating?: number;
url?: string;
genres?: IGDBGenre[];
involved_companies?: IGDBInvolvedCompany[];
}
interface TwitchAuthResponse {
access_token: string;
expires_in: number;
}
export class IGDBAPI extends APIModel {
plugin: MediaDbPlugin;
apiDateFormat: string = 'YYYY-MM-DD';
private accessToken: string = '';
private tokenExpiry: number = 0;
constructor(plugin: MediaDbPlugin) {
super();
this.plugin = plugin;
this.apiName = 'IGDBAPI';
this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).';
this.apiUrl = 'https://api.igdb.com/v4';
this.types = [MediaType.Game];
}
private async getAuthToken(): Promise<string> {
const currentTime = Date.now();
if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken;
const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId);
const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret);
if (!clientId || !clientSecret) {
throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`);
}
console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`);
const response = await requestUrl({
url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`,
method: 'POST',
});
if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`);
const data = response.json as TwitchAuthResponse;
this.accessToken = data.access_token;
this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000;
return this.accessToken;
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId);
if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`);
const token = await this.getAuthToken();
const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`;
const response = await requestUrl({
url: `${this.apiUrl}/games`,
method: 'POST',
headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' },
body: queryBody,
});
if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`);
const data = response.json as IGDBGame[];
return data.map(result => {
const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '';
const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : '';
return new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: year,
dataSource: this.apiName,
id: result.id.toString(),
image: image,
});
});
}
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId);
if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`);
const token = await this.getAuthToken();
const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`;
const response = await requestUrl({
url: `${this.apiUrl}/games`,
method: 'POST',
headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' },
body: queryBody,
});
if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`);
const data = response.json as IGDBGame[];
if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`);
const result = data[0];
const developers: string[] = [];
const publishers: string[] = [];
result.involved_companies?.forEach(c => {
if (c.developer) developers.push(c.company.name);
if (c.publisher) publishers.push(c.company.name);
});
const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : '';
const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : '';
return new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '',
dataSource: this.apiName,
url: result.url,
id: result.id.toString(),
developers: developers,
publishers: publishers,
genres: result.genres?.map(g => g.name) ?? [],
onlineRating: result.total_rating,
image: image,
released: true,
releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '',
userData: { played: false, personalRating: 0 },
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? [];
}
}

View file

@ -132,6 +132,7 @@ export class MALAPI extends APIModel {
subType: undefined,
title: result.title,
englishTitle: result.title_english ?? result.title,
japaneseTitle: result.title_japanese,
year: year,
dataSource: this.apiName,
url: result.url,
@ -162,6 +163,7 @@ export class MALAPI extends APIModel {
subType: type,
title: result.title,
englishTitle: result.title_english ?? result.title,
japaneseTitle: result.title_japanese,
year: year,
dataSource: this.apiName,
url: result.url,
@ -190,6 +192,7 @@ export class MALAPI extends APIModel {
subType: type,
title: result.title,
englishTitle: result.title_english ?? result.title,
japaneseTitle: result.title_japanese,
year: year,
dataSource: this.apiName,
url: result.url,

105
src/api/apis/RAWGAPI.ts Normal file
View file

@ -0,0 +1,105 @@
import { requestUrl } from 'obsidian';
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';
interface RAWGGame {
id: number;
name: string;
released?: string;
background_image?: string;
name_original?: string;
website?: string;
slug?: string;
metacritic?: number;
developers?: { name: string }[];
publishers?: { name: string }[];
genres?: { name: string }[];
}
interface RAWGSearchResponse {
results: RAWGGame[];
}
export class RAWGAPI extends APIModel {
plugin: MediaDbPlugin;
apiDateFormat: string = 'YYYY-MM-DD';
constructor(plugin: MediaDbPlugin) {
super();
this.plugin = plugin;
this.apiName = 'RAWGAPI';
this.apiDescription = 'A large open video game database.';
this.apiUrl = 'https://api.rawg.io/api';
this.types = [MediaType.Game];
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId);
if (!key) {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const response = await requestUrl({
url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`,
method: 'GET',
});
if (response.status !== 200) {
throw Error(`MDB | Error ${response.status} from ${this.apiName}.`);
}
const data = response.json as RAWGSearchResponse;
return data.results.map(
result =>
new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: result.released ? new Date(result.released).getFullYear().toString() : '',
dataSource: this.apiName,
id: result.id.toString(),
image: result.background_image,
}),
);
}
async getById(id: string): Promise<MediaTypeModel> {
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId);
if (!key) {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}
const response = await requestUrl({
url: `${this.apiUrl}/games/${id}?key=${key}`,
method: 'GET',
});
if (response.status !== 200) {
throw Error(`MDB | Error ${response.status} from ${this.apiName}.`);
}
const result = response.json as RAWGGame;
return new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name_original ?? result.name,
year: result.released ? new Date(result.released).getFullYear().toString() : '',
dataSource: this.apiName,
url: result.website ?? `https://rawg.io/games/${result.slug}`,
id: result.id.toString(),
developers: result.developers?.map(d => d.name) ?? [],
publishers: result.publishers?.map(p => p.name) ?? [],
genres: result.genres?.map(g => g.name) ?? [],
onlineRating: result.metacritic,
image: result.background_image,
released: result.released != null,
releaseDate: result.released,
userData: { played: false, personalRating: 0 },
});
}
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? [];
}
}

View file

@ -5,12 +5,14 @@ import { APIManager } from './api/APIManager';
import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI';
import { ComicVineAPI } from './api/apis/ComicVineAPI';
import { GiantBombAPI } from './api/apis/GiantBombAPI';
import { IGDBAPI } from './api/apis/IGDBAPI';
import { MALAPI } from './api/apis/MALAPI';
import { MALAPIManga } from './api/apis/MALAPIManga';
import { MobyGamesAPI } from './api/apis/MobyGamesAPI';
import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI';
import { OMDbAPI } from './api/apis/OMDbAPI';
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
import { RAWGAPI } from './api/apis/RAWGAPI';
import { SteamAPI } from './api/apis/SteamAPI';
import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI';
import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI';
@ -72,6 +74,8 @@ export default class MediaDbPlugin extends Plugin {
this.apiManager.registerAPI(new ComicVineAPI(this));
this.apiManager.registerAPI(new MobyGamesAPI(this));
this.apiManager.registerAPI(new GiantBombAPI(this));
this.apiManager.registerAPI(new IGDBAPI(this));
this.apiManager.registerAPI(new RAWGAPI(this));
this.apiManager.registerAPI(new VNDBAPI(this));
this.mediaTypeManager = new MediaTypeManager();

View file

@ -61,7 +61,13 @@ export class ComicMangaModel extends MediaTypeModel {
}
getTags(): string[] {
return [mediaDbTag, 'manga', 'light-novel', 'comicbook'];
const tags = [mediaDbTag];
if (this.subType) {
tags.push(this.subType);
} else {
tags.push('comicManga');
}
return tags;
}
getMediaType(): MediaType {

View file

@ -6,6 +6,7 @@ import { MediaTypeModel } from './MediaTypeModel';
export type MovieData = ModelToData<MovieModel>;
export class MovieModel extends MediaTypeModel {
japaneseTitle: string;
plot: string;
genres: string[];
director: string[];
@ -32,6 +33,7 @@ export class MovieModel extends MediaTypeModel {
constructor(obj: MovieData) {
super();
this.japaneseTitle = '';
this.plot = '';
this.genres = [];
this.director = [];

View file

@ -6,6 +6,7 @@ import { MediaTypeModel } from './MediaTypeModel';
export type SeriesData = ModelToData<SeriesModel>;
export class SeriesModel extends MediaTypeModel {
japaneseTitle: string;
plot: string;
genres: string[];
writer: string[];
@ -33,6 +34,7 @@ export class SeriesModel extends MediaTypeModel {
constructor(obj: SeriesData) {
super();
this.japaneseTitle = '';
this.plot = '';
this.genres = [];
this.writer = [];

View file

@ -18,6 +18,9 @@ export interface MediaDbPluginSettings {
TMDBKeyId: string;
MobyGamesKeyId: string;
GiantBombKeyId: string;
IGDBClientId: string;
IGDBClientSecret: string;
RAWGAPIKeyId: string;
ComicVineKeyId: string;
BoardgameGeekKeyId: string;
@ -33,6 +36,8 @@ export interface MediaDbPluginSettings {
BoardgameGeekAPI_disabledMediaTypes: MediaType[];
ComicVineAPI_disabledMediaTypes: MediaType[];
GiantBombAPI_disabledMediaTypes: MediaType[];
IGDBAPI_disabledMediaTypes: MediaType[];
RAWGAPI_disabledMediaTypes: MediaType[];
MALAPI_disabledMediaTypes: MediaType[];
MALAPIManga_disabledMediaTypes: MediaType[];
MobyGamesAPI_disabledMediaTypes: MediaType[];
@ -272,6 +277,9 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
TMDBKeyId: '',
MobyGamesKeyId: '',
GiantBombKeyId: '',
IGDBClientId: '',
IGDBClientSecret: '',
RAWGAPIKeyId: '',
ComicVineKeyId: '',
BoardgameGeekKeyId: '',
@ -287,6 +295,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
BoardgameGeekAPI_disabledMediaTypes: [],
ComicVineAPI_disabledMediaTypes: [],
GiantBombAPI_disabledMediaTypes: [],
IGDBAPI_disabledMediaTypes: [],
RAWGAPI_disabledMediaTypes: [],
MALAPI_disabledMediaTypes: [],
MALAPIManga_disabledMediaTypes: [],
MobyGamesAPI_disabledMediaTypes: [],
@ -551,7 +561,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
setting =>
void setting
.setName('TMDB API key')
.setDesc('API key for "https://www.themoviedb.org".')
.setDesc('API Read Access Token for "https://www.themoviedb.org".')
.addComponent(el => {
const component = new SecretComponent(this.app, el);
@ -595,6 +605,54 @@ export class MediaDbSettingTab extends PluginSettingTab {
return component;
}),
);
apiKeyGroup.addSetting(
setting =>
void setting
.setName('IGDB Client ID')
.setDesc('Client ID for IGDB API (Required for Twitch OAuth).')
.addComponent(el => {
const component = new SecretComponent(this.app, el);
component.setValue(this.plugin.settings.IGDBClientId).onChange(data => {
this.plugin.settings.IGDBClientId = data;
void this.plugin.saveSettings();
});
return component;
}),
);
apiKeyGroup.addSetting(
setting =>
void setting
.setName('IGDB Client Secret')
.setDesc('Client Secret for IGDB API.')
.addComponent(el => {
const component = new SecretComponent(this.app, el);
component.setValue(this.plugin.settings.IGDBClientSecret).onChange(data => {
this.plugin.settings.IGDBClientSecret = data;
void this.plugin.saveSettings();
});
return component;
}),
);
apiKeyGroup.addSetting(
setting =>
void setting
.setName('RAWG API Key')
.setDesc('API key for "rawg.io".')
.addComponent(el => {
const component = new SecretComponent(this.app, el);
component.setValue(this.plugin.settings.RAWGAPIKeyId).onChange(data => {
this.plugin.settings.RAWGAPIKeyId = data;
void this.plugin.saveSettings();
});
return component;
}),
);
apiKeyGroup.addSetting(
setting =>
void setting