From fccf46b8194644931c235802e940be73f6b88c9f Mon Sep 17 00:00:00 2001 From: Melih AK Date: Sat, 7 Feb 2026 15:47:37 +0300 Subject: [PATCH] feat: re-implement IGDB and RAWG providers with type safety and conflict resolution --- src/api/apis/IGDBAPI.ts | 111 +++++++++++++++++++++++++++++++++++++++ src/api/apis/RAWGAPI.ts | 65 +++++++++++++++++++++++ src/main.ts | 6 ++- src/settings/Settings.ts | 54 ++++++++++++++++++- 4 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/api/apis/IGDBAPI.ts create mode 100644 src/api/apis/RAWGAPI.ts diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts new file mode 100644 index 0000000..57a42f1 --- /dev/null +++ b/src/api/apis/IGDBAPI.ts @@ -0,0 +1,111 @@ +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 { + const currentTime = Date.now(); + if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; + + if (!this.plugin.settings.IGDBClientId || !this.plugin.settings.IGDBClientSecret) { + 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=${this.plugin.settings.IGDBClientId}&client_secret=${this.plugin.settings.IGDBClientSecret}&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 { + console.log(`MDB | api "${this.apiName}" queried by Title`); + 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': this.plugin.settings.IGDBClientId, '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 { + console.log(`MDB | api "${this.apiName}" queried by ID`); + 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': this.plugin.settings.IGDBClientId, '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 || []; } +} \ No newline at end of file diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts new file mode 100644 index 0000000..1c79e5a --- /dev/null +++ b/src/api/apis/RAWGAPI.ts @@ -0,0 +1,65 @@ +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 { + if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + const response = await requestUrl({ + url: `${this.apiUrl}/games?key=${this.plugin.settings.RAWGAPIKey}&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 { + if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + const response = await requestUrl({ + url: `${this.apiUrl}/games/${id}?key=${this.plugin.settings.RAWGAPIKey}`, + 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 || []; } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index df97525..7f7019e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,8 @@ 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 { RAWGAPI } from './api/apis/RAWGAPI'; import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; @@ -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(); @@ -728,4 +732,4 @@ export default class MediaDbPlugin extends Plugin { await this.saveData(this.settings); } -} +} \ No newline at end of file diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 6ccb6b3..9566bfc 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -18,6 +18,9 @@ export interface MediaDbPluginSettings { TMDBKey: string; MobyGamesKey: string; GiantBombKey: string; + IGDBClientId: string; + IGDBClientSecret: string; + RAWGAPIKey: string; ComicVineKey: string; BoardgameGeekKey: string; sfwFilter: boolean; @@ -32,6 +35,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[]; @@ -271,6 +276,9 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { TMDBKey: '', MobyGamesKey: '', GiantBombKey: '', + IGDBClientId: '', + IGDBClientSecret: '', + RAWGAPIKey: '', ComicVineKey: '', BoardgameGeekKey: '', sfwFilter: true, @@ -285,6 +293,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { BoardgameGeekAPI_disabledMediaTypes: [], ComicVineAPI_disabledMediaTypes: [], GiantBombAPI_disabledMediaTypes: [], + IGDBAPI_disabledMediaTypes: [], + RAWGAPI_disabledMediaTypes: [], MALAPI_disabledMediaTypes: [], MALAPIManga_disabledMediaTypes: [], MobyGamesAPI_disabledMediaTypes: [], @@ -595,6 +605,48 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }), ); + apiKeyGroup.addSetting( + setting => + void setting + .setName('IGDB Client ID') + .setDesc('Client ID for IGDB API (Required for Twitch OAuth).') + .addText(cb => { + cb.setPlaceholder('Client ID') + .setValue(this.plugin.settings.IGDBClientId) + .onChange(data => { + this.plugin.settings.IGDBClientId = data; + void this.plugin.saveSettings(); + }); + }), + ); + apiKeyGroup.addSetting( + setting => + void setting + .setName('IGDB Client Secret') + .setDesc('Client Secret for IGDB API.') + .addText(cb => { + cb.setPlaceholder('Client Secret') + .setValue(this.plugin.settings.IGDBClientSecret) + .onChange(data => { + this.plugin.settings.IGDBClientSecret = data; + void this.plugin.saveSettings(); + }); + }), + ); + apiKeyGroup.addSetting( + setting => + void setting + .setName('RAWG API Key') + .setDesc('API key for "rawg.io".') + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.RAWGAPIKey) + .onChange(data => { + this.plugin.settings.RAWGAPIKey = data; + void this.plugin.saveSettings(); + }); + }), + ); apiKeyGroup.addSetting( setting => void setting @@ -777,4 +829,4 @@ export class MediaDbSettingTab extends PluginSettingTab { }); } } -} +} \ No newline at end of file