@emgeebee/airfreyr
v1.0.23
Published
Music downloader with an HTTP queue server for remote track requests
Maintainers
Readme
@emgeebee/airfreyr
Download music from YouTube. Fork of freyr-js with an HTTP queue server for remote track requests.
Requirements
- Node.js >= 16
- Python >= 3.2 (for
youtube-dl-exec) - AtomicParsley on your
PATH
Installation
npm install -g @emgeebee/airfreyr
# or
npx @emgeebee/airfreyr <command>From source:
git clone https://github.com/emgeebee/air-freyr.git && cd air-freyr
npm install && npm linkQueue server
Run an HTTP server that appends tracks to queue files and triggers downloads automatically.
airfreyr serveConfiguration
Precedence: CLI flags → environment variables → conf.json → defaults.
| Flag | Env | conf.json | Purpose |
| --- | --- | --- | --- |
| -q, --queue-dir <DIR> | AIRFREYR_QUEUE_DIR | serve.queueDir | Queue .json files directory |
| -D, --output-dir <DIR> | AIRFREYR_OUTPUT_DIR | dirs.output | Download output directory |
| -p, --port <PORT> | AIRFREYR_PORT | serve.port | Listen port (default: 3797) |
| -H, --hostname <HOST> | AIRFREYR_HOSTNAME | serve.hostname | Bind address |
"serve": {
"hostname": "localhost",
"port": 3797,
"queueDir": "."
},
"dirs": {
"output": "./0"
}Downloads use dirs.output unless overridden by -D or AIRFREYR_OUTPUT_DIR.
AIRFREYR_QUEUE_DIR=./queues AIRFREYR_OUTPUT_DIR=./music airfreyr serveAPI
All responses include "version" (the running @emgeebee/airfreyr package version).
POST /add — append a track and start downloading the queue file.
{
"file": "kids.json",
"genre": "Kids",
"artist": "Moana",
"title": "You're Welcome",
"path": "https://www.youtube.com/watch?v=G8QjumNNNBY"
}titleis optionalpathcan also be sent asurl- Existing tracks in the file are skipped; only new lines are downloaded
- If a download is already running for that file, another run is queued when it finishes
GET /status?file=kids.json — check whether a download is in progress (includes lastError if the last run failed)
curl 'http://<nas-ip>:3797/status?file=kids.json'Example when a download failed:
{
"version": "1.0.1",
"ok": true,
"file": "kids.json",
"download": {
"running": false,
"pending": false,
"lastError": "airfreyr exited with code 2",
"lastExitCode": 2,
"lastStartedAt": "2026-06-22T20:15:00.000Z",
"lastFinishedAt": "2026-06-22T20:15:42.000Z"
}
}If lastError is set, check container logs for detail: sudo docker logs --tail 100 airfreyr
GET /health — server status and configured directories
curl -X POST http://localhost:3797/add \
-H 'Content-Type: application/json' \
-d '{"file":"kids.json","genre":"Kids","artist":"Moana","title":"You'\''re Welcome","path":"https://www.youtube.com/watch?v=G8QjumNNNBY"}'Batch downloads
Download tracks listed in a queue file:
airfreyr -i kids.json
airfreyr -i kids.json -d ./music # custom output directoryQueue file format
Queue files are JSON with an entries array. Each entry has artist, title, url, and optional disabled, note, and genre (when different from the list name).
{
"entries": [
{
"artist": "Moana",
"title": "You're Welcome",
"url": "https://www.youtube.com/watch?v=G8QjumNNNBY"
},
{
"artist": "Peppa Pig",
"title": "Jumping in Muddy Puddles",
"url": "https://www.youtube.com/watch?v=t7dTdE8Aqtw",
"disabled": true,
"note": "already have this"
}
]
}Genre defaults from the filename: kids.json → Kids, folk rock.json → Folk Rock.
The queue UI and API still accept CSV lines for bulk paste (artist,title,url). Legacy .txt CSV files are migrated to .json automatically when the server starts.
Files are organised as <output>/<genre>/youtube/<artist> - <title>.<format>.
CLI
airfreyr <url-or-uri> # download a single track
airfreyr -i queues/kids.json # batch download from file
airfreyr serve # start the queue server
airfreyr urify <url> # convert URLs to service URIs
airfreyr --help # full optionsCommon flags:
| Flag | Purpose |
| --- | --- |
| -d, --directory <DIR> | Output directory |
| -f, --force | Overwrite existing files |
| -b, --bitrate <N> | Audio bitrate (default: 320k) |
| -x, --format <FORMAT> | Output format (default: mp3) |
| --no-logo / --no-header | Quieter output |
Configuration
On first run, airfreyr creates a user config file with service credentials and defaults:
- Linux:
~/.config/AirFreyr/d3fault.x4p - macOS:
~/Library/Preferences/AirFreyr/d3fault.x4p
Project defaults live in conf.json. Use -o, --config <FILE> to point at an alternative.
Docker (queue server)
A minimal node:20-alpine image runs npx @emgeebee/airfreyr@latest serve and restarts every 3 hours to pull the latest npm publish.
Quick start (build once)
mkdir -p docker/queues docker/music docker/config
cp docker/conf.json.example docker/config/conf.json
cd docker
docker compose up --buildNo build — plain node:alpine
If you only want the stock Node image plus a mounted entrypoint script:
cd docker
docker compose -f compose.alpine.yml up -dThis uses node:20-alpine directly. Python is installed on first start (apk add python3 bash), then the entrypoint runs npx.
Queue files go in docker/queues/ (e.g. kids.json). Downloads land in docker/music/.
docker run (without compose)
docker build -f docker/Dockerfile -t airfreyr-serve .
docker run -d --name airfreyr \
--restart unless-stopped \
-p 3797:3797 \
-e AIRFREYR_HOSTNAME=0.0.0.0 \
-e AIRFREYR_REFRESH_HOURS=3 \
-v "$PWD/queues:/data/queues" \
-v "$PWD/music:/data/music" \
-v "$PWD/config:/data/config" \
airfreyr-servePut conf.json at config/conf.json inside the mounted config volume.
Synology Container Manager
Use docker/synology-compose.yml and docker/conf.json.example.
1. Create folders on the NAS
In File Station, create:
/volume1/docker/airfreyr/queues ← queue .json files (kids.json, pop.json, …)
/volume1/docker/airfreyr/music ← downloaded tracks
/volume1/docker/airfreyr/config ← conf.jsonChange volume1 if your shared folder lives on a different volume.
2. Copy config and compose onto the NAS
| NAS path | Source in repo |
| --- | --- |
| /volume1/docker/airfreyr/config/conf.json | docker/conf.json.example |
| /volume1/docker/airfreyr/docker-compose.yml | docker/synology-compose.yml |
Edit paths in docker-compose.yml if not using volume1. Put queue files in queues/.
3. Create the container project
Container Manager → Project → Create
- Project name:
airfreyr - Path:
/volume1/docker/airfreyr/docker-compose.yml - Start the project (pulls
node:20-alpine, no build step)
Directory mappings
| NAS folder | Container path | Purpose |
| --- | --- | --- |
| /volume1/docker/airfreyr/queues | /data/queues | Queue .json files |
| /volume1/docker/airfreyr/music | /data/music | Downloaded music |
| /volume1/docker/airfreyr/config | /data/config | conf.json (+ saved auth after first run) |
Port
Map host port 3797 → container 3797. Then from your LAN:
curl http://<nas-ip>:3797/healthManual container (UI fields)
If you prefer Container → Create instead of a Project:
| Setting | Value |
| --- | --- |
| Image | node:20-alpine |
| Command | apk add --no-cache python3 bash && exec /entrypoint.sh |
| Entrypoint | mount serve-entrypoint.sh → /entrypoint.sh |
| Port | 3797:3797 |
| AIRFREYR_HOSTNAME | 0.0.0.0 |
| AIRFREYR_QUEUE_DIR | /data/queues |
| AIRFREYR_OUTPUT_DIR | /data/music |
| AIRFREYR_CONFIG | /data/config/conf.json |
| Volume | /volume1/docker/airfreyr/queues → /data/queues |
| Volume | /volume1/docker/airfreyr/music → /data/music |
| Volume | /volume1/docker/airfreyr/config → /data/config |
| Restart policy | Unless stopped |
The container runs npx @emgeebee/airfreyr@latest serve and refreshes every 3 hours.
"Build project failed" (no useful logs)
Synology often shows only Build project '…' failed with no detail. The real error is elsewhere.
1. Check the folders exist first (most common cause):
ls -la /volume1/docker/airfreyr/queues
ls -la /volume1/docker/airfreyr/music
ls -la /volume1/docker/airfreyr/config/conf.jsonSynology will fail if any mapped folder or conf.json is missing.
2. Get the real error via SSH (Control Panel → Terminal & SNMP → Enable SSH):
cd /volume1/docker/airfreyr
sudo docker compose config # validate compose syntax
sudo docker compose up # run in foreground — errors print here
# or after a failed project start:
sudo docker compose logs --tail 50
sudo docker logs airfreyr 2>&13. In Container Manager UI
- Container → select
airfreyr→ Details → Log tab (runtime logs, after container starts) - Project → your project → Action → delete and recreate after fixing paths
- Log Center → search for
docker/containeraround the failure time
4. Common fixes
| Problem | Fix |
| --- | --- |
| Volume path wrong | Use absolute paths; edit volume1 in docker-compose.yml |
| conf.json missing | Copy docker/conf.json.example to config/conf.json |
| Old project stuck | Delete project + container, recreate |
| Still using build: | Use docker/synology-compose.yml — it has no build step |
| Port in use | Change 3797:3797 to e.g. 3798:3797 |
Environment
| Variable | Default | Purpose |
| --- | --- | --- |
| AIRFREYR_HOSTNAME | 0.0.0.0 | Bind address (use 0.0.0.0 in Docker) |
| AIRFREYR_PORT | 3797 | HTTP port |
| AIRFREYR_QUEUE_DIR | /data/queues | Queue .json directory |
| AIRFREYR_OUTPUT_DIR | /data/music | Download output directory |
| AIRFREYR_CONFIG | /data/config/conf.json | Config file for download runs |
| AIRFREYR_REFRESH_HOURS | 3 | Restart interval to pull latest from npm |
| AIRFREYR_REFRESH_SECONDS | — | Override refresh interval in seconds |
On each restart the entrypoint clears the npx cache and runs npx --yes @emgeebee/airfreyr@latest serve.
Publishing to npm
Pushes to main or master run .github/workflows/publish.yml, matching the simple pipeline in phone_cli:
npm cinpm publish --access public
Uses npm trusted publishing (OIDC, no NPM_TOKEN secret). On npmjs.com, link the GitHub repo to the @emgeebee scope before the first publish.
You can also trigger manually: Actions → publish → Run workflow.
Test the API
curl http://localhost:3797/health
curl -X POST http://localhost:3797/add \
-H 'Content-Type: application/json' \
-d '{"file":"kids.json","genre":"Kids","artist":"Moana","title":"You'\''re Welcome","path":"https://www.youtube.com/watch?v=G8QjumNNNBY"}'License
Apache-2.0. Based on freyr-js by Miraculous Owonubi.
