No description
  • HTML 67.3%
  • Kotlin 25.1%
  • Svelte 6.3%
  • TypeScript 1%
  • Shell 0.1%
  • Other 0.1%
Find a file
Midas van Oene 76e67ca62f
All checks were successful
Build and Publish / build (push) Successful in 1m40s
fix: Limit TorBox active downloads to 1 to avoid ACTIVE_LIMIT errors
2026-07-04 10:08:06 +02:00
.forgejo/workflows feat(packaging): add multi-stage Dockerfiles, prod compose and fix CI workflow 2026-04-10 11:15:13 +02:00
backend fix: Limit TorBox active downloads to 1 to avoid ACTIVE_LIMIT errors 2026-07-04 10:08:06 +02:00
frontend feat: Add UI toggle for Active Debrid Provider 2026-07-04 10:01:35 +02:00
scripts support the cyberfoil client alongside tinfoil 2026-06-19 19:33:00 +02:00
.env.example separate downloads from library and add import-from-disk scan 2026-04-12 20:31:28 +02:00
.gitignore chore: bootstrap repo with gitignore, env template, README 2026-04-10 10:28:27 +02:00
CLAUDE.md docs: mark v1 complete 2026-04-10 21:51:59 +02:00
docker-compose.prod.yml separate downloads from library and add import-from-disk scan 2026-04-12 20:31:28 +02:00
docker-compose.yml scraper: add flaresolverr client and compose service 2026-04-11 17:00:11 +02:00
initial-plan.md debrid: add torbox provider alongside real-debrid 2026-04-12 12:49:12 +02:00
README.md support the cyberfoil client alongside tinfoil 2026-06-19 19:33:00 +02:00

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) and ziperto.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 DebridProvider interface. 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 Range resume on .part files, 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 EventSource with a polling fallback.
  • Post-download extraction. zip / 7z / rar (incl. RAR5, multi-part, solid) / tar / gz / bz2 / xz, all handled by a single 7z CLI shell-out with per-task cancellation and optional archive cleanup.
  • Multi-part release handling. Parts that belong to the same release wait in AWAITING_PARTS until 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 richer sections endpoint (/shop/api/shop/sections) with grouped tabs, cover art, and version metadata (needs PWNFOIL_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) and SHOP (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 run the image and set FLARESOLVERR_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.