โ How it works ๐ผ Screenshots
01 Overview
A self-hosted service for collecting game-development assets. You type one query; it searches roughly twenty pluggable source connectors (18 of them surfaced in the UI) at once, and the results you pick flow through a shared pipeline โ download โ de-duplicate โ file โ thumbnail โ AI-tag โ into a searchable library with per-project workspaces. It was the "find real, free assets" counterpart to a sibling generation tool, and it fed the AI game-building pipeline as an asset backend.
Stock & commons
Pexels, Unsplash, Pixabay, OpenVerse, Wikimedia Commons, NASA, Internet Archive.
Sound & tracks
Freesound, ccMixter, Incompetech, and CC / royalty-free YouTube audio via yt-dlp.
Models & textures
Poly Haven, Sketchfab, Quaternius, Kenney.nl asset packs.
Art bits
Game-Icons, OpenGameArt, itch.io feeds, plus a paste-a-URL page scraper.
02 Why I built it
The AI game-building pipeline needed art, sound and music, and I wanted it to reach for real, licensed free assets first and only fall back to generated ones โ so the game platform gathers from Harvester first, and from the generation tool second. That made two things matter: breadth of good sources, and license discipline. Every result carries its license and source URL, so nothing gets used unattributed. And because it ran on a small home server, the whole thing is built to be gentle on memory, CPU and network rather than fast.
03 What I built & how it works
One federated search, then a single serial pipeline every asset passes through.
Fig. 1 โ a search fans out across the connectors; each chosen result runs the same download-to-tag pipeline, one at a time.
- Search โ one query fans out across the enabled connectors in parallel; per-source errors are surfaced without failing the whole search.
- Queue โ the results you pick enter a serial, SQLite-persisted download queue that rebuilds itself after a restart (stuck downloads reset to pending).
- De-duplicate โ each file is streamed and SHA-256 hashed; a duplicate completes instantly and points at the asset already on disk.
- File & thumbnail โ the asset is filed into a date-bucketed, typed layout and given a 256px thumbnail (image resize, audio waveform, video frame, or a typed placeholder).
- Tag โ in the background, a vision model writes a description and tags, stored separately from any tags you add by hand.
04 ๐ Skills & tech used
05 Notable challenges & decisions
The interesting part wasn't any one connector โ it was making a pile of mismatched, sometimes-flaky sources behave like one search box.
Five different ways to fetch a file
No two sources agree on how to hand over an asset, so the connectors use five strategies behind one contract: REST APIs, HTML scraping (Kenney, OpenGameArt, Incompetech), GitHub catalog mining (Game-Icons, Quaternius), RSS feeds (itch.io), and subprocess extraction (YouTube via yt-dlp). Each normalizes into the same result shape, with per-source TTL caches so a single popular query doesn't hammer a source.
One flaky source shouldn't sink the search
Federated search runs the connectors under Promise.allSettled with a capability pre-filter (skip sources that can't serve the requested type) and a strict post-filter on the way back. A scraper timing out or a rate-limited API surfaces as a per-source note, not a failed query.
Coverage isn't the goal; usable results are
A generic web-search source looked like free breadth, but it returned Pinterest / Etsy / Reddit noise rather than downloadable assets, so I delisted it and replaced it with an images-only source. This is also why the honest source count is ~20 connectors and 18 user-facing โ not a bigger, padded number.
A scraping stack that never shipped
The plan was a self-hosted Firecrawl stack for the paste-a-URL scraper, but its official image was access-restricted. Rather than trust an unofficial mirror, I rewrote the same behavior in-process with axios and regex extraction and deleted three now-unneeded compose services.
Built for a small box, and honest about it. The download queue runs one item at a time on purpose โ a deliberate constraint to protect a ~15 GB home server's memory, CPU and network. Filing across a container filesystem and a USB mount also meant handling cross-device EXDEV by copying rather than renaming โ the kind of small thing that only shows up once real files move.
06 Results
Sources: the project's source files and the SQLite database, read directly. Counts describe what was built, not scale of use โ see the honest status below.
07 Screenshots
Captured 2026-08-02 from the verified runnable clone (Steps to Build tab) โ the original service is torn down, but the clone reproduces these screens.


08 Honest status
Asset Harvester was built and iterated over a short sprint in early March 2026 โ sources grew 9 โ 14 โ 18 in a single day โ and then it settled into a quiet role as an asset backend for the game pipeline. It ran self-hosted, LAN-only, with no authentication. It was never exercised at scale: the database records only 13 successful downloads and 14 failures, so several source paths (the scrapers and NASA's manifest fetch in particular) were never proven in real use. Its container has since been removed; the code and data still persist on the server. One correction worth stating plainly: earlier labels called it a "22-source" tool โ the defensible number is 20 connectors. Update (2026-08-02): building the runnable clone (Steps to Build tab) surfaced seven real bugs โ including the two that explain why the original's own database mis-typed every download โ plus two connectors broken by upstream API changes; all were fixed in the clone and backported to the original's source. The screenshots above are from that verified clone.
asset-harvester โ Build Recipe
Take a bare machine to a running game-asset search & organization hub: one web app that searches 20 sources (Openverse, Wikimedia Commons, Internet Archive, NASA, Poly Haven, Kenney, Game-icons.net, Quaternius, OpenGameArt, ccMixter, itch.io, YouTube audio, and more) in one query, downloads assets through a persistent queue with live progress, deduplicates by SHA-256, auto-thumbnails everything (images, audio waveforms, video frames), and organizes results into per-project workspaces. Key-free by default โ 13 sources need no API key, and a small demo library is imported on first boot.
Status: โ Verified 2026-08-02 โ built from this clone on a Linux Docker host (
docker compose up -d --build). First boot imported the bundled demo library (9 assets, 15 MB โ images, SVG sprites, audio, a texture โ with thumbnails and a "Demo Game" project); an all-source key-free search returned 137 results from 7 live sources with zero errors; 9 of 10 real downloads completed end-to-end through the queue (hash โ dedup โ organized storage โ thumbnail; the one failure was a transient Wikimedia rate-limit); a restart keeps the data without re-seeding; a real-browser pass showed the gallery, sources, and project pages working with 0 console errors. All three tiers covered; the prebuilt tarball (docker save asset-harvester-clone) is ~885 MB. The clean clone lives inclone/.Sensitive data: none. The source held no keys (all env-based, and the live app's stored key fields were empty); the demo seed was harvested from public CC0 / CC-BY / public-domain sources with license + author recorded on every row; personal server references were scrubbed. The 7 bug fixes below were also backported to the original live app.
What it is
A single Node/Express + Socket.IO container:
| Piece | Role |
|---|---|
server.js + queue.js |
REST API, live Socket.IO progress, persistent one-at-a-time download queue |
sources/ (20 modules) |
one adapter per asset source โ API, RSS, or scrape; each degrades gracefully |
processors/ |
SHA-256 dedup hashing, sharp/ffmpeg thumbnails, opt-in AI tagging |
db.js |
SQLite (better-sqlite3, WAL) โ assets, downloads, projects, source configs |
public/ |
vanilla-JS SPA: search, downloads, gallery, projects, sources, stats โ no CDNs |
System deps baked into the image: ffmpeg (waveform/frame thumbnails), yt-dlp (YouTube audio source). No GPU, no build step for the frontend.
Prerequisites
New machine? See ../SETUP.md. Then, per tier:
| Tier | You need |
|---|---|
| 1 โ prebuilt container (recommended) | Docker (see ../SETUP.md) + this bundle's clone/ folder (for the compose file). |
| 2 โ build from source | Same as Tier 1. |
| 3 โ bare-metal | Node 20+ and ffmpeg (install commands below); optional yt-dlp for the YouTube source. |
Configuration โ nothing required
Runs key-free: 13 of 20 sources work with no key, and a bundled demo
library populates the gallery on first boot. Optional extras (each unlocks
more, all documented in clone/.env.example): six per-source API keys, AI
auto-tagging via any OpenAI-compatible endpoint, and SearXNG web-image search.
Tier 1 โ Run the prebuilt container (recommended)
Availability: the prebuilt image is available on request โ it is not published or linked anywhere. Ask Kevin for it, or build from source via Tier 2.
docker load -i asset-harvester-clone-image.tar
cd clone
docker compose up -d # โ http://localhost:8214
Tier 2 โ Build from source
cd clone
docker compose up -d --build # โ http://localhost:8214
curl -s localhost:8214/health # โ {"status":"ok", ...}
Open http://localhost:8214 โ the gallery starts populated (demo seed), and a search like castle or nebula across "All sources" returns live results from the keyless sources.
Tier 3 โ Bare-metal (one Node process)
Install the two prerequisites first (../SETUP.md covers Docker/Python but not these):
# Node 20+ and ffmpeg โ
# Windows: winget install OpenJS.NodeJS.LTS Gyan.FFmpeg
# Debian/Ubuntu: sudo apt install -y nodejs npm ffmpeg (Node 20+ via nodesource if the distro's is older)
# macOS: brew install node ffmpeg
# optional, for the YouTube source: pip install yt-dlp
cd clone
npm ci --omit=dev
# ffmpeg must be on PATH (thumbnails); yt-dlp optional (YouTube source)
DATA_DIR=./data ASSETS_DIR=./assets THUMBNAILS_DIR=./thumbnails TEMP_DIR=./temp \
SEED_DIR=./seed node server.js # โ http://localhost:8214
On Windows, set the same variables with set/$env: before node server.js.
Optional extras (all off by default)
- Per-source API keys โ Pixabay, Pexels, Unsplash, Freesound, Sketchfab,
itch.io. Free-tier signup links are in
clone/.env.example; paste a key on the Sources page (or in.env) and toggle the source on. - AI auto-tagging โ point
LITELLM_URLat any OpenAI-compatible endpoint and every downloaded asset gets an AI description + tags. Local Ollama works key-free (ollama pull llama3.2; a vision model improves image tagging):
bash
# in clone/.env
LITELLM_URL=http://host.docker.internal:11434/v1
LITELLM_API_KEY=ollama
LITELLM_MODEL=llama3.2
- Web-image meta-search โ set
SEARXNG_URLto a SearXNG instance (JSON API enabled) and enable the "Web Images (SearXNG)" source.
Provenance โ the demo seed
The bundled first-boot library was harvested through this app itself from its keyless sources (Wikimedia Commons, NASA, Openverse, Game-icons.net, Poly Haven, Kenney, and similar) โ public-domain and Creative-Commons items only. Every seed asset's source, license, author, and origin URL are recorded in its database row and visible in the asset detail view. No model weights, no datasets.
What was stripped from the personal version (de-identification)
- Home-server User-Agent strings (they identified the author's server) โ
generic, non-identifying
AssetHarvester/1.0UAs throughout (the YouTube source keeps a stock browser UA for rate-limit reasons, as the original did). - Private LLM-gateway defaults โ the original defaulted to an internal
LiteLLM endpoint and a private model alias for auto-tagging. Tagging is now
opt-in (
LITELLM_URLunset = off) against any OpenAI-compatible endpoint. - Internal SearXNG dependency โ the web-image source assumed a search
container on the home network; now opt-in via
SEARXNG_URL, off by default. - A TLS-verification bypass for one source (ccMixter's occasionally incomplete cert chain) was removed โ the clone verifies TLS normally and that source simply degrades to empty if the remote chain is broken.
- An orphaned dead-code module (
searxng.js, superseded bywebimages.js) was deleted; personal hardware references in comments were generalized.
Bugs found and fixed during cloning (faithful, minimal fixes)
- Every downloaded asset got type
other/ formattmpโ two compounding bugs, visible in the original's own database. The UI sendsassetTypebut the server readasset_type, so the download modal's type selection was silently dropped; and format detection read the extension of the queue's<id>.tmptemp file (always "tmp") instead of the source metadata. Types, formats, thumbnails, and download filenames all work now. - 12 sources couldn't be configured โ the DB seeded only 9
source_configsrows but 20 sources are registered; the settings API 404s without a row, so most sources could never be toggled or keyed through the UI. The seed now covers every registered source. - Two keyed sources had no key input โ the UI's needs-a-key list omitted Pexels and Unsplash, so their keys couldn't be entered at all.
- Saving settings could wipe a stored key โ the server masks keys in API responses, but clicking Save sent the mask string back and overwrote the real key. The server now ignores the masked echo.
- Poly Haven returned nothing under a type filter โ its API reports asset types as numeric codes, which the adapter compared against strings; results were mislabeled and filtered out (and download URLs malformed). The adapter now tracks which list each entry came from.
- Game-icons.net returned nothing โ the upstream GitHub repo moved to a
flat
author/icon.svglayout; the adapter only matched the oldauthor/originals/svg/paths. It now accepts both. - yt-dlp pointed at a hardcoded Node path (
/usr/local/bin/node) that only exists in the original image; it now uses the running Node binary (process.execPath), so bare-metal installs work too.
Known limitations (stated honestly)
Personal-LAN tool: no authentication โ put it behind a reverse proxy if exposed. Downloads run one at a time by design (original ran on a small shared server). The scraping-based sources are best-effort: Incompetech's modern site is JS-rendered so that source usually returns nothing; Kenney falls back to a fixed pack list if its page layout changes; OpenGameArt sits behind intermittent anti-bot protection. Freesound downloads are the HQ MP3 previews (originals require OAuth2); Sketchfab is search/link-out only. YouTube search and download are subject to rate limits and yt-dlp aging โ a rebuilt image months from now may need a yt-dlp bump. The container runs as root (as the original did); hardening is left to the deployer.