import { requestUrl } from 'obsidian'; import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { Logger } from 'packages/obsidian/src/utils/Logger'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; 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 ok(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) { return err({ kind: MDBErrorKind.Validation, message: `MDB | Client ID or Client Secret for ${this.apiName} missing.`, userMessage: `Client ID or Client Secret for ${this.apiName} missing.`, context: { apiName: this.apiName }, }); } Logger.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); const responseResult = await fromPromise( requestUrl({ url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, method: 'POST', }), cause => toMdbError(cause, { kind: MDBErrorKind.Network, message: `MDB | Network error querying Twitch auth for ${this.apiName}`, userMessage: `Network error querying Twitch auth for ${this.apiName}`, context: { apiName: this.apiName }, }), ); if (!responseResult.ok) { return err(responseResult.error); } const response = responseResult.value; if (response.status !== 200) { return err({ kind: MDBErrorKind.Api, message: `MDB | Auth failed for ${this.apiName}. Check Credentials.`, userMessage: `Auth failed for ${this.apiName}. Check Credentials.`, context: { apiName: this.apiName, status: response.status }, }); } const data = response.json as TwitchAuthResponse; this.accessToken = data.access_token; this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; return ok(this.accessToken); } async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); if (!clientId) { return err({ kind: MDBErrorKind.Validation, message: `MDB | Client ID for ${this.apiName} missing.`, userMessage: `Client ID for ${this.apiName} missing.`, context: { apiName: this.apiName }, }); } const tokenResult = await this.getAuthToken(); if (!tokenResult.ok) { return err(tokenResult.error); } const token = tokenResult.value; const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const responseResult = await fromPromise( requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, body: queryBody, }), cause => toMdbError(cause, { kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, }), ); if (!responseResult.ok) { return err(responseResult.error); } const response = responseResult.value; if (response.status !== 200) { return err({ kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, }); } const data = response.json as IGDBGame[]; return ok( 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> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); if (!clientId) { return err({ kind: MDBErrorKind.Validation, message: `MDB | Client ID for ${this.apiName} missing.`, userMessage: `Client ID for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, }); } const tokenResult = await this.getAuthToken(); if (!tokenResult.ok) { return err(tokenResult.error); } const token = tokenResult.value; 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 responseResult = await fromPromise( requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, body: queryBody, }), cause => toMdbError(cause, { kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, }), ); if (!responseResult.ok) { return err(responseResult.error); } const response = responseResult.value; if (response.status !== 200) { return err({ kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, }); } const data = response.json as IGDBGame[]; if (!data || data.length === 0) { return err({ kind: MDBErrorKind.Api, message: `MDB | No result found for ID ${id}`, userMessage: `No result found for ID ${id}`, context: { apiName: this.apiName, 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 ok( 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 ?? []; } }