From 8dad1b221b3b1b30c888dae507a977798baa84ee Mon Sep 17 00:00:00 2001 From: Melih Ak Date: Sun, 18 Jan 2026 05:08:53 +0300 Subject: [PATCH 1/3] feat: implement IGDB and RAWG api wrappers --- src/api/apis/IGDBAPI.ts | 190 ++++++++++++++++++++++++++++++++++++++++ src/api/apis/RAWGAPI.ts | 109 +++++++++++++++++++++++ 2 files changed, 299 insertions(+) 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..60fba2a --- /dev/null +++ b/src/api/apis/IGDBAPI.ts @@ -0,0 +1,190 @@ +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'; + +export class IGDBAPI extends APIModel { + plugin: MediaDbPlugin; + // DÜZELTME 1: Formatı ISO standartına (YYYY-MM-DD) çektik + 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; + 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; + const ret: MediaTypeModel[] = []; + + for (const result of data) { + let year = ''; + if (result.first_release_date) { + year = new Date(result.first_release_date * 1000).getFullYear().toString(); + } + + let image = ''; + if (result.cover?.url) { + image = 'https:' + result.cover.url.replace('t_thumb', 't_cover_big'); + } + + ret.push( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: year, + dataSource: this.apiName, + id: result.id.toString(), + image: image + }), + ); + } + + return ret; + } + + 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; + if (!data || data.length === 0) { + throw Error(`MDB | No result found for ID ${id}`); + } + + const result = data[0]; + + const developers: string[] = []; + const publishers: string[] = []; + + if (result.involved_companies) { + result.involved_companies.forEach((company: any) => { + if (company.developer) developers.push(company.company.name); + if (company.publisher) publishers.push(company.company.name); + }); + } + + const genres = result.genres ? result.genres.map((g: any) => g.name) : []; + + let image = ''; + if (result.cover?.url) { + image = 'https:' + result.cover.url.replace('t_thumb', 't_cover_big'); + } + + // DÜZELTME 2: Date objesi yerine String'e çevirip gönderiyoruz + const dateStr = result.first_release_date + ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] + : ''; + + 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: genres, + onlineRating: result.total_rating, + image: image, + + released: true, + // Hata veren satır düzeltildi: + 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..70bbe52 --- /dev/null +++ b/src/api/apis/RAWGAPI.ts @@ -0,0 +1,109 @@ +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'; + +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 { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + 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 === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.status !== 200) { + throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); + } + + const data = response.json; + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + 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 // RAWG arama sonucunda resim verir + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + 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 | Received status code ${response.status} from ${this.apiName}.`); + } + + const result = response.json; + + const developers = result.developers?.map((d: any) => d.name) || []; + const publishers = result.publishers?.map((p: any) => p.name) || []; + const genres = result.genres?.map((g: any) => g.name) || []; + + 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: developers, + publishers: publishers, + genres: genres, + onlineRating: result.metacritic, + image: result.background_image, // RAWG yüksek kaliteli görseli default verir + + released: result.released != null, + releaseDate: result.released && this.plugin.dateFormatter.format(result.released, this.apiDateFormat) || '', + + userData: { + played: false, + personalRating: 0, + }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; + } +} \ No newline at end of file From 753996ed3c913e9939c37f7549e37aa99f645a3d Mon Sep 17 00:00:00 2001 From: Melih Ak Date: Sun, 18 Jan 2026 05:11:59 +0300 Subject: [PATCH 2/3] feat: add settings for IGDB client credentials and RAWG api key --- src/settings/Settings.ts | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d50db67..bb425bc 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -15,6 +15,9 @@ export interface MediaDbPluginSettings { OMDbKey: string; MobyGamesKey: string; GiantBombKey: string; + IGDBClientId: string; + IGDBClientSecret: string; + RAWGAPIKey: string; ComicVineKey: string; BoardgameGeekKey: string; sfwFilter: boolean; @@ -30,6 +33,8 @@ export interface MediaDbPluginSettings { SteamAPI_disabledMediaTypes: MediaType[]; MobyGamesAPI_disabledMediaTypes: MediaType[]; GiantBombAPI_disabledMediaTypes: MediaType[]; + IGDBAPI_disabledMediaTypes: MediaType[]; + RAWGAPI_disabledMediaTypes: MediaType[]; WikipediaAPI_disabledMediaTypes: MediaType[]; BoardgameGeekAPI_disabledMediaTypes: MediaType[]; MusicBrainzAPI_disabledMediaTypes: MediaType[]; @@ -79,6 +84,9 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { OMDbKey: '', MobyGamesKey: '', GiantBombKey: '', + IGDBClientId: '', + IGDBClientSecret: '', + RAWGAPIKey: '', ComicVineKey: '', BoardgameGeekKey: '', sfwFilter: true, @@ -94,6 +102,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { SteamAPI_disabledMediaTypes: [], MobyGamesAPI_disabledMediaTypes: [], GiantBombAPI_disabledMediaTypes: [], + IGDBAPI_disabledMediaTypes: [], + RAWGAPI_disabledMediaTypes: [], WikipediaAPI_disabledMediaTypes: [], BoardgameGeekAPI_disabledMediaTypes: [], MusicBrainzAPI_disabledMediaTypes: [], @@ -251,6 +261,41 @@ export class MediaDbSettingTab extends PluginSettingTab { void this.plugin.saveSettings(); }); }); + new Setting(containerEl) + .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(); + }); + }); + + new Setting(containerEl) + .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(); + }); + }); + + new Setting(containerEl) + .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(); + }); + }); new Setting(containerEl) .setName('Comic Vine Key') .setDesc('API key for "www.comicvine.gamespot.com".') From 5a1ec8a6c2f338230bfc1807373d2d9d38f9a248 Mon Sep 17 00:00:00 2001 From: Melih Ak Date: Sun, 18 Jan 2026 05:12:09 +0300 Subject: [PATCH 3/3] feat: register IGDB and RAWG providers in plugin lifecycle --- src/main.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.ts b/src/main.ts index faaf025..18be31c 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'; @@ -62,6 +64,9 @@ export default class MediaDbPlugin extends Plugin { 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.mediaTypeManager = new MediaTypeManager(); this.modelPropertyMapper = new PropertyMapper(this); this.modalHelper = new ModalHelper(this);