2 Commits

Author SHA1 Message Date
2a23f4d14c Fix lints 2025-10-24 09:33:39 +11:00
b20d355527 Improve igdb metadata fetching (#257)
* improve igdb metadata fetching

    * Make sure to get images with reasonable resolution.
      By default the url igdb returns is in "t_thumb" size,
      an image of size 90x90, which is good only for the icon,
      but bad for pretty much else. This commit will make sure
      covers will be of size "t_cover_big", artworks of 1080p
      height (i.e. "t_1080p") and logos will have their original
      size ("t_original"). Maybe "t_logo_med" is more appropriate?

    * Fetch screenshots as well.

    * Use a separate image for icon and for cover.
      icon needs to be a square, and can be of low
      resolution, so the "t_thmb" size is more appropriate
      for him.

    * If there is a storyline for a game use it as a short
      description.

* IDGB -> IGDB

* use the longer text between storyline and description for description

---------

Co-authored-by: udifogiel <udifogiel@proton.me>
2025-10-24 09:25:54 +11:00
3 changed files with 68 additions and 32 deletions

View File

@ -1,3 +1,12 @@
<i18n>
{
"en": {
"↓": "↓",
"↑": "↑"
}
}
</i18n>
<template> <template>
<div> <div>
<div> <div>
@ -180,7 +189,7 @@
> >
{{ option.name }} {{ option.name }}
<span v-if="currentSort === option.param"> <span v-if="currentSort === option.param">
{{ sortOrder === 'asc' ? '↑' : '↓' }} {{ sortOrder === "asc" ? $t("↑") : $t("↓") }}
</span> </span>
</button> </button>
</MenuItem> </MenuItem>
@ -500,11 +509,10 @@ await updateGames(filterQuery.value, true);
function handleSortClick(option: StoreSortOption, event: MouseEvent) { function handleSortClick(option: StoreSortOption, event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
if (currentSort.value === option.param) { if (currentSort.value === option.param) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'; sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
} else { } else {
currentSort.value = option.param; currentSort.value = option.param;
sortOrder.value = option.param === 'name' ? 'asc' : 'desc'; sortOrder.value = option.param === "name" ? "asc" : "desc";
} }
} }
</script>
</script>

View File

@ -123,4 +123,4 @@ export default defineEventHandler(async (h3) => {
]); ]);
return { results, count }; return { results, count };
}); });

View File

@ -72,7 +72,7 @@ interface IGDBCompanyWebsite extends IGDBItem {
} }
interface IGDBCover extends IGDBItem { interface IGDBCover extends IGDBItem {
url: string; image_id: string;
} }
interface IGDBSearchStub extends IGDBItem { interface IGDBSearchStub extends IGDBItem {
@ -179,7 +179,7 @@ export class IGDBProvider implements MetadataProvider {
if (response.status !== 200) if (response.status !== 200)
throw new Error( throw new Error(
`Error in IDGB \nStatus Code: ${response.status}\n${response.data}`, `Error in IGDB \nStatus Code: ${response.status}\n${response.data}`,
); );
this.accessToken = response.data.access_token; this.accessToken = response.data.access_token;
@ -187,7 +187,7 @@ export class IGDBProvider implements MetadataProvider {
seconds: response.data.expires_in, seconds: response.data.expires_in,
}); });
logger.info("IDGB done authorizing with twitch"); logger.info("IGDB done authorizing with twitch");
} }
private async refreshCredentials() { private async refreshCredentials() {
@ -246,39 +246,47 @@ export class IGDBProvider implements MetadataProvider {
return <T[]>response.data; return <T[]>response.data;
} }
private async _getMediaInternal(mediaID: IGDBID, type: string) { private async _getMediaInternal(
mediaID: IGDBID,
type: string,
size: string = "t_thumb",
) {
if (mediaID === undefined) if (mediaID === undefined)
throw new Error( throw new Error(
`IGDB mediaID when getting item of type ${type} was undefined`, `IGDB mediaID when getting item of type ${type} was undefined`,
); );
const body = `where id = ${mediaID}; fields url;`; const body = `where id = ${mediaID}; fields image_id;`;
const response = await this.request<IGDBCover>(type, body); const response = await this.request<IGDBCover>(type, body);
let result = ""; if (!response.length || !response[0].image_id) {
throw new Error(`No image_id found for ${type} with id ${mediaID}`);
}
response.forEach((cover) => { const imageId = response[0].image_id;
if (cover.url.startsWith("https:")) { const result = `https://images.igdb.com/igdb/image/upload/${size}/${imageId}.jpg`;
result = cover.url;
} else {
// twitch *sometimes* provides it in the format "//images.igdb.com"
result = `https:${cover.url}`;
}
});
return result; return result;
} }
private async getCoverURL(id: IGDBID) { private async getCoverURL(id: IGDBID) {
return await this._getMediaInternal(id, "covers"); return await this._getMediaInternal(id, "covers", "t_cover_big");
} }
private async getArtworkURL(id: IGDBID) { private async getArtworkURL(id: IGDBID) {
return await this._getMediaInternal(id, "artworks"); return await this._getMediaInternal(id, "artworks", "t_1080p");
}
private async getScreenshotURL(id: IGDBID) {
return await this._getMediaInternal(id, "screenshots", "t_1080p");
}
private async getIconURL(id: IGDBID) {
return await this._getMediaInternal(id, "covers", "t_thumb");
} }
private async getCompanyLogoURl(id: IGDBID) { private async getCompanyLogoURl(id: IGDBID) {
return await this._getMediaInternal(id, "company_logos"); return await this._getMediaInternal(id, "company_logos", "t_original");
} }
private trimMessage(msg: string, len: number) { private trimMessage(msg: string, len: number) {
@ -327,7 +335,7 @@ export class IGDBProvider implements MetadataProvider {
let icon = ""; let icon = "";
const cover = response[i].cover; const cover = response[i].cover;
if (cover !== undefined) { if (cover !== undefined) {
icon = await this.getCoverURL(cover); icon = await this.getIconURL(cover);
} else { } else {
icon = ""; icon = "";
} }
@ -355,23 +363,26 @@ export class IGDBProvider implements MetadataProvider {
const currentGame = (await this.request<IGDBGameFull>("games", body)).at(0); const currentGame = (await this.request<IGDBGameFull>("games", body)).at(0);
if (!currentGame) throw new Error("No game found on IGDB with that id"); if (!currentGame) throw new Error("No game found on IGDB with that id");
context?.logger.info("Using IDGB provider."); context?.logger.info("Using IGDB provider.");
let iconRaw; let iconRaw, coverRaw;
const cover = currentGame.cover; const cover = currentGame.cover;
if (cover !== undefined) { if (cover !== undefined) {
context?.logger.info("Found cover URL, using..."); context?.logger.info("Found cover URL, using...");
iconRaw = await this.getCoverURL(cover); iconRaw = await this.getIconURL(cover);
coverRaw = await this.getCoverURL(cover);
} else { } else {
context?.logger.info("Missing cover URL, using fallback..."); context?.logger.info("Missing cover URL, using fallback...");
iconRaw = jdenticon.toPng(id, 512); iconRaw = jdenticon.toPng(id, 512);
coverRaw = iconRaw;
} }
const icon = createObject(iconRaw); const icon = createObject(iconRaw);
const coverID = createObject(coverRaw);
let banner; let banner;
const images = [icon]; const images = [coverID];
for (const art of currentGame.artworks ?? []) { for (const art of currentGame.artworks ?? []) {
const objectId = createObject(await this.getArtworkURL(art)); const objectId = createObject(await this.getArtworkURL(art));
if (!banner) { if (!banner) {
@ -384,6 +395,11 @@ export class IGDBProvider implements MetadataProvider {
banner = createObject(jdenticon.toPng(id, 512)); banner = createObject(jdenticon.toPng(id, 512));
} }
for (const screenshot of currentGame.screenshots ?? []) {
const objectId = createObject(await this.getScreenshotURL(screenshot));
images.push(objectId);
}
context?.progress(20); context?.progress(20);
const publishers: CompanyModel[] = []; const publishers: CompanyModel[] = [];
@ -452,13 +468,25 @@ export class IGDBProvider implements MetadataProvider {
const genres = await this.getGenres(currentGame.genres); const genres = await this.getGenres(currentGame.genres);
const deck = this.trimMessage(currentGame.summary, 280); let description = "";
let shortDescription = "";
if (currentGame.summary.length > (currentGame.storyline?.length ?? 0)) {
description = currentGame.summary;
shortDescription = this.trimMessage(
currentGame.storyline ?? currentGame.summary,
280,
);
} else {
description = currentGame.storyline ?? currentGame.summary;
shortDescription = this.trimMessage(currentGame.summary, 280);
}
const metadata = { const metadata = {
id: currentGame.id.toString(), id: currentGame.id.toString(),
name: currentGame.name, name: currentGame.name,
shortDescription: deck, shortDescription,
description: currentGame.summary, description,
released, released,
genres, genres,
@ -471,7 +499,7 @@ export class IGDBProvider implements MetadataProvider {
icon, icon,
bannerId: banner, bannerId: banner,
coverId: icon, coverId: coverID,
images, images,
}; };