Self-hosted service
Nezreka/SoulSync avatar
Nezreka/SoulSync

SoulSync: one application for finding, verifying and filing your media library

Intelligent Music & Video Automation Platform

2,267 stars126 forksPythonMIT

At a glance

What is it?
A self-hosted media manager that replaces the arr-and-scraper relay race with a single Flask app that knows what you already own, checks what it downloads, and explains refusals.
Who is it for?
SoulSync is an ambitious consolidation rather than a component you slot into an existing stack, which is the decision to make before installing it. You give up the modularity of separate downloader, tagger and server processes and gain a single audit trail that refuses bad downloads and tells you why.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Replacing the relay race with one application

The framing in the README is specific and worth repeating because it is the whole pitch. Most self-hosted media setups are described as a relay race: one app wants things, another searches, a third downloads, a fourth tags, a fifth tells the media server, and when a handoff fails nobody notices. SoulSync's claim is that it runs the entire race and keeps receipts.

Four properties follow from that framing, and the README lists all four. It knows what you own, so every search, playlist and recommendation is checked against the real library and missing means missing. It checks its work: downloads are fingerprinted, sanity checked, quality ranked and quarantined when they do not hold up. It explains itself, so stuck wishlist rows show which releases were refused and why. And it is one app for the whole house, with per-user profiles so separate people get their own taste and history.

The repository description frames the same project more narrowly as an intelligent music and video automation platform. Both descriptions are in play and they describe different slices of the same codebase, since the tree carries both `core/video/` and a music subsystem.

What the download and metadata surface actually covers

The at-a-glance table in the README is the densest part of the documentation, and it is worth reading as a capability list rather than marketing.

Download sources are Soulseek through slskd, Tidal, Qobuz, Deezer, HiFi, YouTube, SoundCloud, Lidarr, and torrent or usenet through Prowlarr, with the option of a single source or a drag-ordered hybrid chain. Metadata comes from Spotify with or without an account, Apple Music and iTunes, Deezer, Discogs and MusicBrainz, plus fourteen background enrichment workers. Media server targets are Plex, Jellyfin, Navidrome, or SoulSync Standalone, which is the option when you do not want to run a server at all.

That last entry matters for anyone deciding whether to install this. A standalone mode means the application is not a front end for something else, so a reader who wants a downloader only, with no server commitment, still has a path.

For video the scope is comparable: movies, TV and YouTube channels with TMDB and TVDB metadata, Prowlarr for search, qBittorrent, Transmission, Deluge, SABnzbd or NZBGet for retrieval, and Plex or Jellyfin as the server. Automation is a visual WHEN, DO, THEN builder with more than sixty triggers and actions, plus Discord, Telegram, Pushbullet and webhook notifications.

Running it in Docker and the ports you must expose

The compose file is annotated unusually thoroughly, and the annotations are where the operational instructions actually live. The image is published as `boulderbadgedad/soulsync` and the service runs as a container named `soulsync-webui`:

yaml
image: boulderbadgedad/soulsync:latest
container_name: soulsync-webui
environment:
  - PUID=1000
  - PGID=1000
  - UMASK=022

The PUID, PGID and UMASK triplet is the LinuxServer.io convention for mapping file ownership to a host user, which matters if your library lives on a mounted volume.

Three ports are published, and the third pair is easy to miss:

yaml
ports:
  - "8008:8008"   # Main web app
  - "8888:8888"   # Spotify OAuth callback
  - "8889:8889"   # Tidal OAuth callback

The two callback ports are not optional conveniences. Spotify and Tidal OAuth flows redirect the browser to a local port, so if 8888 or 8889 is taken by something else, such as a VPN container, you must change the port number and the matching mapping together, then update the redirect URI both in SoulSync under Settings, Connections and in your Spotify or Tidal developer dashboard. The compose file also carries a forward-compatibility note: from version 1.3 the database volume moved from `/app/database` to `/app/data`, with the same volume preserved.

For a subpath deployment behind a reverse proxy, `SOULSYNC_URL_BASE` is commented out in the compose file with a pointer to `docs/REVERSE_PROXY_SUBPATH.md`. Timezone handling is explicit through `TZ`, and the requirement is not cosmetic: the compose file and the Dockerfile both install timezone data because schedules compute next runs in the user's local zone.

Where the dependency policy is deliberately inconsistent

The dependency file states its own philosophy in a comment: all dependencies are pinned for reproducible builds. That is largely true, and the stack starts here.

text
Flask==3.1.3
Flask-Limiter==4.1.1
spotipy==2.26.0
PlexAPI==4.18.1
requests==2.33.1

Timezone data gets a different treatment, pinned loosely rather than exactly, because the file argues that IANA timezone data changes a few times a year for real daylight saving policy updates and that pinning one snapshot would freeze the application's timezone knowledge to its build date. The dependency list is therefore pinned by default and deliberately floating where the upstream genuinely moves.

The conspicuous exception is yt-dlp. The comment attached to it says it must track upstream releases to stay functional, and then notes that pip resolves it to the latest stable release, which can lag months behind a breaking YouTube change. The Dockerfile then does something different again, and the two documents disagree about which channel is correct:

dockerfile
RUN pip install --no-cache-dir -U --pre "yt-dlp[default]"

Both positions are stated in the repository. The requirements file treats stable as acceptable and tells you to upgrade manually if downloads fail with a format-unavailable error. The Dockerfile treats nightly as mandatory for image builds, and its comment explains that a commit SHA is interpolated into that layer so continuous integration's layer cache is busted on every commit rather than silently pinning a stale nightly for months. If you are not building the image yourself, the published image already follows the nightly rule, and the requirements file is the one you would hit running from source.

A root directory full of planning documents

The tree is where this project differs most from a typical packaged application. Alongside the expected directories (`api/`, `core/`, `services/`, `database/`, `webui/`, `templates/`, `utils/`, `tests/`, `docs/`, `scripts/`, `config/`, `assets/`) sit a dozen markdown files that read like a working notebook.

There is `BEST_IN_CLASS_PLAN.md`, `VIDEO_SIDE_BEST_IN_CLASS_ROADMAP.md` and `IMPORT_PAGE_PLAN.md`. There are per-release announcement drafts such as `RELEASE_3.4.0_discord.md` and `RELEASE_3.4.1_discord.md`. There is `discovery.md`, `pr_description.md`, and a file whose name contains a space, `codex handoff.md`, alongside `.tool-versions` and a dev entry point in both `dev.py` and `dev.sh`.

Two conclusions follow. The first is that the release notes for this project are written for a Discord audience, which is consistent with the badges linking a support channel and a Ko-fi page. The second is that an AI coding assistant is part of the development workflow, and the handoff document is checked in for that purpose. For a reader deciding whether to depend on this project, the useful signal is that documentation, plans and code move in the same commits rather than on a documentation schedule.

`pyproject.toml` in this repository holds no package metadata at all. It contains Ruff configuration targeting Python 3.11 with a line length of 160, and Pytest configuration that filters network-dependent integration tests out of the default run unless you opt in by marker.

The Soulseek warning that comes before any feature

The README puts a callout above the table of contents, which is unusual placement and correctly so: if you use Soulseek, share your files in slskd, because leechers get banned by the network.

Soulseek is a peer-to-peer network, and its norms are closer to a file-sharing community than to a package registry. Downloading without sharing gets you removed. Any evaluation of SoulSync that routes through slskd has to accept that constraint, because the feature is not usable in a one-way fashion.

The less visible operational requirement is the wishlist audit trail. The README describes discovery as matching each source track to real metadata with live progress, with the option to fix any match by hand, including by MusicBrainz ID, to retry failures, or to let a best-effort guessing mode make matches that get reviewed later. Playlists support three sync modes, Replace, Reconcile and Append, with Reconcile preserving the server playlist's image and description. Those three modes are the difference between a mirror and an editor, and they are the part of playlist handling worth reading about before you point this at a library you care about.

The same instinct appears on the media server side, where a side-by-side compare editor for Plex, Jellyfin and Navidrome shows matched, missing and extra tracks, lets you swap versions, find and add, reorder to match the source, and export.

Editorial conclusion

SoulSync is an ambitious consolidation rather than a component you slot into an existing stack, which is the decision to make before installing it. You give up the modularity of separate downloader, tagger and server processes and gain a single audit trail that refuses bad downloads and tells you why. Start with the compose file, publish the two OAuth callback ports alongside 8008, and read the Soulseek note first, because leeching gets you banned from the network before any feature matters.

Frequently asked questions

How does SoulSync work?

It runs the whole download pipeline inside one Flask application: it searches sources, checks results against your existing library, downloads, fingerprints and quality-ranks files, tags them, and pushes them to Plex, Jellyfin, Navidrome or its own standalone player.

Does SoulSync require a separate media server?

No. Plex, Jellyfin and Navidrome are all supported targets, but SoulSync Standalone is listed as an option that needs no server, which makes it usable as a downloader and library on its own.

Why do I need to expose ports 8888 and 8889 besides the main port?

They are the OAuth callback ports for Spotify and Tidal. The browser redirects to those ports during authorization, so if they conflict with another container you must change the port numbers and the matching mappings, then update the redirect URI in both SoulSync and your developer dashboard.

Is it safe to run SoulSync with yt-dlp from the nightly channel?

The repository's own files disagree on this and both say so. The requirements file leaves yt-dlp unpinned on the stable channel and warns it can lag YouTube changes, while the Dockerfile installs the nightly channel deliberately and busts the build cache so a stale nightly cannot persist.

Official sources

  1. License: MIT
  2. Nezreka/SoulSync on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nezreka-soulsync.svg)](https://hysenlabs.com/projects/nezreka-soulsync)