8000 Migrate game metadata import to task system #90 by DecDuck · Pull Request #103 · Drop-OSS/drop · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Migrate game metadata import to task system #90 #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pages/admin/library/import.vue
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ async function importGame(useMetadata: boolean) {
: undefined;
const option = games.unimportedGames[currentlySelectedGame.value];

const game = await $dropFetch("/api/v1/admin/import/game", {
const { taskId } = await $dropFetch("/api/v1/admin/import/game", {
method: "POST",
body: {
path: option.game,
Expand All @@ -343,7 +343,7 @@ async function importGame(useMetadata: boolean) {
},
});

router.push(`/admin/library/${game.id}`);
router.push(`/admin/task/${taskId}`);
}
function importGame_wrapper(metadata = true) {
importLoading.value = true;
Expand Down
6 changes: 1 addition & 5 deletions pages/admin/task/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,7 @@
<pre v-for="(line, idx) in task.log" :key="idx">{{ line }}</pre>
</div>
</div>
<div
v-else
role="status"
class="w-full h-screen flex items-center justify-center"
>
<div v-else role="status" class="w-full flex items-center justify-center">
<svg
aria-hidden="true"
class="size-8 text-transparent animate-spin fill-white"
Expand Down
17 changes: 12 additions & 5 deletions server/api/v1/admin/import/game/index.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,17 @@ export default defineEventHandler<{ body: typeof ImportGameBody.infer }>(
statusMessage: "Invalid library or game.",
});

if (!metadata) {
return await metadataHandler.createGameWithoutMetadata(library, path);
} else {
return await metadataHandler.createGame(metadata, library, path);
}
const taskId = metadata
? await metadataHandler.createGame(metadata, library, path)
: await metadataHandler.createGameWithoutMetadata(library, path);

if (!taskId)
throw createError({
statusCode: 400,
statusMessage:
"Duplicate metadata import. Please chose a different game or metadata provider.",
});

return { taskId };
},
);
30 changes: 24 additions & 6 deletions server/internal/metadata/giantbomb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import axios, { type AxiosRequestConfig } from "axios";
import TurndownService from "turndown";
import { DateTime } from "luxon";
import type { TaskRunContext } from "../tasks";

interface GiantBombResponseType<T> {
error: "OK" | string;
Expand Down Expand Up @@ -164,12 +165,12 @@ export class GiantBombProvider implements MetadataProvider {

return mapped;
}
async fetchGame({
id,
publisher,
developer,
createObject,
}: _FetchGameMetadataParams): Promise<GameMetadata> {
async fetchGame(
{ id, publisher, developer, createObject }: _FetchGameMetadataParams,
context?: TaskRunContext,
): Promise<GameMetadata> {
context?.log("Using GiantBomb provider");

const result = await this.request<GameResult>("game", id, {});
const gameData = result.data.results;

Expand All @@ -180,28 +181,38 @@ export class GiantBombProvider implements MetadataProvider {
const publishers: Company[] = [];
if (gameData.publishers) {
for (const pub of gameData.publishers) {
context?.log(`Importing publisher "${pub.name}"`);

const res = await publisher(pub.name);
if (res === undefined) continue;
publishers.push(res);
}
}

context?.progress(35);

const developers: Company[] = [];
if (gameData.developers) {
for (const dev of gameData.developers) {
context?.log(`Importing developer "${dev.name}"`);

const res = await developer(dev.name);
if (res === undefined) continue;
developers.push(res);
}
}

context?.progress(70);

const icon = createObject(gameData.image.icon_url);
const banner = createObject(gameData.image.screen_large_url);

const imageURLs: string[] = gameData.images.map((e) => e.original);

const images = [banner, ...imageURLs.map(createObject)];

context?.log(`Found all images. Total of ${images.length + 1}.`);

const releaseDate = gameData.original_release_date
? DateTime.fromISO(gameData.original_release_date).toJSDate()
: DateTime.fromISO(
Expand All @@ -210,8 +221,11 @@ export class GiantBombProvider implements MetadataProvider {
}-${gameData.expected_release_day ?? 1}`,
).toJSDate();

context?.progress(85);

const reviews: GameMetadataRating[] = [];
if (gameData.reviews) {
context?.log("Found reviews, importing...");
for (const { api_detail_url } of gameData.reviews) {
const reviewId = api_detail_url.split("/").at(-2);
if (!reviewId) continue;
Expand All @@ -225,6 +239,7 @@ export class GiantBombProvider implements MetadataProvider {
});
}
}

const metadata: GameMetadata = {
id: gameData.guid,
name: gameData.name,
Expand All @@ -245,6 +260,9 @@ export class GiantBombProvider implements MetadataProvider {
images,
};

context?.log("GiantBomb provider finished.");
context?.progress(100);

return metadata;
}
async fetchCompany({
Expand Down
189 changes: 104 additions & 85 deletions server/internal/metadata/igdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { AxiosRequestConfig } from "axios";
import axios from "axios";
import { DateTime } from "luxon";
import * as jdenticon from "jdenticon";
import type { TaskRunContext } from "../tasks";

type IGDBID = number;

Expand Down Expand Up @@ -345,107 +346,125 @@ export class IGDBProvider implements MetadataProvider {

return results;
}
async fetchGame({
id,
publisher,
developer,
createObject,
}: _FetchGameMetadataParams): Promise<GameMetadata> {
async fetchGame(
{ id, publisher, developer, createObject }: _FetchGameMetadataParams,
context?: TaskRunContext,
): Promise<GameMetadata> {
const body = `where id = ${id}; fields *;`;
const response = await this.request<IGDBGameFull>("games", body);
const currentGame = (await this.request<IGDBGameFull>("games", body)).at(0);
if (!currentGame) throw new Error("No game found on IGDB with that id");

for (let i = 0; i < response.length; i++) {
const currentGame = response[i];
if (!currentGame) continue;
context?.log("Using IDGB provider.");

let iconRaw;
const cover = currentGame.cover;
if (cover !== undefined) {
iconRaw = await this.getCoverURL(cover);
} else {
iconRaw = jdenticon.toPng(id, 512);
}
const icon = createObject(iconRaw);
let banner = "";

const images = [icon];
for (const art of currentGame.artworks ?? []) {
// if banner not set
if (banner.length <= 0) {
banner = createObject(await this.getArtworkURL(art));
images.push(banner);
} else {
images.push(createObject(await this.getArtworkURL(art)));
}
let iconRaw;
const cover = currentGame.cover;

if (cover !== undefined) {
context?.log("Found cover URL, using...");
iconRaw = await this.getCoverURL(cover);
} else {
context?.log("Missing cover URL, using fallback...");
iconRaw = jdenticon.toPng(id, 512);
}

const icon = createObject(iconRaw);
let banner;

const images = [icon];
for (const art of currentGame.artworks ?? []) {
const objectId = createObject(await this.getArtworkURL(art));
if (!banner) {
banner = objectId;
}
images.push(objectId);
}

if (!banner) {
banner = createObject(jdenticon.toPng(id, 512));
}

context?.progress(20);

const publishers: Company[] = [];
const developers: Company[] = [];
for (const involvedCompany of currentGame.involved_companies ?? []) {
// get details about the involved company
const involved_company_response =
await this.request<IGDBInvolvedCompany>(
"involved_companies",
`where id = ${involvedCompany}; fields *;`,
const publishers: Company[] = [];
const developers: Company[] = [];
for (const involvedCompany of currentGame.involved_companies ?? []) {
// get details about the involved company
const involved_company_response = await this.request<IGDBInvolvedCompany>(
"involved_companies",
`where id = ${involvedCompany}; fields *;`,
);
for (const foundInvolved of involved_company_response) {
// now we need to get the actual company so we can get the name
const findCompanyResponse = await this.request<
{ name: string } & IGDBItem
>("companies", `where id = ${foundInvolved.company}; fields name;`);

for (const company of findCompanyResponse) {
context?.log(
`Found involved company "${company.name}" as: ${foundInvolved.developer ? "developer, " : ""}${foundInvolved.publisher ? "publisher" : ""}`,
);
for (const foundInvolved of involved_company_response) {
// now we need to get the actual company so we can get the name
const findCompanyResponse = await this.request<
{ name: string } & IGDBItem
>("companies", `where id = ${foundInvolved.company}; fields name;`);

for (const company of findCompanyResponse) {
// if company was a dev or publisher
// CANNOT use else since a company can be both
if (foundInvolved.developer) {
const res = await developer(company.name);
if (res === undefined) continue;
developers.push(res);
}
if (foundInvolved.publisher) {
const res = await publisher(company.name);
if (res === undefined) continue;
publishers.push(res);
}

// if company was a dev or publisher
// CANNOT use else since a company can be both
if (foundInvolved.developer) {
const res = await developer(company.name);
if (res === undefined) continue;
developers.push(res);
}

if (foundInvolved.publisher) {
const res = await publisher(company.name);
if (res === undefined) continue;
publishers.push(res);
}
}
}
}

const firstReleaseDate = currentGame.first_release_date;
context?.progress(80);

return {
id: "" + response[i].id,
name: response[i].name,
shortDescription: this.trimMessage(currentGame.summary, 280),
description: currentGame.summary,
released:
firstReleaseDate === undefined
? new Date()
: DateTime.fromSeconds(firstReleaseDate).toJSDate(),
const firstReleaseDate = currentGame.first_release_date;
const released =
firstReleaseDate === undefined
? new Date()
: DateTime.fromSeconds(firstReleaseDate).toJSDate();

reviews: [
{
metadataId: "" + currentGame.id,
metadataSource: MetadataSource.IGDB,
mReviewCount: currentGame.total_rating_count ?? 0,
mReviewRating: (currentGame.total_rating ?? 0) / 100,
mReviewHref: currentGame.url,
},
],
const review = {
metadataId: currentGame.id.toString(),
metadataSource: MetadataSource.IGDB,
mReviewCount: currentGame.total_rating_count ?? 0,
mReviewRating: (currentGame.total_rating ?? 0) / 100,
mReviewHref: currentGame.url,
};

publishers: [],
developers: [],
const tags = await this.getGenres(currentGame.genres);

tags: await this.getGenres(currentGame.genres),
const deck = this.trimMessage(currentGame.summary, 280);

icon,
bannerId: banner,
coverId: icon,
images,
};
}
const metadata = {
id: currentGame.id.toString(),
name: currentGame.name,
shortDescription: deck,
description: currentGame.summary,
released,

reviews: [review],

publishers,
developers,

tags,

icon,
bannerId: banner,
coverId: icon,
images,
};

context?.log("IGDB provider finished.");
context?.progress(100);

throw new Error("No game found on igdb with that id");
return metadata;
}
async fetchCompany({
query,
Expand Down
Loading
0