style
Build & Deploy / build-and-deploy (push) Successful in 39s

This commit is contained in:
david.alvarez
2026-06-09 14:03:37 +02:00
parent 397c99bd0c
commit c3a332d600
16 changed files with 82 additions and 75 deletions
@@ -7,10 +7,6 @@ import java.util.*;
/** /**
* Single source of truth for all DOSBox configuration generation. * Single source of truth for all DOSBox configuration generation.
* Eliminates the duplication that existed between GameService and UploadResource.
*
* Uses Java 21 text blocks for readable, maintainable config templates.
* Adding a new config parameter requires a change in exactly ONE place.
*/ */
@ApplicationScoped @ApplicationScoped
public class ConfigBuilder { public class ConfigBuilder {
@@ -20,8 +16,6 @@ public class ConfigBuilder {
".cue", 5, ".iso", 4, ".ccd", 3, ".img", 2, ".bin", 1 ".cue", 5, ".iso", 4, ".ccd", 3, ".img", 2, ".bin", 1
); );
// ─── Public API ───────────────────────────────────────────────
/** /**
* Build a dosbox.conf for a game with a known executable. * Build a dosbox.conf for a game with a known executable.
* @param relExe relative path to the executable inside the ZIP (e.g. "FALLOUT.EXE" or "FALLOUT/FALLOUT.EXE") * @param relExe relative path to the executable inside the ZIP (e.g. "FALLOUT.EXE" or "FALLOUT/FALLOUT.EXE")
@@ -83,9 +77,6 @@ public class ConfigBuilder {
public record ExeParts(String dir, String exe) {} public record ExeParts(String dir, String exe) {}
// ─── Config Templates ────────────────────────────────────────
// language=ini
private static final String DOSBOX_CONF_TEMPLATE = """ private static final String DOSBOX_CONF_TEMPLATE = """
[sdl] [sdl]
autolock=true autolock=true
@@ -121,7 +112,6 @@ public class ConfigBuilder {
${EXEC_PATH} ${EXEC_PATH}
"""; """;
// language=ini
private static final String CD_ONLY_DOSBOX_CONF_TEMPLATE = """ private static final String CD_ONLY_DOSBOX_CONF_TEMPLATE = """
[sdl] [sdl]
autolock=true autolock=true
@@ -159,8 +149,6 @@ public class ConfigBuilder {
echo Type D: and press Enter, then look for the game executable (e.g. DOTT.EXE). echo Type D: and press Enter, then look for the game executable (e.g. DOTT.EXE).
"""; """;
// ─── Internal Helpers ────────────────────────────────────────
/** /**
* Build the CD mount lines for the autoexec section. * Build the CD mount lines for the autoexec section.
* Mounts ALL CD images, deduplicating by parent directory. * Mounts ALL CD images, deduplicating by parent directory.
@@ -32,10 +32,10 @@ public class ConfigPatcher {
}) })
.forEach(f -> { .forEach(f -> {
try { try {
String text = new String(Files.readAllBytes(f), StandardCharsets.ISO_8859_1); String text = Files.readString(f, StandardCharsets.ISO_8859_1);
String patched = patchText(text, extractDir); String patched = patchText(text, extractDir);
if (!patched.equals(text)) { if (!patched.equals(text)) {
Files.write(f, patched.getBytes(StandardCharsets.ISO_8859_1)); Files.writeString(f, patched, StandardCharsets.ISO_8859_1);
LOG.info("Patched absolute paths in " + f.getFileName()); LOG.info("Patched absolute paths in " + f.getFileName());
} }
} catch (IOException e) { } catch (IOException e) {
@@ -4,10 +4,16 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
/** /**
* Detects game executables within extracted archives. * Detects game executables within extracted archives.
+2 -15
View File
@@ -17,37 +17,26 @@ public class Game {
private String publisher; private String publisher;
private String description; private String description;
private Double rating; private Double rating;
private String bundleFile;
private String bundleFile; // relative path within game dir private String setupExe;
private String setupExe; // relative path to SETUP.EXE (null if none)
/** Game bundle file size in bytes. Populated dynamically at load time (not persisted). */ /** Game bundle file size in bytes. Populated dynamically at load time (not persisted). */
private Long bundleSize; private Long bundleSize;
/** Currently selected main executable (relative path inside the bundle). */ /** Currently selected main executable (relative path inside the bundle). */
private String executable; private String executable;
/** All discoverable executables in the bundle (for the UI picker). */ /** All discoverable executables in the bundle (for the UI picker). */
private List<String> executables; private List<String> executables;
/** Platform type: "dos" for pure DOS, "windows" for Windows 3.x/9x+ */ /** Platform type: "dos" for pure DOS, "windows" for Windows 3.x/9x+ */
private String platform; private String platform;
private Integer igdbId; private Integer igdbId;
private String igdbCoverId; private String igdbCoverId;
private boolean hasCover; private boolean hasCover;
private boolean hasScreenshots; private boolean hasScreenshots;
private List<String> screenshots; private List<String> screenshots;
private List<String> videos; private List<String> videos;
private boolean ready; private boolean ready;
private Instant createdAt; private Instant createdAt;
private Instant updatedAt; private Instant updatedAt;
// ─── Constructors ────────────────────────────────────────────
public Game() {} public Game() {}
public Game(String id, String title) { public Game(String id, String title) {
@@ -58,8 +47,6 @@ public class Game {
this.updatedAt = Instant.now(); this.updatedAt = Instant.now();
} }
// ─── Getters & Setters ──────────────────────────────────────
public String getId() { return id; } public String getId() { return id; }
public void setId(String id) { this.id = id; } public void setId(String id) { this.id = id; }
@@ -1,7 +1,15 @@
package com.dostalgia; package com.dostalgia;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput; import jakarta.ws.rs.core.StreamingOutput;
@@ -27,7 +35,6 @@ public class GameResource {
try { try {
var all = svc.list(); var all = svc.list();
// Client-side filtering (small dataset, fine for a personal library)
if (search != null && !search.isBlank()) { if (search != null && !search.isBlank()) {
all = all.stream() all = all.stream()
.filter(g -> g.getTitle().toLowerCase().contains(search.toLowerCase())) .filter(g -> g.getTitle().toLowerCase().contains(search.toLowerCase()))
+6 -7
View File
@@ -12,7 +12,12 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.*; import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
@@ -53,8 +58,6 @@ public class GameService implements GameStore {
} }
} }
// ─── Game Directory Helpers ──────────────────────────────────
public Path gameDir(String id) { public Path gameDir(String id) {
return gamesDir.resolve(id); return gamesDir.resolve(id);
} }
@@ -63,8 +66,6 @@ public class GameService implements GameStore {
return gameDir(id).resolve("game.json"); return gameDir(id).resolve("game.json");
} }
// ─── CRUD ────────────────────────────────────────────────────
/** Load a game by ID. Throws if not found. */ /** Load a game by ID. Throws if not found. */
public Game load(String id) throws IOException { public Game load(String id) throws IOException {
Path path = gameJsonPath(id); Path path = gameJsonPath(id);
@@ -142,8 +143,6 @@ public class GameService implements GameStore {
return true; return true;
} }
// ─── Utility ─────────────────────────────────────────────────
/** Sanitize a string for use as a game directory ID. */ /** Sanitize a string for use as a game directory ID. */
public static String sanitizeId(String s) { public static String sanitizeId(String s) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -1,7 +1,13 @@
package com.dostalgia; package com.dostalgia;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.Map; import java.util.Map;
+6 -11
View File
@@ -10,11 +10,14 @@ import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
/** Client for the IGDB (Internet Game Database) API via Twitch OAuth2. */ /** Client for the IGDB (Internet Game Database) API via Twitch OAuth2. */
@@ -42,8 +45,6 @@ public class IgdbService {
private String accessToken; private String accessToken;
private Instant tokenExpiry; private Instant tokenExpiry;
// ─── Token Management ─────────────────────────────────────────
public boolean isConfigured() { public boolean isConfigured() {
return clientId.isPresent() && clientSecret.isPresent() return clientId.isPresent() && clientSecret.isPresent()
&& !clientId.get().isBlank() && !clientSecret.get().isBlank(); && !clientId.get().isBlank() && !clientSecret.get().isBlank();
@@ -84,8 +85,6 @@ public class IgdbService {
.header("Content-Type", "text/plain"); .header("Content-Type", "text/plain");
} }
// ─── Search ───────────────────────────────────────────────────
/** /**
* Search IGDB for games matching the given query. * Search IGDB for games matching the given query.
* Returns a list of simplified result maps suitable for JSON serialization. * Returns a list of simplified result maps suitable for JSON serialization.
@@ -264,8 +263,6 @@ public class IgdbService {
return screenshots; return screenshots;
} }
// ─── Auto-Scrape (called during upload) ──────────────────────
/** /**
* Auto-search IGDB for the game title and apply the best match. * Auto-search IGDB for the game title and apply the best match.
* Updates the Game object in-place. Does NOT save — caller must svc.save(). * Updates the Game object in-place. Does NOT save — caller must svc.save().
@@ -439,8 +436,6 @@ public class IgdbService {
} }
} }
// ─── Utility ──────────────────────────────────────────────────
private String sanitize(String s) { private String sanitize(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\""); return s.replace("\\", "\\\\").replace("\"", "\\\"");
} }
@@ -1,6 +1,5 @@
package com.dostalgia; package com.dostalgia;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
@@ -8,8 +7,10 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput; import jakarta.ws.rs.core.StreamingOutput;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.*; import java.io.IOException;
import java.nio.file.*; import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map; import java.util.Map;
/** Serves game bundles (.jsdos) and artwork from the external data directory. */ /** Serves game bundles (.jsdos) and artwork from the external data directory. */
@@ -10,10 +10,15 @@ import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.multipart.FileUpload;
import java.io.*; import java.io.IOException;
import java.nio.file.*; import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.*; import java.util.List;
import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
@jakarta.ws.rs.Path("/api/upload") @jakarta.ws.rs.Path("/api/upload")
@@ -46,8 +51,6 @@ public class UploadResource {
private static final Logger LOG = Logger.getLogger(UploadResource.class.getName()); private static final Logger LOG = Logger.getLogger(UploadResource.class.getName());
// ─── Upload ──────────────────────────────────────────────────
@POST @POST
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
public Response upload( public Response upload(
@@ -217,8 +220,6 @@ public class UploadResource {
return Response.ok(Map.of("status", "ok")).build(); return Response.ok(Map.of("status", "ok")).build();
} }
// ─── File Utilities ──────────────────────────────────────────
private void deleteDir(Path dir) throws IOException { private void deleteDir(Path dir) throws IOException {
if (!Files.exists(dir)) return; if (!Files.exists(dir)) return;
Files.walkFileTree(dir, new SimpleFileVisitor<>() { Files.walkFileTree(dir, new SimpleFileVisitor<>() {
+7 -2
View File
@@ -5,8 +5,13 @@ import jakarta.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.file.*; import java.nio.file.Files;
import java.util.*; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
@@ -1,6 +1,5 @@
package com.dostalgia; package com.dostalgia;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@@ -8,6 +7,10 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CdImageServiceTest { class CdImageServiceTest {
private final CdImageService service = new CdImageService(); private final CdImageService service = new CdImageService();
@@ -1,13 +1,13 @@
package com.dostalgia; package com.dostalgia;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNull;
class ConfigBuilderTest { class ConfigBuilderTest {
private final ConfigBuilder builder = new ConfigBuilder(); private final ConfigBuilder builder = new ConfigBuilder();
@@ -1,6 +1,5 @@
package com.dostalgia; package com.dostalgia;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@@ -8,6 +7,12 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConfigPatcherTest { class ConfigPatcherTest {
private final ConfigPatcher patcher = new ConfigPatcher(); private final ConfigPatcher patcher = new ConfigPatcher();
@@ -1,6 +1,5 @@
package com.dostalgia; package com.dostalgia;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@@ -8,6 +7,11 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExecutableDetectorTest { class ExecutableDetectorTest {
private final ExecutableDetector detector = new ExecutableDetector(); private final ExecutableDetector detector = new ExecutableDetector();
@@ -125,8 +129,6 @@ class ExecutableDetectorTest {
assertFalse(ExecutableDetector.isLikelySelfExtractor(f)); assertFalse(ExecutableDetector.isLikelySelfExtractor(f));
} }
// ─── helpers ─────────────────────────────────────────────────
private static byte[] createMZ(int size) { private static byte[] createMZ(int size) {
byte[] buf = new byte[size]; byte[] buf = new byte[size];
buf[0] = 0x4D; // M buf[0] = 0x4D; // M
@@ -1,12 +1,14 @@
package com.dostalgia; package com.dostalgia;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class PlatformDetectorTest { class PlatformDetectorTest {
private final PlatformDetector detector = new PlatformDetector(); private final PlatformDetector detector = new PlatformDetector();