3 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
fa9620eac1 Use 7zip for archive backend (#264)
* feat: use 7zip for archive backend

* fix: lint
2025-10-13 13:02:27 +11:00
8 changed files with 126 additions and 84 deletions

View File

@ -3,11 +3,11 @@
<button
v-for="(_, i) in amount"
:key="i"
@click="slideTo(i)"
:class="[
carousel.currentSlide === i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
'transition-all cursor-pointer h-2 rounded-full'
'transition-all cursor-pointer h-2 rounded-full',
]"
@click="slideTo(i)"
/>
</div>
</template>
@ -23,4 +23,4 @@ function slideTo(index: number) {
const offsetIndex = index + carousel.minSlide;
carousel.nav.slideTo(offsetIndex);
}
</script>
</script>

View File

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

View File

@ -21,7 +21,7 @@
},
"dependencies": {
"@discordapp/twemoji": "^16.0.1",
"@drop-oss/droplet": "3.0.1",
"@drop-oss/droplet": "3.2.0",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3",

93
pnpm-lock.yaml generated
View File

@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet
importers:
.:
@ -12,8 +15,8 @@ importers:
specifier: ^16.0.1
version: 16.0.1
'@drop-oss/droplet':
specifier: 3.0.1
version: 3.0.1
specifier: 3.2.0
version: 3.2.0
'@headlessui/vue':
specifier: ^1.7.23
version: 1.7.23(vue@3.5.22(typescript@5.8.3))
@ -370,67 +373,67 @@ packages:
'@discordapp/twemoji@16.0.1':
resolution: {integrity: sha512-figLiBWzjS5cyrAjLaGjM8AAaowO3qvK8rg5bA2dElB4qsaPMvBVlFDMO2d3x+nC1igt7kgWH4dvNmvvUHUF8w==}
'@drop-oss/droplet-darwin-arm64@3.0.1':
resolution: {integrity: sha512-LXe8vsXUBL96boI78H6oXpSaPVwF4cCwJ5l/QVtsOWMebNo6gk9wICDZ+5IoR/Ol32t1a1lk+DjbD1zeGenPxg==}
'@drop-oss/droplet-darwin-arm64@3.2.0':
resolution: {integrity: sha512-dH/vRFxuLjOzYBBvDG140wKcx4LmFxBJ5iTjZrWzV641wiRjx8B38niWXuqZ2ZADkCL4muOvgRGFJ4W1N/j6jQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@drop-oss/droplet-darwin-universal@3.0.1':
resolution: {integrity: sha512-Mf2gjC24u6s8djV/3slZvwdr4+h0qBu2OYXBUSDfR4H/VJwV5TstnWVKF+U8d1hjmHE9eLO8elbGNnpQmSoTOQ==}
'@drop-oss/droplet-darwin-universal@3.2.0':
resolution: {integrity: sha512-k7Xhzs2mXrQcm3SLhLNDBkUaCWqtbQ6dyme1ubsG9PZEcvv25T//8CNVFEsHVZTKqj5nF41iSh4Wz1Qn6VxkVw==}
engines: {node: '>= 10'}
os: [darwin]
'@drop-oss/droplet-darwin-x64@3.0.1':
resolution: {integrity: sha512-4IIDl/E+hzZ2Vt9m4FMPlZEXwj1EwE6qXyUidACK6TTFqpjLpsEHKuhv1FOxGyJ8qkvagtyPCc+cs1TxoZD6FA==}
'@drop-oss/droplet-darwin-x64@3.2.0':
resolution: {integrity: sha512-GvRwQrtcC1Dq6YyXxBGSFj+WasnIa1dk9t2lCaR9OQdh3qp2did21o2poo1Sgdjg+mI2lUdgZ6w0yXJlL1vl+A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@drop-oss/droplet-linux-arm64-gnu@3.0.1':
resolution: {integrity: sha512-klGvlLf1xSMT3iYsIAaBbmbir1ZJWtcVyOMUlsfc1lkJ8mgyB+PrW4BsnYj7Pp4G34n7WsOChjC8TdJDBBuBWg==}
'@drop-oss/droplet-linux-arm64-gnu@3.2.0':
resolution: {integrity: sha512-ZqH0xTEeSeJF77vy8rZDxHEV8JMaN0khdg6ptpnbBfc56J5jt6wS3NlHK8M0ZVlDqqZnXMS1vUO0b6rfmQodKw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@drop-oss/droplet-linux-arm64-musl@3.0.1':
resolution: {integrity: sha512-oOjvGETlrJGC1RlNhUoVS9N89Rn/0DqBauVz3BBFjJTKSd5jU3/gLzwgmfkKDGVEU5lyGPAn2WQroiESEG9wdA==}
'@drop-oss/droplet-linux-arm64-musl@3.2.0':
resolution: {integrity: sha512-TTw44PggYfp3RJkvNhXH89duuuvONEA8c8oRBCzCczRf3hDnbzCQLaB1UlnIlESsJZXXiFSDIBV2/0kkpB+Ukg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@drop-oss/droplet-linux-riscv64-gnu@3.0.1':
resolution: {integrity: sha512-Zf3gUsWq9Hqb275MOi7PJDhmJz7Qa/Y1XMen880bxPaOeDFqFOoKUxUr2/qv1MYp6tT3zO27NprGsHirYWqsyA==}
'@drop-oss/droplet-linux-riscv64-gnu@3.2.0':
resolution: {integrity: sha512-Ee/PfkoG8pm/9C3LFXJleIi5N8V5cK+44p+iDaneAo6gj5k67zYzuga3mJVswTgd3fncG1cw+xPqBl4PUWc1pg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@drop-oss/droplet-linux-x64-gnu@3.0.1':
resolution: {integrity: sha512-sskblycJdtNJVnRHjPHhwHkQUfQNaDIWDzXOzEaBPOcDKqYA7od7VMDAseqBkrKDn7l8bBUtRXFAipdsO8hffw==}
'@drop-oss/droplet-linux-x64-gnu@3.2.0':
resolution: {integrity: sha512-L2M/MEoe5Y74MTtzpEWHIvdyRSPLgM1WLzpb/xRNCWe8d6FcUFDgdMlbd6rDj5t4Q6JEzyMIHUciVRaYIv+ShA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@drop-oss/droplet-linux-x64-musl@3.0.1':
resolution: {integrity: sha512-lh+1M6UAf5+ET1/ZEFRsB3shFHjkT/9Ql9akr/vyUue91TWPmP71meqVkCugWDhP6lxBt56jg2VVrJfmPAsK6w==}
'@drop-oss/droplet-linux-x64-musl@3.2.0':
resolution: {integrity: sha512-F/uQUAHWbhiiAtoyKHQHPgjG7jJd8pQX6sCgdf5ufCdwFLvHEdu9pO0qN+xpzaACceIKX4Vip0vUwQwEzYhAKA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@drop-oss/droplet-win32-arm64-msvc@3.0.1':
resolution: {integrity: sha512-caQDPoDNJyyJXUEijw+hGTy0wmCrW5efTqBwnvMcQ282EOilg1d5WeJ31pfEcuLYF4MK1t9uaLcG6jZ9YLtzEQ==}
'@drop-oss/droplet-win32-arm64-msvc@3.2.0':
resolution: {integrity: sha512-x7i1KKL8vQGcXbKIyH56LCEdQxLKNEk/KFjuD/YGrbBJ/+Q+fh46hLK+Sx4I/HzPHecd5g3xc2kVgO7+DgjhYA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@drop-oss/droplet-win32-x64-msvc@3.0.1':
resolution: {integrity: sha512-bp8KwewF/T3JkVeJWkg86U3b0cGQD9i8k92x6HYPtnF5nLPAb2UIUEJgmYYFNPFe36RECBV7PIIG0ujdT1ELQw==}
'@drop-oss/droplet-win32-x64-msvc@3.2.0':
resolution: {integrity: sha512-lC8a456IQ0ArzX40IlStolV4GIdl26xF9PikcuQ9r+n4VDqWSHb8A0Wwj87leU3QdoMu+Y2nlA1QHKgpVSEuoQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@drop-oss/droplet@3.0.1':
resolution: {integrity: sha512-YhtgpwNqEHO8R03yf9Xb5LXuaLWkQvY+2lxOD1PwzpGI5V9PKlDE+x1IJBmdBF5bDPDGk9MxQidGtnYQuAEBEA==}
'@drop-oss/droplet@3.2.0':
resolution: {integrity: sha512-+3zw3MPriMrj8HlKAq2VTlXEPOXN0homusjmQcBRzVx7GjtGvb5Y9YIHs16qfn8zdTEDi5twrtsUBQYkVjU2bQ==}
engines: {node: '>= 10'}
'@emnapi/core@1.4.5':
@ -6133,48 +6136,48 @@ snapshots:
jsonfile: 5.0.0
universalify: 0.1.2
'@drop-oss/droplet-darwin-arm64@3.0.1':
'@drop-oss/droplet-darwin-arm64@3.2.0':
optional: true
'@drop-oss/droplet-darwin-universal@3.0.1':
'@drop-oss/droplet-darwin-universal@3.2.0':
optional: true
'@drop-oss/droplet-darwin-x64@3.0.1':
'@drop-oss/droplet-darwin-x64@3.2.0':
optional: true
'@drop-oss/droplet-linux-arm64-gnu@3.0.1':
'@drop-oss/droplet-linux-arm64-gnu@3.2.0':
optional: true
'@drop-oss/droplet-linux-arm64-musl@3.0.1':
'@drop-oss/droplet-linux-arm64-musl@3.2.0':
optional: true
'@drop-oss/droplet-linux-riscv64-gnu@3.0.1':
'@drop-oss/droplet-linux-riscv64-gnu@3.2.0':
optional: true
'@drop-oss/droplet-linux-x64-gnu@3.0.1':
'@drop-oss/droplet-linux-x64-gnu@3.2.0':
optional: true
'@drop-oss/droplet-linux-x64-musl@3.0.1':
'@drop-oss/droplet-linux-x64-musl@3.2.0':
optional: true
'@drop-oss/droplet-win32-arm64-msvc@3.0.1':
'@drop-oss/droplet-win32-arm64-msvc@3.2.0':
optional: true
'@drop-oss/droplet-win32-x64-msvc@3.0.1':
'@drop-oss/droplet-win32-x64-msvc@3.2.0':
optional: true
'@drop-oss/droplet@3.0.1':
'@drop-oss/droplet@3.2.0':
optionalDependencies:
'@drop-oss/droplet-darwin-arm64': 3.0.1
'@drop-oss/droplet-darwin-universal': 3.0.1
'@drop-oss/droplet-darwin-x64': 3.0.1
'@drop-oss/droplet-linux-arm64-gnu': 3.0.1
'@drop-oss/droplet-linux-arm64-musl': 3.0.1
'@drop-oss/droplet-linux-riscv64-gnu': 3.0.1
'@drop-oss/droplet-linux-x64-gnu': 3.0.1
'@drop-oss/droplet-linux-x64-musl': 3.0.1
'@drop-oss/droplet-win32-arm64-msvc': 3.0.1
'@drop-oss/droplet-win32-x64-msvc': 3.0.1
'@drop-oss/droplet-darwin-arm64': 3.2.0
'@drop-oss/droplet-darwin-universal': 3.2.0
'@drop-oss/droplet-darwin-x64': 3.2.0
'@drop-oss/droplet-linux-arm64-gnu': 3.2.0
'@drop-oss/droplet-linux-arm64-musl': 3.2.0
'@drop-oss/droplet-linux-riscv64-gnu': 3.2.0
'@drop-oss/droplet-linux-x64-gnu': 3.2.0
'@drop-oss/droplet-linux-x64-musl': 3.2.0
'@drop-oss/droplet-win32-arm64-msvc': 3.2.0
'@drop-oss/droplet-win32-x64-msvc': 3.2.0
'@emnapi/core@1.4.5':
dependencies:

View File

@ -1 +1,4 @@
overrides:
droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet
shamefullyHoist: true

View File

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

View File

@ -191,7 +191,7 @@ export class GiantBombProvider implements MetadataProvider {
const res = await publisher(pub.name);
if (res === undefined) {
context?.logger.warn(`Failed to import publisher "${pub}"`);
context?.logger.warn(`Failed to import publisher "${pub.name}"`);
continue;
}
context?.logger.info(`Imported publisher "${pub.name}"`);
@ -208,10 +208,10 @@ export class GiantBombProvider implements MetadataProvider {
const res = await developer(dev.name);
if (res === undefined) {
context?.logger.warn(`Failed to import developer "${dev}"`);
context?.logger.warn(`Failed to import developer "${dev.name}"`);
continue;
}
context?.logger.info(`Imported developer "${dev}"`);
context?.logger.info(`Imported developer "${dev.name}"`);
developers.push(res);
}
}

View File

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