feat: Auto-fetch game covers via Steam, Wikipedia, and CheapShark API fallback

This commit includes:

- PocketBase hook to download the highest quality vertical cover images possible.

- UI adjustments with blurred background fallback for non-vertical layouts.

- Bugfixes in PocketBase docker startup command.
This commit is contained in:
mmacierzanka committed 2026-03-31 21:13:19 +02:00
1 parent b430c0c204
commit 2ab3ada62f
5 files changed
+121 -23

No files matched your search

+3 -2
View File
@@ -30,7 +30,8 @@ FROM pb_base AS production
# Skopiuj statyczny build frontendu prosto z vite do pb_public (serwowania przez Pocketbase)
COPY --from=build_frontend /app/dist /pb_public
# Skopiuj ewentualne migracje na produkcję
# Skopiuj ewentualne migracje i hooki na produkcję
COPY backend/pb_migrations /pb_migrations
COPY backend/pb_hooks /pb_hooks
CMD ["/pocketbase", "serve", "--http=0.0.0.0:8090"]
CMD ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--hooksDir=/pb_hooks"]
+111
View File
@@ -0,0 +1,111 @@
// Pocketbase Hook fetch_covers
console.log("POCKETBASE HOOK: loaded fetch_covers.pb.js");
onRecordAfterCreateRequest((e) => {
try {
console.log("Wyzwolono onRecordAfterCreateRequest dla " + e.record.get("title"));
const title = e.record.get("title");
if (!title) return;
// Jeśli ma już okładkę, ignorujemy
const existingFile = e.record.get("cover_image");
if (existingFile) return;
const urlArgs = encodeURIComponent(title);
let thumbUrl = null;
console.log("Szukam w Steam API: " + title);
try {
const steamRes = $http.send({
url: `https://store.steampowered.com/api/storesearch/?term=${urlArgs}&l=english&cc=US`,
method: "GET"
});
if (steamRes.json && steamRes.json.total > 0 && steamRes.json.items.length > 0) {
const appId = steamRes.json.items[0].id;
// Steam serwuje kapitalne pionowe okladki library_600x900_2x (2x to wysoka rozdzielczosc)
const steamThumb = `https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/${appId}/library_600x900_2x.jpg`;
try {
const checkRes = $http.send({ url: steamThumb, method: "HEAD" });
if (checkRes.statusCode === 200) {
thumbUrl = steamThumb;
console.log("Znaleziono okładke Steam dla: " + title);
}
} catch(err) {
// Brak okładki 600x900, probujemy standardowej capsule
const fallbackSteam = `https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/${appId}/capsule_616x353.jpg`;
const checkRes2 = $http.send({ url: fallbackSteam, method: "HEAD" });
if (checkRes2.statusCode === 200) {
thumbUrl = fallbackSteam;
}
}
}
} catch (e) {
console.log("Steam API error, pomijam:", e);
}
if (!thumbUrl) {
console.log("Szukam w Wikipedia API (fallback): " + title);
try {
const wikiRes = $http.send({
url: `https://en.wikipedia.org/w/api.php?action=query&prop=pageimages&titles=${urlArgs}&pithumbsize=600&format=json`,
method: "GET",
headers: { "User-Agent": "Nene-Game-App/1.0" }
});
const pages = wikiRes.json && wikiRes.json.query && wikiRes.json.query.pages;
if (pages) {
const keys = Object.keys(pages);
if (keys.length > 0 && pages[keys[0]].thumbnail) {
thumbUrl = pages[keys[0]].thumbnail.source;
console.log("Znaleziono okładke na Wikipedii dla: " + title);
}
}
} catch(e) {
console.log("Wikipedia API error, pomijam:", e);
}
}
if (!thumbUrl) {
console.log("Szukam w CheapShark API (druga deska ratunku): " + title);
try {
const searchRes = $http.send({
url: `https://www.cheapshark.com/api/1.0/games?title=${urlArgs}`,
method: "GET"
});
if (searchRes.json && searchRes.json.length > 0) {
thumbUrl = searchRes.json[0].thumb;
console.log("Znaleziono miniaturę w CheapShark dla: " + title);
}
} catch(e) {}
}
if (thumbUrl) {
console.log("Ostatecznie pobieram wybraną okladke z sieci...");
const imgRes = $http.send({
url: thumbUrl,
method: "GET"
});
const safeTitle = title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
let ext = ".jpg";
const lowerThumbUrl = thumbUrl.toLowerCase();
if (lowerThumbUrl.includes(".png")) ext = ".png";
else if (lowerThumbUrl.includes(".webp")) ext = ".webp";
else if (lowerThumbUrl.includes(".gif")) ext = ".gif";
const file = $filesystem.fileFromBytes(imgRes.raw, `${safeTitle}_cover${ext}`);
// Używamy formularza (RecordUpsertForm) aby wgrać do serwera i zlinkować
const form = new RecordUpsertForm($app, e.record);
form.addFiles("cover_image", file);
form.submit();
console.log("Pomyslnie dodano okładkę dla: " + title);
} else {
console.log("Niestety w żadnym źródle (Steam, Wikipedia, CheapShark) nie znaleziono danych dla: " + title);
}
} catch (err) {
console.error("Generalny błąd podczas pobierania okladki: ", err);
}
}, "games");
@@ -55,25 +55,6 @@ migrate((db) => {
const dao = new Dao(db);
dao.saveCollection(collection);
// Add examples:
const examples = [
"Wiedźmin 3: Dziki Gon",
"Cyberpunk 2077",
"Red Dead Redemption 2",
"Baldur's Gate 3",
"GTA V",
"Minecraft",
"Hades",
"Elden Ring"
];
for (const title of examples) {
const record = new Record(collection);
record.set("title", title);
dao.saveRecord(record);
}
}, (db) => {
const dao = new Dao(db);
const collection = dao.findCollectionByNameOrId("games1234567890");
+2
View File
@@ -6,11 +6,13 @@ services:
context: .
dockerfile: Dockerfile
target: pb_base
command: ["/pocketbase", "serve", "--http=0.0.0.0:8090", "--hooksDir=/pb_hooks"]
ports:
- "8090:8090"
volumes:
- ./pb_data:/pb_data
- ./backend/pb_migrations:/pb_migrations
- ./backend/pb_hooks:/pb_hooks
restart: unless-stopped
frontend:
+5 -2
View File
@@ -84,8 +84,11 @@ async function handleDraw() {
: null;
card.innerHTML = `
<div class="game-cover">
${imageUrl ? `<img src="${imageUrl}" alt="Okładka" width="100%" height="100%" style="object-fit:cover;">` : '<span>Brak Okładki</span>'}
<div class="game-cover" style="position: relative; overflow: hidden;">
${imageUrl ? `
<img src="${imageUrl}" style="position: absolute; width: 100%; height: 100%; object-fit: cover; filter: blur(12px) brightness(0.4); transform: scale(1.2); z-index: 1;">
<img src="${imageUrl}" alt="Okładka" style="position: relative; width: 100%; height: 100%; object-fit: contain; z-index: 2; padding: 15px;">
` : '<span style="z-index: 2; position: relative;">Brak Okładki</span>'}
</div>
<div class="game-info">
<div class="game-title">${game.title}</div>