diff --git a/frontend/src/pages/GameDetail.svelte b/frontend/src/pages/GameDetail.svelte index 2b79a14..d85f2e8 100644 --- a/frontend/src/pages/GameDetail.svelte +++ b/frontend/src/pages/GameDetail.svelte @@ -186,6 +186,38 @@ + + {#if game.videos?.length || game.screenshots?.length} +
+ 📺 Media +
+ {#if game.videos?.length} +
+ +
+ {/if} + {#if game.screenshots?.length} +
+ {#each game.screenshots as shot} + Screenshot + {/each} +
+ {/if} +
+
+ {/if} +
@@ -280,6 +312,61 @@ .edit-actions { display: flex; gap: 8px; margin-top: 12px; } textarea { width: 100%; min-height: 100px; } + /* Media Section — video + screenshots */ + .media-section { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid var(--border); + } + .media-label { + display: block; + font-size: 0.85rem; + color: var(--text-dim); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 12px; + } + .media-grid { + display: flex; + flex-direction: column; + gap: 16px; + } + .video-container { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; + border-radius: var(--radius); + overflow: hidden; + background: #000; + } + .video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + .screenshots-row { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 4px; + } + .screenshot-thumb { + flex-shrink: 0; + width: 240px; + height: 135px; + object-fit: cover; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + transition: border-color 0.15s; + cursor: pointer; + } + .screenshot-thumb:hover { + border-color: var(--phosphor-dim); + } + /* IGDB Scrape */ .scrape-section { margin-top: 24px; diff --git a/src/main/java/com/dostalgia/Game.java b/src/main/java/com/dostalgia/Game.java index f69a0c2..ce832c1 100644 --- a/src/main/java/com/dostalgia/Game.java +++ b/src/main/java/com/dostalgia/Game.java @@ -28,6 +28,7 @@ public class Game { private boolean hasCover; private boolean hasScreenshots; private List screenshots; + private List videos; private boolean ready; private Instant createdAt; @@ -95,6 +96,9 @@ public class Game { public List getScreenshots() { return screenshots; } public void setScreenshots(List screenshots) { this.screenshots = screenshots; } + public List getVideos() { return videos; } + public void setVideos(List videos) { this.videos = videos; } + public boolean isReady() { return ready; } public void setReady(boolean ready) { this.ready = ready; } diff --git a/src/main/java/com/dostalgia/IgdbService.java b/src/main/java/com/dostalgia/IgdbService.java index d2d0632..5a1f431 100644 --- a/src/main/java/com/dostalgia/IgdbService.java +++ b/src/main/java/com/dostalgia/IgdbService.java @@ -93,11 +93,12 @@ public class IgdbService { public List> search(String query) throws Exception { if (!isConfigured()) return List.of(); - // Search broadly; IGDB returns relevance-sorted results + // Search limited to DOS platform (IGDB platform ID 13) String apql = "search \"" + sanitize(query) + "\";" + " fields name,first_release_date,genres.name," + " involved_companies.company.name,involved_companies.developer,involved_companies.publisher," + " summary,cover.url,platforms;" + + " where platforms = [13];" + " limit 20;"; HttpRequest req = igdbRequest("/games") @@ -212,6 +213,57 @@ public class IgdbService { return IMG_BASE + "/" + COVER_SIZE + "/" + thumbUrl.substring(thumbUrl.lastIndexOf('/') + 1); } + /** + * Fetch YouTube video IDs associated with an IGDB game. + */ + public List fetchVideos(int igdbId) throws Exception { + String apql = "fields video_id; where game = " + igdbId + ";"; + HttpRequest req = igdbRequest("/game_videos") + .POST(HttpRequest.BodyPublishers.ofString(apql)) + .build(); + + HttpResponse res = http.send(req, HttpResponse.BodyHandlers.ofString()); + if (res.statusCode() != 200) return List.of(); + + JsonNode results = mapper.readTree(res.body()); + if (!results.isArray()) return List.of(); + + List videos = new ArrayList<>(); + for (JsonNode v : results) { + if (v.has("video_id")) { + videos.add(v.get("video_id").asText()); + } + } + return videos; + } + + /** + * Fetch screenshot image URLs for an IGDB game. + */ + public List fetchScreenshots(int igdbId) throws Exception { + String apql = "fields url; where game = " + igdbId + ";"; + HttpRequest req = igdbRequest("/screenshots") + .POST(HttpRequest.BodyPublishers.ofString(apql)) + .build(); + + HttpResponse res = http.send(req, HttpResponse.BodyHandlers.ofString()); + if (res.statusCode() != 200) return List.of(); + + JsonNode results = mapper.readTree(res.body()); + if (!results.isArray()) return List.of(); + + List screenshots = new ArrayList<>(); + for (JsonNode s : results) { + if (s.has("url")) { + String thumbUrl = s.get("url").asText(); + // Use bigger size for display + String fullUrl = "https:" + thumbUrl.replace("t_thumb", "t_1080p"); + screenshots.add(fullUrl); + } + } + return screenshots; + } + // ─── Auto-Scrape (called during upload) ────────────────────── /** @@ -242,6 +294,29 @@ public class IgdbService { applyMatch(game, best); LOG.info("IGDB auto-scrape: applied '" + best.get("name") + "' to '" + game.getTitle() + "'"); + // Fetch videos and screenshots + Object igdbIdObj = best.get("igdb_id"); + if (igdbIdObj instanceof Number igdbIdNum) { + int igdbId = igdbIdNum.intValue(); + try { + List videos = fetchVideos(igdbId); + if (!videos.isEmpty()) { + game.setVideos(videos); + } + } catch (Exception e) { + LOG.warning("IGDB: failed to fetch videos: " + e.getMessage()); + } + try { + List screenshots = fetchScreenshots(igdbId); + if (!screenshots.isEmpty()) { + game.setScreenshots(screenshots); + game.setHasScreenshots(true); + } + } catch (Exception e) { + LOG.warning("IGDB: failed to fetch screenshots: " + e.getMessage()); + } + } + } catch (Exception e) { LOG.warning("IGDB auto-scrape failed for '" + game.getTitle() + "': " + e.getMessage()); } @@ -272,6 +347,25 @@ public class IgdbService { } applyMatch(game, results.get(0)); + + // Fetch videos and screenshots + try { + List videos = fetchVideos(igdbId); + if (!videos.isEmpty()) { + game.setVideos(videos); + } + } catch (Exception e) { + LOG.warning("IGDB: failed to fetch videos for game " + igdbId + ": " + e.getMessage()); + } + try { + List screenshots = fetchScreenshots(igdbId); + if (!screenshots.isEmpty()) { + game.setScreenshots(screenshots); + game.setHasScreenshots(true); + } + } catch (Exception e) { + LOG.warning("IGDB: failed to fetch screenshots for game " + igdbId + ": " + e.getMessage()); + } } /** diff --git a/src/main/java/com/dostalgia/UploadResource.java b/src/main/java/com/dostalgia/UploadResource.java index d5928f8..92b6c5f 100644 --- a/src/main/java/com/dostalgia/UploadResource.java +++ b/src/main/java/com/dostalgia/UploadResource.java @@ -107,7 +107,9 @@ public class UploadResource { // Auto-populate from IGDB (non-blocking if credentials are set) if (igdb.isConfigured()) { igdb.autoScrape(game); - if (game.isHasCover() || game.getYear() != null || game.getGenre() != null) { + if (game.isHasCover() || game.getYear() != null || game.getGenre() != null + || (game.getVideos() != null && !game.getVideos().isEmpty()) + || (game.getScreenshots() != null && !game.getScreenshots().isEmpty())) { svc.save(game); // re-save with enriched metadata } }