Skip to content

Repository files navigation

Zymphony

Self-hosted Docker service that turns the music ZIPs you get from @deezload2bot on Telegram — playlists, albums, compilations — into clean, correctly named and properly tagged albums, ready for Navidrome or any other music server.

Published on Docker Hub as jorgejiro/zymphony (linux/amd64 + linux/arm64).

Everything happens locally, on your own machine or NAS: Zymphony watches a folder, processes the ZIPs it finds there and writes the result into your music library. Your files are never uploaded anywhere. The only outbound traffic is a read-only lookup against the Spotify Web API to fetch the playlist's real name and cover art.

What it does for each download:

  1. Detects when all ZIP parts have finished downloading (size + stability heuristic).
  2. Fetches the playlist name and cover art from the Spotify API (with fallbacks for editorial/regional playlists — see Playlist metadata).
  3. Extracts all ZIPs, tags every MP3 (album, albumartist, tracknumber, compilation) and writes cover.jpg to the output folder.
  4. Renames the folder after the real playlist title (sanitized for the filesystem), moves the result to your music library and deletes the source ZIPs.
  5. Optionally sends a Discord notification on success or failure.

Where the ZIPs come from: @deezload2bot

Zymphony is the post-processing half of a two-step workflow. It does not download anything itself — the download is done by @deezload2bot, a Telegram bot that takes a Spotify (or Deezer) link and sends back the music as ZIP files.

The workflow end to end

  1. Send the link to the bot. In Telegram, open @deezload2bot and send it the share link of the Spotify playlist or album you want, e.g. https://open.spotify.com/playlist/7EAqBCOVkDZcbccjxZmgjp.

  2. The bot replies with one or more ZIP files. The archive is named after the Spotify ID of what you asked for. Large playlists are split into parts of roughly 1 GB each, numbered with a NN. prefix:

    7EAqBCOVkDZcbccjxZmgjp.zip           ← fits in a single archive
    00. 7EAqBCOVkDZcbccjxZmgjp.zip       ← first part of a split download
    01. 7EAqBCOVkDZcbccjxZmgjp.zip       ← …middle parts, ~1 GB each
    02. 7EAqBCOVkDZcbccjxZmgjp.zip       ← last part, smaller than the rest
    
  3. Save the ZIPs into the watched folder. Drop them into whatever directory you mapped to /input (e.g. /volume1/downloads on a Synology NAS). Point your Telegram client's download folder there, or copy the files over — either way works, Zymphony just looks at the directory.

  4. Zymphony takes it from here. On its next scan it groups the parts, waits until they are complete and stable, and produces a finished album in your music library.

Why post-processing is needed

The ZIPs the bot produces are raw dumps: the folder is named after an opaque 22-character Spotify ID, there is no cover art on disk, and each MP3 carries whatever album/track metadata came from its original release. Import that straight into Navidrome and a 60-track playlist explodes into 60 one-track albums with no artwork and meaningless names.

Zymphony fixes exactly that, without you touching a single file:

Problem in the raw ZIP What Zymphony does
Folder named 7EAqBCOVkDZcbccjxZmgjp Renames it to the real playlist title from Spotify, sanitized for the filesystem
Playlist split across several ZIPs Groups the parts by ID, waits for all of them and merges them into one album
No cover art Downloads the highest-resolution playlist cover, saves it as cover.jpg and embeds it in every MP3 (APIC)
Every track has its own album tag Rewrites album to the playlist name so the tracks group as one release
Tracks scatter as separate albums Sets compilation (TCMP=1) and albumartist (Various Artists, or the shared artist if all tracks have the same one)
Track numbers from the original releases Renumbers tracknumber sequentially 1..N, following part order (NN) and then filename order
ZIPs pile up in the downloads folder Deletes the source ZIPs once the album has been written successfully

The result is a single, coherent album entry in Navidrome, with artwork and the tracks in the right order.

Note on IDs: Zymphony identifies a download by the 22-character Spotify playlist ID in the filename (bare, with an NN. prefix, or as a full open.spotify.com/playlist/… URL or spotify:playlist:… URI). Files whose name does not contain such an ID are ignored and left untouched.


Prerequisites

  • Docker (Engine 24+ / Desktop 4.25+) with buildx if you build locally.
  • A Spotify Developer app — see Create a Spotify app.
  • A one-time bootstrap to obtain the OAuth refresh token.

Create a Spotify app

  1. Go to https://developer.spotify.com/dashboard and log in.
  2. Click Create app. Name and description can be anything.
  3. Under Redirect URIs, add http://localhost:8888/callback and save.
  4. Open the app settings and note the Client ID and Client Secret.

Quick start

# 1. Copy the example compose file and fill in your values
cp docker-compose.yml docker-compose.local.yml
#    Edit: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, volume paths, PUID/PGID

# 2. Run the one-time authorization (on a machine with a browser — see below)
docker compose -f docker-compose.local.yml run --rm \
  -p 8888:8888 zymphony zymphony auth

# 3. Start the service
docker compose -f docker-compose.local.yml up -d

One-time Spotify authorization

The service needs a refresh token stored in /config/spotify_token.json. This token is permanent — you only do this step once.

Option A — bootstrap on your Mac (recommended)

Run the auth command locally (not in Docker). The browser opens automatically and the token is written to the path you specify:

CONFIG_DIR=/path/to/config \
SPOTIFY_CLIENT_ID=your_id \
SPOTIFY_CLIENT_SECRET=your_secret \
SPOTIFY_REDIRECT_URI=http://localhost:8888/callback \
pip install zymphony && zymphony auth

Then copy spotify_token.json to the NAS config volume:

scp /path/to/config/spotify_token.json \
    user@NAS_IP:/volume1/docker/zymphony/config/

Option B — bootstrap in Docker on the NAS

Expose port 8888 temporarily, set SPOTIFY_REDIRECT_URI to the NAS IP, and register that URI in your Spotify app settings first:

# On the NAS via SSH:
docker run --rm -it \
  -p 8888:8888 \
  -v /volume1/docker/zymphony/config:/config \
  -e SPOTIFY_CLIENT_ID=your_id \
  -e SPOTIFY_CLIENT_SECRET=your_secret \
  -e SPOTIFY_REDIRECT_URI=http://NAS_IP:8888/callback \
  -e CONFIG_DIR=/config \
  jorgejiro/zymphony:latest zymphony auth

Open the printed URL in your browser, authorize, and the token is saved.


Environment variables

Variable Default Description
SPOTIFY_CLIENT_ID (required) Spotify app client ID
SPOTIFY_CLIENT_SECRET (required) Spotify app client secret
SPOTIFY_REDIRECT_URI http://localhost:8888/callback Must match your Spotify app settings
PUID 1000 UID that will own output files
PGID 1000 GID that will own output files
TZ UTC Container timezone
SCAN_INTERVAL_SECONDS 60 Seconds between directory scans
STABLE_MINUTES 5 Minutes a file must be unchanged before processing
PART_SIZE_THRESHOLD_MB 1000 Size threshold (MB) separating full parts from the final part
MIN_TOTAL_SIZE_MB 100 Minimum total group size before considering it
ALBUM_ARTIST Various Artists Value written to the albumartist tag, unless every track shares the same artist (then that artist is kept)
DELETE_ZIPS_AFTER true Delete source ZIPs after successful processing
LOG_LEVEL INFO Python logging level (DEBUG, INFO, WARNING, ERROR)
INPUT_DIR /input Directory to watch for ZIP files
OUTPUT_DIR /output Navidrome music library root
CONFIG_DIR /config Persistent config directory (Spotify token)
DISCORD_WEBHOOK_URL (empty) Discord webhook for success/failure notifications; leave empty to disable

Discord notifications (optional)

Get a notification in Discord every time a compilation is added or a group fails to process.

  1. In your Discord server, open Server Settings → Integrations → Webhooks.
  2. Click New Webhook, pick the target channel, and Copy Webhook URL.
  3. Set DISCORD_WEBHOOK_URL to that URL in your docker-compose.yml (or -e flag) and restart the container.

Leave the variable empty to disable notifications entirely. Delivery is best-effort: if Discord is unreachable the failure is logged and processing continues normally.


Playlist metadata & editorial playlists

Zymphony fetches the playlist name and cover separately so that Spotify's editorial and regional playlists (e.g. "Los 90 España") work even though their authenticated Web API endpoint returns 404:

  • Name — the authenticated Web API is tried first (this is what makes private playlists work). If it returns 404, Zymphony falls back to Spotify's public oEmbed endpoint. If both fail, the playlist ID is used as the album name so processing never blocks.
  • Cover — always fetched from the dedicated /images endpoint, which works for editorial playlists even when the main endpoint rejects the request. If no cover is available, the tracks are still tagged and moved (no cover.jpg).

Re-downloading a playlist

If you ask @deezload2bot for the same playlist again and drop the new ZIPs into /input, Zymphony replaces the existing output folder with the freshly processed content instead of skipping it. This lets you refresh a compilation after adding tracks to the Spotify playlist — just request it from the bot again and drop the new ZIPs in.


Synology setup

1. Find your PUID and PGID

SSH into your NAS and run:

id your_username
# uid=1026(your_username) gid=100(users) ...

Use uid as PUID and gid as PGID. These must match the user that owns your Navidrome music library, so that Zymphony can write files and Navidrome can read them.

2. Create the required folders

# Folder where you save the ZIPs sent by @deezload2bot
mkdir -p /volume1/downloads

# Navidrome library root (skip if it already exists)
mkdir -p /volume1/music

# Persistent config: Spotify token + processing state
mkdir -p /volume1/docker/zymphony/config

Suggested folder structure:

/volume1/
├── downloads/              → /input   (ZIPs from @deezload2bot)
├── music/                  → /output  (Navidrome library root)
└── docker/
    └── zymphony/
        └── config/         → /config  (Spotify token + state)

3. Run the one-time Spotify authorization

Before starting the service, generate the refresh token once. The easiest approach is to do this from your Mac (Option A above) and then copy the resulting spotify_token.json to the NAS:

scp spotify_token.json user@NAS_IP:/volume1/docker/zymphony/config/

4a. Deploy via Container Manager (DSM 7.2+)

  1. Open Container ManagerProjectCreate.

  2. Set a project name (e.g. zymphony).

  3. Under Source, choose Create docker-compose.yml and paste the contents of the docker-compose.yml from this repo — or import the file directly.

  4. Edit the three volume paths under volumes: to match step 2:

    volumes:
      - /volume1/downloads:/input
      - /volume1/music:/output
      - /volume1/docker/zymphony/config:/config
  5. Fill in SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, PUID, and PGID.

  6. Click Next → Done. Container Manager starts the service automatically.

4b. Deploy via SSH / CLI

# SSH into the NAS
ssh user@NAS_IP

mkdir -p /volume1/docker/zymphony
cd /volume1/docker/zymphony

# Upload docker-compose.yml and edit it, then:
docker compose up -d

Check logs

docker compose logs -f zymphony

A healthy start-up looks like:

zymphony  | [INFO] Starting Zymphony service
zymphony  | [INFO] Watching /input every 60s

Update the container

docker compose pull
docker compose up -d

Image tags

The image is published to Docker Hub as jorgejiro/zymphony as a multi-arch manifest (linux/amd64 + linux/arm64):

  • jorgejiro/zymphony:latest — always the newest release.
  • jorgejiro/zymphony:<version> — pin a specific release (e.g. 0.2.1).

Pinning a version is recommended for a NAS deployment so an update is a deliberate change. To upgrade, bump the tag (or docker compose pull on latest) and recreate the container.


Building the image

Local build (single arch)

docker build -t zymphony:latest .

Multi-arch build and push (amd64 + arm64)

# One-time setup of a multi-arch builder
docker buildx create --use --name multiarch --driver docker-container

# Build and push both architectures, tagging the version and `latest` together.
# Keep the version in sync with `version` in pyproject.toml.
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t jorgejiro/zymphony:0.2.1 \
  -t jorgejiro/zymphony:latest \
  --push \
  .

Troubleshooting

The service processes no files after dropping ZIPs into /input. Check LOG_LEVEL=DEBUG for detail. Common causes:

  • The ZIPs were not yet stable for STABLE_MINUTES — wait or lower the value.
  • The total size is below MIN_TOTAL_SIZE_MB — lower the threshold or verify the ZIPs are correct.
  • The filename does not contain a 22-character Spotify playlist ID. Keep the names exactly as @deezload2bot sends them (00. PLAYLISTID.zip); renaming them, or letting Telegram append a (1) suffix to a duplicate download, makes the file unrecognizable.

SpotifyException: No token / auth errors. The refresh token is missing or expired. Re-run zymphony auth and copy the new spotify_token.json to /config.

Output files are owned by root instead of my user. PUID/PGID are probably not set. Check the environment section of your compose file and verify the values with id your_username on the NAS.

MP3s appear in Navidrome as individual albums instead of one compilation. The compilation (TCMP=1) and albumartist tags were not written correctly. Enable LOG_LEVEL=DEBUG, check for tagging errors, and verify the file is a valid ID3v2.3/2.4 MP3.

A playlist fails repeatedly and I want to retry from scratch. The ZIPs are not deleted on failure — just restart the service or wait for the next scan cycle. No manual cleanup is needed.

Discord notifications don't arrive. Check that DISCORD_WEBHOOK_URL is set and correct. Delivery is best-effort, so a bad URL or an unreachable Discord only produces a WARNING in the logs and never blocks processing — enable LOG_LEVEL=DEBUG and look for Failed to send Discord notification.


Development

# Install in editable mode with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Lint and format
ruff check src tests
black src tests

Running the auth bootstrap locally

export SPOTIFY_CLIENT_ID=your_id
export SPOTIFY_CLIENT_SECRET=your_secret
export SPOTIFY_REDIRECT_URI=http://localhost:8888/callback
export CONFIG_DIR=./local-config
mkdir -p ./local-config
zymphony auth

License

Released under the MIT License.

About

Procesa el ZIP de música descargada vía deezload para etiquetar correctamente los archivos y bajar la carátula

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages