- HTML 67.3%
- Kotlin 25.1%
- Svelte 6.3%
- TypeScript 1%
- Shell 0.1%
- Other 0.1%
|
All checks were successful
Build and Publish / build (push) Successful in 1m40s
|
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| frontend | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| initial-plan.md | ||
| README.md | ||
pwnfoil
Self-hosted game download manager. Scrape ROM sources, unrestrict hoster links via a debrid service, run resumable downloads with live progress, auto-unpack the archives into a clean on-disk layout, and optionally serve the result to Tinfoil as a private game shop.
- Backend: Kotlin + Spring Boot 3.4 (Maven, Java 21)
- Frontend: Svelte 5 (runes) + Vite + TypeScript
- Database: PostgreSQL 16 with Liquibase migrations
- Deploy: Docker Compose (db + FlareSolverr + backend + nginx-fronted SPA)
Features
- Import by URL. Paste a source page; the matching scraper pulls title, cover, description, and download links. Current scrapers:
nswgame.com(HTTP/2) andziperto.com(through FlareSolverr, handles multi-part release blocks and "Offline Crack" sections). - Link-shortener resolver. ouo.io links are walked automatically before unrestricting, so the debrid provider always sees a real hoster URL. New resolvers plug in via a registry.
- Debrid unrestricting. Real-Debrid and TorBox are both wired up end-to-end behind the
DebridProviderinterface. Each provider has its own hoster priority list in settings, and the download path auto-falls over to the next mirror if the first one refuses to unrestrict. - Release-grouped downloads. Links are grouped by release (base / update / DLC + version) and the UI collapses to one row per release, per hoster — enqueue a whole release in one click.
- Resumable downloads. HTTP/2
Rangeresume on.partfiles, retries with exponential backoff, zombie recovery on startup for any task left mid-transit by a crash. - Live progress via SSE. Per-task speed + ETA streamed to the UI, reconnecting
EventSourcewith a polling fallback. - Post-download extraction.
zip / 7z / rar (incl. RAR5, multi-part, solid) / tar / gz / bz2 / xz, all handled by a single7zCLI shell-out with per-task cancellation and optional archive cleanup. - Multi-part release handling. Parts that belong to the same release wait in
AWAITING_PARTSuntil every sibling is on disk; the last one to land runs extraction once for the whole set. - Organized on disk. Files land at
<download-dir>/<Platform>/<Game Title>/…using human-readable platform names (Switch,Game Boy Advance,Nintendo DS,Nintendo 64,SNES,Game Boy Color,PSP,PlayStation,PlayStation 2). - IGDB metadata enrichment. On import (or via the admin re-enrich endpoint), games are matched against IGDB and hydrated with summary, storyline, ratings, developers/publishers, age rating, genres, release date, and screenshots. Credentials live in settings; the Twitch OAuth token is cached in the DB so restarts don't burn a fresh round.
- Tinfoil / CyberFoil shop. The backend can expose the downloaded Switch library as a Tinfoil-compatible shop (
/shop) with referrer enforcement, Hauth pinning on first admin connect, MOTD, and an optional public mode. CyberFoil works too: in its legacy/Tinfoil mode via the same flat payload, and in its native mode via the richersectionsendpoint (/shop/api/shop/sections) with grouped tabs, cover art, and version metadata (needsPWNFOIL_PUBLIC_BASE_URL). A background scanner keeps the index in sync with what's actually on disk, with a configurable grace period before pruning missing files. - Stats dashboard. Per-platform counts, total library size, recent download throughput.
- Auth + multi-user. Spring Security session cookies + CSRF. Two roles:
ADMIN(full app) andSHOP(read-only shop access for sharing the Tinfoil endpoint with a trusted user). - Safety. Path traversal checks on the download target, the extractor, and the shop file server; strict entry sanitisation inside archives.
Quick start (dev)
cp .env.example .env
# fill in ADMIN_PASSWORD at minimum; REALDEBRID_API_KEY + IGDB creds are set in the UI
docker compose up -d db # postgres on host port 55432
cd backend && ./mvnw spring-boot:run
cd frontend && npm install && npm run dev
Backend: http://localhost:8080 — Frontend: http://localhost:5173
The dev postgres listens on 55432 to avoid conflicts with a host-local 5432. The prod compose file doesn't expose it at all.
Ziperto and any other Cloudflare-protected source needs FlareSolverr. Dev has no FlareSolverr by default — either
docker runthe image and setFLARESOLVERR_URL, or stick to nswgame.com.
Production
docker compose -f docker-compose.prod.yml up -d --build
Full stack: postgres + FlareSolverr + backend (with p7zip-full baked in for extraction) + nginx-served Svelte SPA with /api reverse-proxy and SSE-friendly buffering. Images are built and pushed to the private registry by .forgejo/workflows/bap.yaml on pushes to develop / main.
Required env: ADMIN_PASSWORD, POSTGRES_PASSWORD, PWNFOIL_PUBLIC_BASE_URL (your public origin — used for CORS + the shop's Hauth host check). REALDEBRID_API_KEY and the IGDB client id/secret can be set via the settings UI after first boot.
Status
v1 shipped and running in production. Post-v1 additions live in the history in initial-plan.md: additional scrapers (Ziperto), link-shortener resolvers, IGDB enrichment, Tinfoil shop, stats, multi-user auth, multi-part releases, TorBox provider. Still on the backlog: more scrapers, notification hooks, duplicate detection, search across sources without importing.