264 lines
8.8 KiB
TypeScript
264 lines
8.8 KiB
TypeScript
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<Result<string, MDBError>> {
|
|
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<Result<MediaTypeModel[], MDBError>> {
|
|
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<Result<MediaTypeModel, MDBError>> {
|
|
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 ?? [];
|
|
}
|
|
}
|