Trackhop

Trackhop on a server (Docker, beta)

Beta. The server works for everyday use (it has run scheduled syncs for days) but is still young: pages and settings can change between releases. Keep a copy of the data volume (or save the set-up from Tools › Backup & restore) and report what goes wrong. The unofficial connectors below stay an alpha.

The same sync engine as the Android app, running on a home server, a NAS or a small VPS, with a web page to connect services, create syncs and see how they went. Scheduled syncs run there, whether or not your phone is on.

Start it

docker run -d --name trackhop \
  -p 8080:8080 \
  -v trackhop-data:/data \
  -e SONGPORT_URL=http://127.0.0.1:8080 \
  -e SONGPORT_PASSWORD=choose-one \
  ghcr.io/xlollx/trackhop:latest

Then open http://127.0.0.1:8080 (any user name, the password you chose).

With Docker Compose:

services:
  trackhop:
    image: ghcr.io/xlollx/trackhop:latest
    container_name: trackhop
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./trackhop-data:/data
    environment:
      SONGPORT_URL: http://127.0.0.1:8080
      SONGPORT_PASSWORD: choose-one
      # SONGPORT_LOCALE: it

The same image is also on Docker Hub as xlollx/trackhop (docker pull xlollx/trackhop, the same latest and version tags), for tools and NAS app stores that only search there. Either name works; the commands in this guide use ghcr.io/xlollx/trackhop.

The image runs on amd64 and arm64 (Raspberry Pi 4/5, most NAS). To build it yourself, from the repository root: docker build -f server/Dockerfile -t trackhop .

Coming from Songport? Trackhop was called Songport up to version 1.1. The old image name, ghcr.io/xlollx/songport, still works and gets every release with the same tags as ghcr.io/xlollx/trackhop, so an existing setup keeps updating as it is. To switch, change only the image name: keep your volume (or folder) and your settings. The variables keep their SONGPORT_ prefix and the log file its name songport.log, so nothing else needs to change.

Update

The page says when a new version is out (Settings › Updates): twice a day at most it asks GitHub’s public API which version is the latest, nothing else. Turn it off there, or for everyone with SONGPORT_UPDATE_CHECK=false.

The image is ghcr.io/xlollx/trackhop:latest, or a version (:0.7.0): Docker and Trackhop Desktop have their own version, separate from the app’s, listed in the Desktop & Server releases. To update, pull the image and start the container again; everything is in the volume, so nothing is lost:

docker pull ghcr.io/xlollx/trackhop:latest
docker rm -f trackhop
docker run -d --name trackhop … ghcr.io/xlollx/trackhop:latest   # the same command as the first time

With Docker Compose: docker compose pull && docker compose up -d. The page’s footer shows the version running, and the app version whose engine it uses.

Settings

Variable Default What it does
SONGPORT_URL http://127.0.0.1:8080 The address your browser uses to reach Trackhop. Sign-ins come back to <SONGPORT_URL>/callback.
SONGPORT_PASSWORD none Asks for this password on every page. The browser also asks for a user name: any will do, only the password counts. Set it whenever anyone else can reach the port. With SONGPORT_MULTIUSER, the admin’s password.
SONGPORT_MULTIUSER off true: several people, each with their own services and syncs, signing in with a user name and a password. Needs SONGPORT_PASSWORD.
SONGPORT_LOCALE English Language of the texts that come from the engine: it, de, fr, es, pt-BR, pl, ru, tr, zh, ja, ko.
SONGPORT_PORT 8080 Port inside the container.
SONGPORT_DATA /data Where everything is kept (the volume).
SPOTIFY_CLIENT_ID, TIDAL_CLIENT_ID, DEEZER_APP_ID, GOOGLE_CLIENT_ID, LASTFM_API_KEY none Your keys, if you’d rather pass them here than type them in the page.
DEEZER_REDIRECT_URL <SONGPORT_URL>/callback Deezer’s redirect, which must be https.

Everything lives in the /data volume: store.json (syncs, reports, match cache, settings), tokens.json (sign-ins, readable only by the container’s user), songport.log, and csv/ for playlist files. Back that folder up and you have backed up Trackhop.

Services

Service On the server What you need
Spotify yes Your own key from developer.spotify.com
YouTube yes Your own Google OAuth client (with its secret)
TIDAL yes Your own key from developer.tidal.com
Deezer yes (beta) Your own app, and an https address
Navidrome / Subsonic, Jellyfin, Plex yes The server’s address and your login
Last.fm, ListenBrainz yes, as sources Your user name
SoundCloud yes, as a source (unofficial) Nothing: pick it as the source of a new sync and paste a public set or likes link. More
NetEase Cloud Music, QQ Music yes, as sources (unofficial) Nothing: pick one as the source of a new sync and paste a public playlist link. More
CSV files yes Put them in csv/ inside the data volume
YouTube Music yes (unofficial, alpha) The cookies of a browser where you are signed in: see below
Spotify (web) yes (unofficial, alpha) The sp_dc cookie of a browser where you are signed in: see below
Deezer (web) yes (unofficial, alpha) The arl cookie of a browser where you are signed in: see below
Amazon Music, Apple Music no See below

Why not all of them. The other quick routes (Amazon Music, Apple Music without a key) work by running the service’s own web player inside the phone’s browser engine: there is no such browser on a server. Apple Music’s official route signs in with Apple’s MusicKit JS, which needs one too. Keep those syncs in the app.

YouTube Music (unofficial)

The server talks to music.youtube.com the way the app’s built-in YouTube Music connector does, through the web player’s own endpoints, signed in with the cookies of a browser where you are signed in. It is not an official API: Google can change it or block it at any time, and it is an alpha on the server.

Copy the cookies (a desktop browser, Chrome, Edge or Firefox):

  1. Open music.youtube.com and sign in with the account to sync.
  2. Open the developer tools (F12, or Ctrl+Shift+I; on a Mac Cmd+Option+I) and pick the Network tab.
  3. Reload the page, type browse in the filter box and click one of the requests that appear (any request to music.youtube.com will do).
  4. Under Headers, then Request Headers, find cookie:, right-click its value and copy it (in Firefox: Raw view, then select the value).
  5. In Trackhop, Services, YouTube Music (unofficial, alpha), paste it in Cookies and press Connect. The page checks them with one call before saving.

A cookies.txt export or the JSON export of a cookie-editor extension works too: paste the file’s content. Only YouTube’s and Google’s cookies are kept. Optionally paste your browser’s user agent as well (type navigator.userAgent in the developer tools’ Console): requests then look like that browser’s.

What to know before using it:

Your own key. The app’s built-in keys only work with the app’s own redirect (songport://), so on the server every service that needs a key needs yours. Each service page in Trackhop lists the steps and shows the exact redirect address to paste. The rules each service sets:

Open the page at the same address as SONGPORT_URL (127.0.0.1, not localhost): the service sends you back there after the sign-in. Spotify no longer accepts localhost at all.

Signing in from another computer than the server is fine, as long as SONGPORT_URL is the address that computer’s browser uses.

Spotify and Deezer without a key (unofficial)

Spotify (web) and Deezer (web) talk to the web players’ own backends, as the app’s connectors of the same name do, signed in with the cookies of a browser where you are signed in. Not an official API: against the services’ terms, they can stop working when the players change, and they are an alpha on the server.

Copy the cookie (a desktop browser): sign in on open.spotify.com or deezer.com, open the developer tools (F12), then Application (Firefox: Storage) › Cookies › https://open.spotify.com (or https://www.deezer.com). Copy sp_dc for Spotify (with sp_key and sp_t when listed) or arl for Deezer as name=value, several joined by ; , and paste them in Trackhop, Services, in the connector’s Cookies. The cookie: header of a request, a cookies.txt or a JSON export work too.

The same cautions as YouTube Music apply: the cookies are as good as your password, and they end when you sign out in that browser. Spotify’s access token is read from the player page fetched with the cookies: when that page answers as a signed-out visitor, Trackhop says the cookies have expired. Paste fresh ones then.

Radio syncs

A radio sync listens to an internet radio in the hours you choose and saves the songs it plays to a playlist on any service: the morning show’s music in your Spotify, a station’s evening mix on YouTube Music. On the home page, New radio sync:

  1. Find the station by name (from radio-browser.info, a free directory of some 50,000 radios), or paste its stream’s address.
  2. Pick where the songs go: a service, then a playlist or a new one.
  3. Pick the days and the hours. The same time twice listens all day; an end before the start runs past midnight (Friday 22:00–02:00 listens into Saturday night).

While its window is open, Trackhop reads the title of each song the station sends with its stream (Icecast and Shoutcast do, as most internet radios and the streams of FM stations); the audio itself is read and dropped, never kept. Each new song is noted once in a File playlist of its own (*Radio - *), the station's announcements, adverts and news left out, and the sync takes them to your playlist every 10 minutes and once more when the window closes, matched as any sync. Its card wears a radio and says what is on air.

A station that sends no titles is reported on the card (“this stream sends no song titles”): try another of its streams (an MP3 one rather than HLS), or another station. Listening uses the station’s bandwidth (128 kbps is about 60 MB an hour) on the server and on Trackhop Desktop, which are always on. The Android app has radio syncs too: there the phone samples the stream every 2 minutes, on Wi‑Fi by default, instead of listening throughout (guide).

Several people on one server

One container can serve the whole household: each person signs in with their own user name and password and has a Trackhop of their own, with their own services, syncs, files, backups and log. Nobody sees the others’. Add SONGPORT_MULTIUSER:

    environment:
      SONGPORT_URL: http://<NAS-IP>:8080   # example: http://192.168.1.10:8080
      SONGPORT_PASSWORD: choose-one        # the admin's password
      SONGPORT_MULTIUSER: "true"
  1. Open the page and sign in as admin, with SONGPORT_PASSWORD.
  2. In Users, add each person with a user name, a first password and what they may use (below).
  3. They sign in at the same address with their name and password, and connect their own services. The 👤 link with their name, in the top bar, is where they change their password or sign out.

Without SONGPORT_MULTIUSER nothing changes: one Trackhop, one password, any user name.

Behind a reverse proxy

Point the proxy at port 8080 and set SONGPORT_URL to the public https:// address. Trackhop needs nothing else: no websockets, no special headers. /health answers ok without a password, for health checks.

On your phone, as an app

Trackhop’s page can sit on your phone’s home screen and open full screen, like an app (a PWA): the syncs keep running on the server, so it works with the phone off too.

It shows the server’s pages live, nothing is kept on the phone; if the server is off or out of reach, it says so. Connect the services (Spotify and the others) from a browser: their sign-in can leave the home-screen app and not come back to it.

How it runs

Tools

The page’s Tools are the app’s: Export a playlist, File playlists (upload any format, download CSV, M3U, XSPF, JSPF or text), Full backup (by hand, daily or weekly, optionally copied to a folder: mount one, for example -v /mnt/nas/trackhop:/backups, and enter /backups), Duplicates, Unplayable tracks, AI playlists and, on Docker only, Lyrics. Long runs go on in the background: their page refreshes until the result, and you can leave it.

Lyrics

Trackhop can give a music library on the server its lyrics: for each audio file without them, it asks LRCLIB, a free archive written by its users, and saves what it finds next to the file, synced lyrics as <name>.lrc and plain ones as <name>.txt. Navidrome, Jellyfin and Plex show them in their players after their next library scan. Only on Docker, not in the app or in Trackhop Desktop.

Mount the music folder and name it in SONGPORT_MUSIC:

services:
  trackhop:
    image: ghcr.io/xlollx/trackhop:latest
    volumes:
      - ./trackhop-data:/data
      - /path/to/music:/music
    environment:
      SONGPORT_MUSIC: /music

The container runs as user 999, so it needs to be allowed to write in that folder (and only to add files; it changes none): sudo setfacl -R -m u:999:rwX -m d:u:999:rwX /path/to/music gives it that without changing the files’ owner. Do not mount it :ro.

Then Tools › Lyrics looks through the whole library or one folder of it. The artist, title, album and length come from the files’ tags (or from names like Artist - Title.mp3 when a file has none); a title with “Remastered”, “(feat. …)” or “(with …)” is tried again without them, and the length has to match within a few seconds, so a live version does not get the studio lyrics.

For each file, its artist, title, album and length are sent to LRCLIB, nothing else. Lyrics are protected by copyright: they are for your own listening. On a server shared by several people, only the admin sees this tool.

AI playlists

Describe a playlist; your own AI writes it and Trackhop creates it on a service (or saves it into File). Trackhop has no AI of its own and sends the AI only your description. Free ways:

What the AI proposes is searched on the service like any sync, so a track it made up is simply not found.

API

Other tools (Home Assistant, n8n, Node-RED, a script) can list the syncs, start them and read how they went. Turn it on in Tools › API: make a token (shown once; only its hash is kept) and send it as Authorization: Bearer <token>. It is separate from the page’s password; on Trackhop Desktop only the computer itself can reach it. From a terminal or a script, the trackhop command does the same with less typing: docker exec trackhop trackhop run --all, no token needed inside the container.

Call What it does
GET /api/v1/status Version, number of syncs, the ones running, the last run’s time
GET /api/v1/syncs Every sync: source, target, schedule, running, last result
GET /api/v1/syncs/<id> One sync and its last 20 runs
POST /api/v1/syncs/<id>/run Starts it (202)
POST /api/v1/syncs/run-all Starts every enabled sync
GET /api/v1/runs/<id> A run: added, removed, not found, with the tracks
GET /api/v1/services The services and whether they are connected
GET /api/v1/files, GET /api/v1/files/<name>.csv?format=m3u The File playlists, in any export format
POST /api/v1/files/<name> Adds a playlist file sent as the body (CSV, M3U, XSPF, JSPF, iTunes XML, JSON or text), ready to sync (201)
POST /api/v1/syncs/<id>/enable, …/disable Turns a sync on or off
GET /api/v1/backup, POST /api/v1/backup The last full backup and its schedule; starts one (202, with a task id)
GET /api/v1/tasks/<id> A background task (a backup, lyrics): progress, done, error
GET /api/v1/lyrics, POST /api/v1/lyrics The music folder and the last lyrics run (files, saved, synced, plain, not found); starts one (202, with a task id): ?folder=Queen/Jazz for one folder, &synced=true for synced lyrics only, &retry=true to ask again for the ones not found. Lyrics
GET /api/v1/config (?history=true) The whole set-up as a .trackhop file, as Tools › Backup & restore saves it
GET /api/v1/metrics Prometheus metrics: syncs, running, and per sync the last run’s time, result, added, removed and not found
GET /api/v1/services/<service>/playlists A connected service’s playlists (and its liked songs, saved albums… where it has them)
POST /api/v1/services/<service>/playlists Creates a playlist: {"name": "…"} (201, with its id)
GET /api/v1/services/<service>/playlists/<id>/tracks The playlist’s tracks: id, title, artists, album, duration, ISRC
POST /api/v1/services/<service>/playlists/<id>/tracks Adds tracks, searched and matched as a sync does: {"tracks": [{"title": "…", "artist": "…"}, {"link": "https://open.spotify.com/track/…"}, {"id": "…"}, "Artist - Title"]}. Tracks already there are skipped ("allowDuplicates": true to add them anyway); the answer lists what was added, already there and not found
DELETE /api/v1/services/<service>/playlists/<id>/tracks Removes tracks named by id, or by title and artist matched against the playlist (POST …/tracks/remove does the same, for tools that cannot send a body with DELETE)
PATCH, DELETE /api/v1/services/<service>/playlists/<id> Renames ({"name": "…"}) or deletes a playlist, where the service allows it
GET /health ok, with no token: for Docker’s health check, Uptime Kuma or a reverse proxy
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/api/v1/syncs
curl -X POST -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/api/v1/syncs/run-all

Webhook. With an address set in Tools › API, every finished run is POSTed there as JSON ("event": "sync.finished", the sync, and the run with its added and not-found tracks), and every full backup as "event": "backup.finished" (services, failures). With a secret, X-Trackhop-Signature: sha256=<hex> is the HMAC-SHA256 of the body. In Home Assistant, a Webhook trigger with that address starts an automation (a notification when tracks were not found, say); a rest_command that POSTs to /api/v1/syncs/run-all starts every sync from one.

Prometheus and Grafana. /api/v1/metrics speaks Prometheus’ text format; a scrape job with the token is enough:

scrape_configs:
  - job_name: trackhop
    metrics_path: /api/v1/metrics
    bearer_token: th_your_token
    static_configs:
      - targets: ["trackhop:8080"]

Playlists and tracks. <service> is an id from GET /api/v1/services (spotify, ytm, navidrome_1…). A Home Assistant button that adds the song playing in the kitchen to a playlist, or a script that collects the songs a radio played:

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"tracks": ["Daft Punk - Digital Love", {"link": "https://open.spotify.com/track/2VEZx7NWsZ1D0eJ4uv5Fym"}]}' \
  http://127.0.0.1:8080/api/v1/services/ytm/playlists/PLxxxxxxxx/tracks

Scripts. Push a playlist file and sync it, or save the set-up every night:

curl -X POST -H "Authorization: Bearer $TOKEN" --data-binary @mix.m3u http://127.0.0.1:8080/api/v1/files/mix.m3u
curl -H "Authorization: Bearer $TOKEN" -o trackhop-$(date +%F).trackhop http://127.0.0.1:8080/api/v1/config

Not there yet

The connectors that run a service’s own web player (Amazon Music, Spotify and Apple Music without a key, Deezer web, SoundCloud with an account) need a browser inside Trackhop: they are the next step. The compatibility card and the playlist poster are not on the page yet either.

Back to the guide