feat: re-implement IGDB and RAWG providers with type safety and conflict resolution
This commit is contained in:
parent
e503ca5c66
commit
fccf46b819
4 changed files with 234 additions and 2 deletions
111
src/api/apis/IGDBAPI.ts
Normal file
111
src/api/apis/IGDBAPI.ts
Normal file
|
|
@ -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<string> {
|
||||
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<MediaTypeModel[]> {
|
||||
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<MediaTypeModel> {
|
||||
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 || []; }
|
||||
}
|
||||
65
src/api/apis/RAWGAPI.ts
Normal file
65
src/api/apis/RAWGAPI.ts
Normal file
|
|
@ -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<MediaTypeModel[]> {
|
||||
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<MediaTypeModel> {
|
||||
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 || []; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue