Skip to main content

SpotiFLAC Python Module

Fetch Spotify track metadata and retrieve matching lossless audio through Tidal, Qobuz, Amazon Music and other provider backends — supplied entirely via extensions you choose and configure yourself. Integrate directly into your Python projects, build custom Telegram bots, automation tools, or bulk downloaders.

GitHub stars Latest release PyPI version Python versions GitHub downloads PyPI downloads Telegram community

Disclaimer

This project is intended for educational and personal use only. The developer does not condone or encourage copyright infringement. The software is licensed under the MIT License.

SpotiFLAC-Module-Version is an independent, third-party tool and is not affiliated with, endorsed by, or connected to Spotify, Tidal, Qobuz, Amazon Music, Deezer, or any other streaming service. It is also not affiliated with, and has no control over or responsibility for, any other project sharing a similar name on other platforms.

No copyrighted content is hosted, stored, mirrored, or distributed by this repository. The core application does not bundle, ship, or default to any third-party extension, registry, or provider. Extensions are installed only if a user explicitly configures and requests them from a source of their own choosing; the maintainer has no control over, does not review, and assumes no responsibility for the content or behavior of third-party extensions or registries a user may choose to install.

You are solely responsible for:

  1. Ensuring your use of this software, and any extension or registry you choose to install, complies with your local laws.
  2. Reading and adhering to the Terms of Service of any platform or provider you access, directly or through an extension.
  3. Any legal consequences resulting from the use or misuse of this tool.

This software is provided free of charge by the maintainer. If you paid a third party for access to it, you may have been misled or scammed.

The software is provided "as is", without warranty of any kind, express or implied. The author assumes no liability for any bans, damages, or legal issues arising from its use or misuse. Users assume all risk associated with its use.

If you are a copyright holder or an authorized representative and believe this repository infringes upon your rights, please contact the maintainer with sufficient detail (including relevant URLs and proof of ownership); the matter will be promptly investigated.


Looking for a standalone app?


Why the module (instead of the standalone apps)

The Desktop and Mobile apps are built for direct, immediate use: open it, paste a link, download. The Python module exists for a different case — integrating this logic into something else.

It makes sense to start here if:

  • You're building a bot (Telegram, Discord) or a service that needs to handle requests from many users automatically, not a single manual download.
  • You need the async API (AsyncSpotiFLAC) inside an existing FastAPI/Quart/Sanic app, with shared connection pooling.
  • You want to orchestrate bulk downloads via scripts — multiple playlists, full discographies, local library retagging — with custom logic (filenames via a Python function, post-download actions, saved profiles).
  • You need to run headless, on a server, a NAS, or inside Docker, possibly as part of a larger pipeline.
  • You want full control over which extensions get loaded and from which registry, instead of relying on a fixed set baked into an app.

If you just want a GUI for personal use, with no code involved, the Desktop or Mobile apps remain the simpler choice — this module is the building block those (and similar projects) can be built on top of.


Features

  • Native synchronous and asynchronous Python APIs
  • Modular JavaScript and Python Extension system (bring-your-own registry — nothing bundled)
  • Automatic fallback among the extensions you have installed
  • Built-in GUI, as a native window or served locally in a browser (--gui / --web)
  • Interactive CLI Wizard
  • Docker support
  • Configuration Profiles
  • MusicBrainz metadata enrichment
  • Embedded synchronized lyrics
  • Optional MP3 320 kbps transcoding
  • Watch mode: re-sync a playlist/album/artist/URL on an interval, indefinitely
  • Optional shared-secret or per-account authentication for --web, plus a queued, per-user download history in multi-user mode
  • Extension discovery directories, and optional Ed25519 signature verification on top of registry checksums
  • Local-library duplicate detection by acoustic fingerprint, independent of tags/ISRC
  • Extension scaffolding + dry-run validation for developing your own
  • Installable as a PWA in --web mode (Add to Home Screen / standalone window)

Installation

pip install SpotiFLAC

Important: out of the box, SpotiFLAC does nothing but resolve Spotify metadata — it ships with no built-in provider and no default extension source. Before you can download anything, you need to point it at an extension registry of your own choosing and install at least one extension. See Extensions below.


Quick Start

SpotiFLAC can be used in multiple ways. Choose the mode that fits your needs.

GUI Mode (recommended for most users)

Launch the graphical user interface with the --gui flag:

spotiflac --gui

(Or python launcher.py --gui if running from source)

Web Mode (same GUI, in your browser)

Runs the exact same interface as --gui, served as a local web server instead of a native window — open it at http://127.0.0.1:8000 (or whatever host/port you choose) in any browser:

spotiflac --web

(Or python launcher.py --web if running from source)

Binds to 127.0.0.1 (this machine only) by default. Override with --host/--port if needed — see the CLI Flag Reference below. Useful for running the GUI on a headless machine, inside Docker without a virtual display, or just preferring a browser tab over a native window.

Security note: binding --host to anything other than 127.0.0.1/localhost (e.g. 0.0.0.0, or a LAN address) exposes the GUI — including endpoints that trigger downloads — to anyone who can reach that address, with no authentication of any kind, unless you set up --web-token or --web-multiuser below. Only bind beyond localhost deliberately, on a network you trust, and consider putting it behind your own authentication (a reverse proxy, VPN, etc.) regardless.

Authentication (--web-token)

Off by default (today's behavior, unchanged). Set a shared secret and every request — page, static asset, or API call — needs it, either as ?token=... on the first visit or a cookie from then on:

spotiflac --web --host 0.0.0.0 --web-token "some-long-random-string"
# or: export SPOTIFLAC_WEB_TOKEN="some-long-random-string"

Then open http://your-host:8000/?token=some-long-random-string once; the browser remembers it after that. This travels as a plain query param/cookie with no HTTPS here, so treat it as basic access control on a network you already trust, not a substitute for real TLS.

Multi-user mode (--web-multiuser)

An alternative to (or combined with) --web-token: per-account login instead of one shared secret, plus a small per-user download queue and history.

# One-time: create an account
spotiflac --web-user-add alice "a real password"
spotiflac --web-user-list
spotiflac --web-user-remove alice

# Then run the server with accounts required
spotiflac --web --web-multiuser

The web GUI shows a sign-in screen automatically when it detects --web-multiuser is on (via GET /api/auth/status); a "Sign Out" option then appears under Settings → General. You can also call the endpoints directly — POST /api/auth/login {"username", "password"} to get a session cookie, POST /api/auth/logout to clear it — from a script or a frontend of your own. POST /api/queue/submit-download {"selected_indices", "config"} and GET /api/queue/mine submit and list a user's own queued downloads.

What this does and doesn't isolate: accounts gate who can act, and downloads get tagged with an owner for history/filtering — but every account still shares the same underlying instance (download_dir, current search results, etc.), the same way every browser tab already does in single-user mode. Good for a household or small team who'd otherwise just share one login; not multi-tenant isolation for people who shouldn't see each other's search results.

Installable as an app (PWA)

--web mode is installable — "Add to Home Screen" on a phone, or a standalone window from a desktop browser's install prompt. This needs a secure context: it works out of the box at 127.0.0.1/localhost (browsers treat those as secure even over plain HTTP), but a LAN address (--host 0.0.0.0 and a phone visiting http://192.168.x.x:8000) needs HTTPS in front of it — the same reverse-proxy setup the security note above already recommends for auth. The service worker behind this only exists for installability and a same-page-reload fallback; it's deliberately network-first for everything so it can never make you look at stale frontend code, and it never touches /api/* or the WebSocket.

Interactive Mode (step-by-step wizard)

SpotiFLAC features a smart Interactive Wizard that guides you step-by-step. To launch the wizard, use the --interactive flag:

spotiflac --interactive

(Or python launcher.py --interactive if running from source)

On launch it automatically runs a lyrics-provider health check before asking any questions, so you always know which of your configured lyric sources are reachable.

What the wizard does at startup:

  • Lyrics Provider Health Check — probes the configured lyric endpoints and shows availability inline (✅ / ❌) before asking anything
  • URL History — shows your last 8 downloads so you can re-run one with a single keypress
  • Folder Memory — remembers your last output directory and offers it as the default
  • Profile Load — optionally restores a full saved configuration

Smart URL Detection: If you input an Artist URL, it will ask if you want to download "Featuring" tracks. It skips this question for albums or playlists.

Smart File Paths: If you input a Single Track URL, it will ask if you want to set a specific .flac output path. If you do, it intelligently skips all questions about filename formatting and subfolder organization.

Unified Quality Profiles: Choose HI_RES_LOSSLESS for the best available lossless tier or LOSSLESS for standard lossless audio. SpotiFLAC translates either profile into each provider's native quality token; lossy-only services use their best available audio. If Tidal is your only configured service, the wizard also offers DOLBY_ATMOS — it's Tidal-exclusive, so it isn't offered when other providers are involved (they'd just fall back to HI_RES_LOSSLESS).

CLI Generator: At the end of the configuration, it generates and prints the exact CLI command for your specific setup, so you can copy and reuse it in your automated scripts.

Profile Save: After confirming the download, you can save the entire configuration as a named profile to reuse later.

Python API (Synchronous)

The classic synchronous API remains the simplest way to integrate SpotiFLAC into your own applications. services accepts either a legacy alias (resolved to ext:<id> if you have a matching extension installed) or an explicit ext:<id>.

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url="https://open.spotify.com/track/TRACK_ID",
    output_dir="./downloads",
    services=["ext:tidal-web"],  # requires the corresponding extension to already be installed
)

This API is fully backwards-compatible with previous releases and is recommended for scripts and applications that do not require asynchronous execution.

Which API should I use?

API Best for
SpotiFLAC Scripts, CLI wrappers, automation
AsyncSpotiFLAC Discord bots, Telegram bots, FastAPI, asyncio applications

Asynchronous API

SpotiFLAC now features a 100% native asynchronous engine, making it ideal for modern Python applications built on asyncio, including:

  • Discord bots
  • Telegram bots
  • FastAPI applications
  • Quart / Sanic web servers
  • Background workers
  • Any asynchronous Python project

The new AsyncSpotiFLAC client uses a shared asynchronous HTTP session, allowing multiple downloads and metadata requests to run efficiently without blocking the event loop.

import asyncio
from SpotiFLAC import AsyncSpotiFLAC

async def main():
    async with AsyncSpotiFLAC(
        output_dir="./downloads",
        services=["ext:tidal-web", "ext:qobuz-web"],
        quality="LOSSLESS",
    ) as client:

        # Download a single track
        await client.download_track(
            "https://open.spotify.com/track/TRACK_ID"
        )

        # Fetch playlist metadata without downloading
        info, tracks = await client.get_playlist(
            "https://open.spotify.com/playlist/PLAYLIST_ID"
        )

        print(f"{info['name']} contains {len(tracks)} tracks")

asyncio.run(main())

Why use the async API?

  • Fully non-blocking (asyncio native)
  • Shared HTTP connection pooling
  • Lower memory usage
  • Much better performance when downloading multiple tracks concurrently
  • Perfect for long-running applications and web backends

Note: The classic synchronous SpotiFLAC() API remains fully supported and backwards-compatible.


Extensions

SpotiFLAC has no built-in download provider and no default extension source. Every provider — Tidal, Qobuz, Amazon Music, Deezer, or anything else — is supplied entirely by extensions that you find, review, and choose to install yourself. Two extension runtimes are supported:

  • JavaScript — sharing the same extension format used by SpotiFLAC Mobile, executed via a Node.js bridge.
  • Python — packaged as .spotiflac-ext / .sflx files (a ZIP containing a manifest and a Python entry point), loaded directly in-process.

Extensions are never fetched or installed automatically. You must explicitly configure a registry before SpotiFLAC will contact anything. There are several equivalent ways to do it — pick whichever fits your workflow:

Environment variable:

# Comma-separated list of registry JSON URLs — none is set by default
export SPOTIFLAC_REGISTRIES="https://example.com/my-registry.json"

.env file (see .env.example):

SPOTIFLAC_REGISTRIES=https://example.com/my-registry.json

CLI flag (--registries, repeat once per URL) — persisted to ~/.spotiflac/registry_settings.json, so you only need to pass it once and it's picked up on every future run, exactly like a registry added from the GUI/Interactive wizard:

spotiflac --registries https://example.com/my-registry.json URL ./out

Python API (registries parameter on SpotiFLAC() / AsyncSpotiFLAC()) — persisted the same way as the CLI flag:

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url="https://open.spotify.com/track/...",
    output_dir="./downloads",
    registries=["https://example.com/my-registry.json"],
)
from SpotiFLAC import AsyncSpotiFLAC

async with AsyncSpotiFLAC(
    output_dir="./downloads",
    registries=["https://example.com/my-registry.json"],
) as client:
    await client.download_track("https://open.spotify.com/track/...")

Interactive wizard / GUI — both expose a registry manager (add, remove, list, and see where each URL came from) without touching environment variables or files by hand.

All of these feed into the same merged, deduplicated list (extensions.registry_config.effective_urls()), regardless of which entry point you use — the CLI, the GUI, and the Python API all end up contacting the same registries.

Once configured, you can also install and manage extensions directly:

from SpotiFLAC.extensions import ExtensionManager

em = ExtensionManager()
em.install("some-extension-id", registry_url="https://example.com/my-registry.json")

Extensions use the ext: prefix and are referenced like any other provider:

spotiflac URL ./out \
  --service ext:tidal-web ext:qobuz-web

Note: If Node.js is not installed, SpotiFLAC automatically attempts to install it the first time a JavaScript extension is used, printing progress as it goes (core/node_check.py) — it never escalates privileges itself (no sudo/runas is ever added on your behalf), so on Linux this works out of the box when already running as root (e.g. inside the Docker image) and otherwise falls back to telling you the exact command to run yourself. A startup check (same idea as the ffmpeg one) also warns upfront if Node.js is missing, independent of whether the auto-install ends up working.

Supported package managers:

  • Linux: apt-get, dnf, yum, pacman
  • macOS: brew
  • Windows: winget, choco

A note on legacy names: for backwards compatibility, short names like tidal, qobuz, amazon, deezer, apple, soundcloud, youtube, pandora are still accepted in services/--service, and are resolved to an installed extension with a matching ID (e.g. tidalext:tidal-web) if — and only if — you have that extension installed. They are aliases, not built-in providers; nothing downloads without an extension behind it.

The maintainer does not review, endorse, or take responsibility for the content or behavior of any third-party registry or extension. Choose your sources with the same care you would apply to installing any other third-party code.

Extension Discovery (Directories)

Finding a registry in the first place is still on you — a directory is just a JSON file that lists registries (name, URL, description), for you to review and add yourself the normal way. Nothing is bundled here either: no default directory ships with SpotiFLAC.

export SPOTIFLAC_REGISTRY_DIRECTORIES="https://example.com/my-directory.json"
# or: spotiflac --registry-directories https://example.com/my-directory.json URL ./out

A directory JSON looks like:

{
  "registries": [
    {
      "name": "Example Community Registry",
      "url": "https://example.com/registry.json",
      "description": "A few extra extensions",
      "maintainer": "someone"
    }
  ]
}

Once you've added one, Settings → Extensions in the GUI (or get_registry_directories() / add_registry_directory() / remove_registry_directory() / discover_registries() in the Python/web API) fetches it and probes each listed registry for reachability, so you see a "reachable, N extensions" badge before deciding to add it as a registry of your own via the normal --registries flow. Probing is read-only and never installs anything on its own.

Registry Trust (Signed Extensions)

The sha256 checksum a registry provides (see above) proves a package wasn't corrupted or swapped in transit — it says nothing about who put it in the registry in the first place. Ed25519 signatures close that gap: a registry maintainer signs each entry with their own private key, and you decide whose public key you're willing to trust, once, up front. Nothing is trusted by default — an unsigned entry is exactly as trusted as it is today (checksum-only, if the registry provides one at all).

# Add a maintainer's public key you've decided to trust
spotiflac --trust-key-add "some-maintainer" "<base64 Ed25519 public key>"
spotiflac --trust-key-list
spotiflac --trust-key-remove "some-maintainer"

Or from Settings → Extensions in the GUI ("Trusted Signing Keys"), backed by get_trusted_keys() / add_trusted_key() / remove_trusted_key() in the Python/web API.

Once added, any installed extension's RegistryEntry gets a .trust_tier of "signed" (verified against a trusted key), "checksum-only", or "unverified".

For registry maintainers — generate a keypair and sign your own entries with the bundled tool:

python -m SpotiFLAC.tools.registry_signing_cli keygen
# publish the printed public key however you publish your registry;
# keep the private key secret

python -m SpotiFLAC.tools.registry_signing_cli sign \
  --private-key <base64> --id tidal-web --version 1.2.0 \
  --sha256 <hex> --download-url https://example.com/tidal-web.spotiflac-ext
# paste the printed "signature" into that entry in your registry.json

Developing Extensions

  • JavaScript extensions reuse the format built for SpotiFLAC Mobile. Its Extension Development Guide is the closest available reference, but it was written for Mobile — some details (packaging, available runtime capabilities) may not match this project exactly. Verify against this repository's own loader (SpotiFLAC/extensions/runtime.py) before relying on it.
  • Python extensions are ZIP packages (.spotiflac-ext / .sflx) containing a manifest and a Python module, loaded directly by SpotiFLAC/extensions/python_provider.py. There's no separate guide yet — reading that file, and an existing extension's manifest, is currently the best way to see the expected shape.

Scaffolding a new extension generates a starting point that already satisfies this repo's own loader, instead of reverse-engineering the shape from an existing extension:

spotiflac --ext-scaffold my-provider --runtime python      # or --runtime javascript
# writes ./my-provider/{manifest.json, my_provider.py, README.md}

Validating it — without installing into your real ~/.spotiflac/extensions or contacting any registry — checks the manifest, confirms the entry point exists and imports/parses cleanly, and (Python) that it exposes exactly one BaseProvider subclass, or (JavaScript, if node is on PATH) that it's syntactically valid and calls registerExtension(...):

spotiflac --ext-dry-run ./my-provider
# or against an already-packaged ZIP:
spotiflac --ext-dry-run ./my-provider.spotiflac-ext

If you build something reusable, consider publishing it to your own registry rather than asking the maintainer to bundle or endorse it — see Extensions above for why nothing is bundled by design.


Docker Usage & Headless Automation

A lightweight, CLI-focused Docker image is available for running SpotiFLAC on servers, NAS devices, or any headless environment.

Build the Image

docker build -t spotiflac .

Basic Docker Usage

The image runs a virtual display (Xvfb) and exposes it over VNC — some installed extensions may rely on a headless browser internally. Map port 6080 (web VNC viewer) and set --shm-size=1g, or the browser-dependent parts may crash:

Run a download by mounting local directories to persist your downloads, configuration, cache, and extension registry across container restarts. Remember to also pass SPOTIFLAC_REGISTRIES (via -e or an .env file) since none is configured by default:

docker run --rm -it \
  -p 6080:6080 \
  --shm-size=1g \
  -e SPOTIFLAC_REGISTRIES="https://example.com/my-registry.json" \
  -v "$(pwd)/downloads:/app/downloads" \
  -v "$(pwd)/.spotiflac_docker:/root/.spotiflac" \
  -v "$(pwd)/.cache_docker:/root/.cache/spotiflac" \
  spotiflac "https://open.spotify.com/track/TRACK_ID" \
  /app/downloads -s ext:deezer-web -q LOSSLESS

Open http://localhost:6080/vnc.html in a browser to watch the virtual screen live, if needed. Set X11VNC_PASSWORD (env var, see .env.example) to protect the VNC session with a password; if unset, it starts without one.

Web Mode in Docker (lighter alternative to VNC)

If you just want the GUI itself over the network — not a live view of a virtual desktop — --web mode needs none of the above. The entrypoint detects --web and skips Xvfb/Fluxbox/VNC entirely, so the container starts faster and uses less memory:

docker run --rm -it \
  -p 8000:8000 \
  -e SPOTIFLAC_REGISTRIES="https://example.com/my-registry.json" \
  -v "$(pwd)/downloads:/app/downloads" \
  -v "$(pwd)/.spotiflac_docker:/root/.spotiflac" \
  -v "$(pwd)/.cache_docker:/root/.cache/spotiflac" \
  spotiflac --web --host 0.0.0.0 --port 8000

Open http://localhost:8000 in a browser.

Note: --host 0.0.0.0 is required here — the CLI default (127.0.0.1) would only accept connections from inside the container itself, unreachable from the host. This also means the GUI is reachable by anything that can reach the mapped port, with no authentication unless you add --web-token / --web-multiuser (see Authentication). Only publish the port on a network you trust, or put it behind your own authentication/reverse proxy.

docker-compose.example.yml in the repo root does the above as a compose file, plus a real HTTP healthcheck for this specific mode (docker compose -f docker-compose.example.yml up).

Published Image (GHCR)

Official Docker images are published on GitHub Container Registry (GHCR), allowing you to run the latest version without building locally.

docker pull ghcr.io/bartolomeorusso9/spotiflac-module-version:latest

Logs in Headless Environments

A progress bar is a stream of carriage returns: readable on a terminal, unreadable in a log file. docker logs collapses each refresh into a [285B blob data] line, which buries everything worth reading.

SpotiFLAC therefore draws animated bars only when stderr is an interactive terminal. Everywhere else — Docker, cron, a redirected file — it prints the same information as plain lines instead:

[RUN] 24 track(s) · ext:tidal-web, ext:qobuz-web · LOSSLESS · 2 in parallel → /app/downloads
Track [3/24] Track Title — Artist Name (Album Name)
  ⬇  Track Title  ·  47%  ·  13.4 MB / 28.4 MB
  ✓  Track Title  ·  TIDAL-WEB  ·  FLAC  ·  28.4 MB  ·  12s

Progress lines are throttled to at most one per 25% and per 10 seconds, so a track costs a handful of lines rather than one per received chunk.

Set SPOTIFLAC_PROGRESS_BARS to override the detection in either direction:

export SPOTIFLAC_PROGRESS_BARS=0   # never draw bars, even on a terminal
export SPOTIFLAC_PROGRESS_BARS=1   # always draw bars

Supported URL Types

SpotiFLAC's core resolves the following URL formats as input; whether a given target is actually reachable depends entirely on which extensions you have installed:

Type Spotify
Track open.spotify.com/track/...
Album open.spotify.com/album/...
Playlist open.spotify.com/playlist/...
Discography (via artist URL) open.spotify.com/artist/...

Extensions may add support for resolving Tidal, Apple Music, SoundCloud, YouTube, Pandora, or other platform URLs directly, and may output FLAC, ALAC/M4A, AAC, or MP3 depending on what the source and the extension support. Consult the documentation of the specific extension you install for its supported URL formats and output format.


Advanced Configuration

You can customize the download behavior, prioritize specific installed extensions, and organize your files automatically into folders.

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url="https://open.spotify.com/album/ALBUM_ID",
    output_dir="./MusicLibrary",
    services=["ext:qobuz-web", "ext:amazon-web", "ext:tidal-web"],
    filename_format="{year} - {album}/{track}. {title}",
    use_artist_subfolders=True,
    use_album_subfolders=True,
    loop=60,                     # retry duration in minutes
    track_max_retries=2,         # extra per-track retries on failure
    post_download_action="notify"
)

Lyrics Provider Health Check

SpotiFLAC can probe the endpoints of the configured lyrics providers before embedding lyrics, to verify which lyric sources are currently reachable.

This check is specifically about lyrics sources, not audio download providers. In Interactive Mode it runs automatically at startup. In code or scripts you can call it directly:

from SpotiFLAC.core.health_check import (
    run_health_check,
    print_health_report,
    get_working_providers,
)

import asyncio
from SpotiFLAC.core.health_check import (
    run_health_check,
    print_health_report,
    get_working_providers,
)

async def main():
    results = await run_health_check(["ext:tidal-web", "ext:qobuz-web", "ext:deezer-web"])
    print_health_report(results)

    working = get_working_providers(results)
    print("Available providers:", working)

asyncio.run(main())
# CLI: check installed extensions then download
spotiflac https://open.spotify.com/track/... ./out --service ext:tidal-web ext:qobuz-web

The health check runs in parallel with a configurable timeout (default: 5 s per endpoint) and never blocks your download if a check fails. In the GUI, the check reports provider-level availability and endpoint counts, without exposing individual raw endpoint URLs.

Configuration Profiles

Save and reuse complete download configurations without re-typing them every time.

Save a profile

# Save current flags as "hires-tidal"
spotiflac https://... ./out \
  --service ext:tidal-web \
  --quality HI_RES_LOSSLESS \
  --use-album-subfolders \
  --filename-format "{year} - {album}/{track}. {title}" \
  --save-profile hires-tidal

Load a profile

# Load "hires-tidal" — flags override profile values when both are present
spotiflac https://... ./out --profile hires-tidal

In Python

import asyncio
from SpotiFLAC.core.profiles import (
    save_profile_async,
    get_profile_async,
    list_profiles_async,
)

async def main():
    await save_profile_async("hires-tidal", {
        "services":             ["ext:tidal-web"],
        "quality":              "HI_RES_LOSSLESS",
        "use_album_subfolders": True,
        "filename_format":      "{year} - {album}/{track}. {title}",
    })

    cfg = await get_profile_async("hires-tidal")
    print(await list_profiles_async())  # ['hires-tidal']

asyncio.run(main())

Profiles are stored at ~/.cache/spotiflac/profiles.json. In the Interactive Wizard, you are prompted to load a profile at startup and optionally save one at the end.

Batch Downloads

Pass a list of URLs to download them all in sequence. Failed tracks per URL are collected and can be retried with loop.

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url=[
        "https://open.spotify.com/album/ALBUM_ID",
        "https://open.spotify.com/playlist/PLAYLIST_ID",
    ],
    output_dir="./MusicLibrary",
    services=["ext:tidal-web", "ext:qobuz-web"],
    use_album_subfolders=True,
)

Auto-Retry on Failure

Set track_max_retries (Python) or --retries (CLI) to automatically retry failed tracks. Each retry cycles through all configured extensions from the beginning, waiting exponentially longer between attempts (2 s → 4 s → 8 s …, capped at 30 s).

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url="https://open.spotify.com/album/...",
    output_dir="./downloads",
    services=["ext:tidal-web", "ext:qobuz-web", "ext:deezer-web"],
    track_max_retries=3,   # up to 3 extra attempts per track
)
spotiflac https://open.spotify.com/album/... ./out \
  --service ext:tidal-web ext:qobuz-web ext:deezer-web \
  --retries 3

Tip: Combine --retries with --loop for maximum resilience — --retries handles transient errors on individual tracks, while --loop re-queues permanently failed tracks after N minutes.

Per-Track Timeout

Set timeout_s (Python) or --timeout (CLI) to cap the time SpotiFLAC will spend downloading a single track. If the download does not complete within the specified number of seconds, the process is terminated and the track is marked as failed — allowing the next extension or retry to take over.

# CLI — skip any track that takes more than 3 minutes
spotiflac https://open.spotify.com/album/... ./out --service ext:tidal-web --timeout 180
# Python API
from SpotiFLAC import SpotiFLAC
SpotiFLAC(
    url="https://open.spotify.com/album/...",
    output_dir="./downloads",
    services=["ext:tidal-web", "ext:qobuz-web"],
    timeout_s=120,
)

Tip: Pair --timeout with --retries so that a stalled track is automatically re-attempted against the next extension instead of blocking the entire queue indefinitely.

MP3 Transcoding

Downloads use the selected quality profile: HI_RES_LOSSLESS requests the best available lossless tier, while LOSSLESS requests standard lossless audio. Set transcode_to="mp3" (Python) or --mp3 / --transcode mp3 (CLI) to convert every finished track to MP3 — 320 kbps by default — for players or car stereos that cannot handle lossless files. Tags, cover art and lyrics are carried over to the MP3, and the original file is deleted once the conversion succeeds unless transcode_keep_original / --keep-original is set.

Requires ffmpeg. Checked upfront — before any track downloads — so you never download a whole album only to fail at the conversion step. If it's not on your PATH, SpotiFLAC automatically attempts to install it right there, printing progress as it goes (core/ffmpeg_check.py), using the same package managers and privilege rules as the Node.js auto-install described above (never escalates privileges itself); the run only fails if that attempt doesn't work out. Tidal FLAC muxing and Amazon decryption also need ffmpeg but have no such auto-install — they just fail if it's missing, same as before.

# CLI — every track ends up as a 320 kbps MP3
spotiflac https://open.spotify.com/album/... ./out --service ext:tidal-web --mp3

# Keep the FLAC too, and use 192 kbps instead
spotiflac https://open.spotify.com/album/... ./out --mp3 --transcode-bitrate 192k --keep-original
# Python API
from SpotiFLAC import SpotiFLAC
SpotiFLAC(
    url="https://open.spotify.com/album/...",
    output_dir="./downloads",
    services=["ext:tidal-web", "ext:qobuz-web"],
    transcode_to="mp3",
    transcode_bitrate="320k",
)

Skipping already-downloaded tracks still works. The converted file keeps the exact name the extension would have used, only with an .mp3 extension, so SpotiFLAC looks for that file before contacting any extension and skips the track when it is already there — no network request, no re-encode. Running the same album twice therefore costs nothing the second time. A leftover file from an earlier lossless run is converted in place instead of being re-downloaded, so an existing library converges to MP3 in a single pass.

The conversion is a no-op for extensions that already deliver MP3, which are passed through untouched.

Hi-Res Verification

Enable verify_hires=True (Python) or --verify-hires (CLI) to run a spectral-analysis QA check on every successful lossless download, flagging files that declare a high sample rate (e.g. 96 kHz) but whose actual audio content stops well short of it — a common fingerprint of upsampling: taking a CD-quality or lossy source and re-encoding it at a higher sample rate without adding any real high-frequency content, so it looks like Hi-Res without being one.

spotiflac https://open.spotify.com/album/... ./out --service ext:tidal-web -q HI_RES_LOSSLESS --verify-hires
from SpotiFLAC import SpotiFLAC
SpotiFLAC(
    url="https://open.spotify.com/album/...",
    output_dir="./downloads",
    services=["ext:tidal-web"],
    quality="HI_RES_LOSSLESS",
    verify_hires=True,
)

How it works: for each finished track, a short segment (default 30s) is decoded from the middle of the file — never the whole track, to keep memory usage bounded — and its average frequency spectrum is compared against the noise floor. If the file's sample rate implies Hi-Res but no real content is found above ~24 kHz, a warning is printed and logged; nothing else happens.

Design notes worth knowing before you turn it on:

  • Off by default and fully opt-in. It requires the optional librosa and numpy packages, which are not installed by default — install them with pip install librosa numpy or pip install SpotiFLAC[hires]. If they're missing, the check is silently skipped (a debug-level log line, nothing more) rather than breaking your run.

  • Never blocks or fails a download. The check runs as a background task after the file has already been saved successfully — a track download is never delayed, retried, or marked as failed because of it, and analysis errors (corrupt segment, unreadable file, etc.) are swallowed and logged at debug level, not surfaced as errors.

  • A finding is a hint, not a certification. Some genuine Hi-Res masters are deliberately low-pass filtered during mastering (common in pop/rock) and will still read as "no anomaly". Treat a "possibly upsampled" warning as something worth a closer listen, not definitive proof.

  • Skipped automatically for lossy output. If transcode_to="mp3" (or --mp3) is set, the already-lossy result is never analyzed — checking an MP3 for ultrasonic content would be meaningless.

  • Standalone tool. The underlying checker also ships as a CLI you can point at any file(s) you already have, independent of a download run:

    python -m SpotiFLAC.tools.hires_check_cli "My Track.flac" --seconds 45
    

Multiple Playlists in One Folder

Pass --playlist (-p) once per playlist to sync several of them into a single destination folder. Repeat the flag as many times as you need — the last positional argument is the destination:

spotiflac -p https://open.spotify.com/playlist/AAA \
          -p https://open.spotify.com/playlist/BBB \
          -p https://open.spotify.com/playlist/CCC \
          ./Music --service ext:tidal-web
  • One copy per track. A song that appears in three of those playlists is downloaded once. Tracks are matched by ISRC (resolved automatically when the metadata lacks it), falling back to artist + title, so the same recording pulled from different playlists is recognised even when the catalogue ids differ.
  • Nothing already on disk is downloaded again. The destination folder is indexed before any extension is contacted, in any audio format — a track already there as .m4a is not re-fetched just because this run would produce a .flac.
  • One M3U per playlist. Each playlist gets a <Playlist Name>.m3u8 file in the destination folder listing its own tracks, in playlist order, with paths relative to the folder — so the whole directory stays portable and can be copied to a phone or a USB stick as is. Two playlists sharing a name get Name.m3u8 and Name (2).m3u8.
  • Cheap to re-run. Playlist files are rewritten only when their content actually changed. Running the same command again after a playlist gained a track downloads that one track and touches that one M3U file.

Tracks that failed to download are left out of the playlist file, so it always lists files that really exist; they are picked up on the next run.

Everything else keeps working as usual — --mp3, --filename-format, --service, --retries and friends all apply:

# Sync three playlists as 320 kbps MP3, writing classic .m3u files
spotiflac -p URL1 -p URL2 -p URL3 ./Music --mp3 --m3u m3u

With --mp3 a playlist entry points at the converted file, and a track already present as MP3 is skipped without any network request. Use --m3u none to merge the playlists into one folder without writing playlist files at all.

Note: avoid --use-track-numbers (and {position} in --filename-format) here: the number depends on the merged playlist order, so filenames would change whenever any playlist does — and previously downloaded tracks would be fetched again under the new name. SpotiFLAC warns when you do.

Watch Mode (keep syncing on an interval)

Add --watch MINUTES to any run — a single URL, or one or more --playlist — to re-run the exact same sync every N minutes, forever, instead of exiting after one pass:

# Re-check this playlist every hour for new tracks
spotiflac https://open.spotify.com/playlist/... ./Music --service ext:tidal-web --watch 60

Every download path already indexes what's on disk and skips it (by ISRC/tags for --playlist, by filename otherwise — see Multiple Playlists in One Folder above and the download flow in general), so each cycle after the first is cheap: it only fetches tracks that are actually new. Stop it with Ctrl+C.

--watch is a different tool from --loop: --loop retries failed tracks for a bounded time after one session ends; --watch re-runs the whole sync indefinitely. Combine both if you want each cycle to also retry transient failures:

spotiflac https://open.spotify.com/album/... ./Music --watch 1440 --loop 30

--watch is saved/restored by --save-profile/--profile like any other flag. Not available in --interactive mode, and it does not cover Spotify's "Liked Songs" — that's a private, per-account list that would need a full Spotify login (OAuth) to read, which this project deliberately doesn't implement (see the "no-account" design goal throughout this README). Point --watch at a public playlist, album, or artist URL instead.

Post-Download Actions

Action Description
none Do nothing (default)
open_folder Open the output folder in the system file manager
notify Send an OS desktop notification with a summary
command Run a custom shell command — placeholders: {folder}, {succeeded}, {skipped}, {failed} (quote {folder} in your template, e.g. '{folder}', to handle spaces; this does not protect against an apostrophe inside the path itself)
SpotiFLAC(url="...", output_dir="./downloads", post_download_action="open_folder")

SpotiFLAC(url="...", output_dir="./downloads",
          post_download_action="command",
          post_download_command="rsync -av '{folder}/' user@nas:/music/")
spotiflac https://... ./out --post-action notify
spotiflac https://... ./out --post-action command --post-command "rsync -av '{folder}/' user@nas:/music/"

Note: Wrap {folder} in single quotes in your command template (e.g. '{folder}') to safely handle spaces and most special characters. Single quotes do not protect against an apostrophe (') inside the output path itself — avoid apostrophes in output_dir, or escape them manually for your shell before running the command.

Discography Download

Download the complete discography of an artist. Duplicate tracks (same ISRC across different releases) are automatically skipped.

from SpotiFLAC import SpotiFLAC

SpotiFLAC(url="https://open.spotify.com/artist/ARTIST_ID", output_dir="./MusicLibrary",
          services=["ext:qobuz-web", "ext:tidal-web"], use_album_subfolders=True,
          filename_format="{year} - {album}/{track}. {title}")
spotiflac https://open.spotify.com/artist/... ./MusicLibrary \
  --service ext:tidal-web --include-featuring \
  --use-album-subfolders --filename-format "{year} - {album}/{track}. {title}"

Recommended layout: --use-album-subfolders + --filename-format "{year} - {album}/{track}. {title}".

Custom Output Path (single tracks)

For single track downloads you can specify the exact file path instead of relying on output_dir + filename_format.

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url="https://open.spotify.com/track/TRACK_ID",
    output_dir="./downloads",
    output_path="files/song.flac"
)

Note: output_path is automatically ignored when the URL points to an album, playlist, or artist/discography.

Passing Settings to an Extension (e.g. a self-hosted API instance)

qobuz_local_api_url and tidal_custom_api (and equivalents you'll find documented by other extensions) are not built-in behaviors of the core — they are optional settings forwarded to whichever extension you have installed for that service, if that extension supports them. Whether they do anything at all, what they connect to, and what account or credentials they expect depends entirely on the specific extension's own documentation and implementation, which the maintainer of this repository does not control or vouch for.

from SpotiFLAC import SpotiFLAC

SpotiFLAC(
    url="https://open.spotify.com/track/TRACK_ID",
    output_dir="./downloads",
    services=["ext:tidal-web"],
    tidal_custom_api="https://your-instance.example.com",
)
spotiflac https://open.spotify.com/track/... ./downloads \
  --service ext:tidal-web \
  --tidal-api "https://your-instance.example.com"

Note: These values are also saved and restored when using --save-profile / --profile.


Local Tagging

Improve your existing music library by automatically matching local audio files against Spotify metadata and applying professional-grade tags. This is useful for:

  • Fixing incomplete or incorrect tags on older ripped CDs or downloads
  • Enriching a library with album art, genres, BPM, ISRCs, and other metadata from Spotify and MusicBrainz
  • Bulk-updating hundreds of files in a single operation

The Local Tagging system works in three phases:

  1. Scan — reads all audio files in a folder, extracts their current tags, and (for files with no tags) guesses artist/title from the filename
  2. Match — searches for each file using the extracted or guessed metadata, returns ranked candidate matches from Spotify sorted by confidence
  3. Apply — writes the chosen metadata to each file with automatic backup

Supported Audio Formats

Scans and tags any format that SpotiFLAC can write: FLAC, MP3, M4A/AAC, OGG Vorbis, Opus, WAV, AIFF, WMA, WavPack, Monkey's Audio, Musepack, TrueAudio.

Using the GUI / Web Interface

Open the "Fix Local Files" tab and follow the wizard:

  1. Choose a folder — browse to your music directory (or drag & drop a folder onto the interface)
  2. Review matches — SpotiFLAC scans, matches, and displays each file with up to 5 candidate matches, sorted by confidence (0–100)
  3. Select metadata — for each file, choose which match to apply, or skip it entirely
  4. Preview changes — see what tags will be written before applying
  5. Apply — apply all changes at once with progress tracking and automatic per-file backup

Files with confidence ≥ 90% are marked as "safe to auto-apply"; files below that threshold are flagged for manual review.

Using the Python API

import asyncio
from SpotiFLAC.core.local_processor import (
    scan_and_match_async,
    retag_local_file_async,
    default_embed_options,
)
from SpotiFLAC.core.models import TrackMetadata

async def fix_library(folder_path: str) -> None:
    # Phase 1 & 2: scan folder and find matches for each file
    entries = await scan_and_match_async(
        folder_path,
        recursive=True,           # scan subdirectories too
        candidates_per_file=5,    # show top 5 matches
    )

    # Phase 3: apply metadata for each file
    embed_opts = default_embed_options()
    for entry in entries:
        if entry.best and entry.is_safe_match:  # confidence >= 90%
            result = await retag_local_file_async(
                file_path=str(entry.info.file_path),
                metadata=entry.best.metadata,
                options=embed_opts,
                backup=True,  # automatic .bak backup
            )
            if result.success:
                print(f"✓ {entry.info.file_path}: tagged successfully")
            else:
                print(f"✗ {entry.info.file_path}: {result.error}")

asyncio.run(fix_library("~/Music/MyLibrary"))

Match Confidence & Safety

Matching uses a string-similarity algorithm that compares the file's title + artist against each Spotify search result:

  • Confidence ≥ 90% — marked as "safe" and can be auto-applied without review
  • Confidence < 90% — flagged for manual review to avoid mislabeling

The matching algorithm is heuristic and does not analyze audio content — it compares text only. A track with unusual spelling or featuring artists can have legitimate lower scores even when the match is correct. Always review before applying in bulk if you're unsure.

Metadata Written

When applying a match, the following tags are written (previous tags are stripped):

  • Standard tags: title, artist, album, album artist, date, disc, track number, genre
  • Extended metadata: ISRC, BPM, labels, lyrics (if embed_lyrics=True)
  • Cover art: highest-resolution available from Spotify and enrichment providers
  • MusicBrainz enrichment: genre, BPM, organization/label, UPC (if available)

Backup & Recovery

Every file gets an automatic .bak backup before tagging:

MyTrack.flac         (original)
MyTrack.flac.bak     (backup)

If something goes wrong during the apply step, the backup is restored and the operation is rolled back for that file. You can also delete .bak files manually after confirming the results are correct.

Per-File Customization

Fine-tune embedding options for specific use cases:

from SpotiFLAC.core.tagger import EmbedOptions

custom_opts = EmbedOptions(
    embed_cover=True,
    embed_lyrics=True,
    lyrics_type="lrc",
    flac_compression_level=8,
)

result = await retag_local_file_async(
    file_path="song.flac",
    metadata=matched_metadata,
    options=custom_opts,
    backup=True,
)

Duplicate Detection (acoustic fingerprint)

Local Tagging's own dedup (above) matches by ISRC or by normalized title+artist text — cheap and usually right, but blind to a re-rip with wrong or missing tags, or the same recording pulled from two different providers with slightly different metadata. This is a second, independent signal that looks at the audio itself instead: Chromaprint acoustic fingerprints, compared locally — no network call, no AcoustID lookup, no API key.

Off by default and fully opt-in (same posture as Hi-Res Verification): needs the optional pyacoustid package and the fpcalc binary it wraps.

pip install SpotiFLAC[dedup]
# then install fpcalc — most package managers ship it as "chromaprint" or
# "libchromaprint-tools" (see https://acoustid.org/chromaprint)

python -m SpotiFLAC.tools.dedup_check_cli ~/Music/MyLibrary
Fingerprinting 340 file(s)…

Found 2 duplicate group(s):

Group 1 (2 files):
  - /Users/you/Music/MyLibrary/Artist - Song.flac
  - /Users/you/Music/MyLibrary/Compilation/Artist - Song (re-rip).mp3

Also available as a "Find Duplicates" button in the GUI's Fix Local Files tab (same folder path as a normal scan), backed by get_dedup_status() (whether it can run at all on this machine) and scan_for_duplicates(path, recursive=True, threshold=0.95) (runs in a background thread; results arrive via the app_dedup_results push event, app_dedup_error on failure — same shape as scan_local()), or directly in Python:

from SpotiFLAC.core.audio_fingerprint import (
    compute_fingerprint, find_duplicate_groups, is_available,
)

if is_available():
    fingerprints = [compute_fingerprint(f) for f in my_files]
    for group in find_duplicate_groups(fingerprints):
        print("Duplicates:", group)

A duration pre-filter (duration_tolerance_s, default 3.0) skips the (more expensive) fingerprint comparison for any pair that couldn't plausibly match, so this stays practical for a real, varied library. Like Hi-Res Verification, treat a match as a strong hint, not a certification — review before deleting anything.


CLI Usage (standalone executables)

./SpotiFLAC-Windows.exe url
                        output_dir
                        [--service ext:<id> [ext:<id> ...]]
                        [--filename-format "{title} - {artist}"]
                        [--output-path "files/song.flac"]
                        [--quality LOSSLESS]
                        [--use-track-numbers]
                        [--use-album-track-numbers]
                        [--use-artist-subfolders]
                        [--use-album-subfolders]
                        [--first-artist-only]
                        [--artist-separator SEP]
                        [--qobuz-local-api URL]
                        [--tidal-api URL]
                        [--timeout seconds]
                        [--loop minutes]
                        [--no-extensions-fallback]
                        [--verbose]
                        [--no-lyrics]
                        [--lyrics-providers spotify apple musixmatch amazon lrclib]
                        [--no-enrich]
                        [--enrich-providers deezer apple qobuz tidal]
                        [--retries N]
                        [--post-action none|open_folder|notify|command]
                        [--post-command "CMD with {folder} {succeeded} {skipped} {failed}"]
                        [--profile NAME]
                        [--save-profile NAME]
chmod +x SpotiFLAC-Linux-arm64
./SpotiFLAC-Linux-arm64 url
                        output_dir
                        [--service ext:<id> [ext:<id> ...]]
                        [--filename-format "{title} - {artist}"]
                        [--output-path "files/song.flac"]
                        [--quality LOSSLESS]
                        [--use-track-numbers]
                        [--use-album-track-numbers]
                        [--use-artist-subfolders]
                        [--use-album-subfolders]
                        [--first-artist-only]
                        [--qobuz-local-api URL]
                        [--tidal-api URL]
                        [--timeout seconds]
                        [--loop minutes]
                        [--no-extensions-fallback]
                        [--verbose]
                        [--no-lyrics]
                        [--lyrics-providers spotify apple musixmatch amazon lrclib]
                        [--no-enrich]
                        [--enrich-providers deezer apple qobuz tidal]
                        [--retries N]
                        [--post-action none|open_folder|notify|command]
                        [--post-command "CMD with {folder} {succeeded} {skipped} {failed}"]
                        [--profile NAME]
                        [--save-profile NAME]

(For ARM devices like Raspberry Pi, replace x86_64 with arm64)

Reminder: --service values only resolve to something functional if you have already installed a matching extension (--service ext:tidal-web needs the tidal-web extension installed from a registry you configured). See Extensions.


API Reference

SpotiFLAC() Parameters

Parameter Type Default Description
url str / list[str] Required A single URL or a list of URLs (batch mode).
output_dir str Required The destination directory path where the audio files will be saved.
output_path str None Exact destination file path for single track downloads. Overrides output_dir + filename_format. Automatically ignored for albums, playlists and artist discographies.
services list ["ext:tidal-web"] Extensions to use and their priority order, as ext:<id> (or a legacy alias — see Extensions). Each id must correspond to an extension you have already installed; nothing is bundled or installed automatically.
registries list None One or more extension-registry JSON URLs to add before the run, as an alternative to SPOTIFLAC_REGISTRIES or a .env file. Must be https://. Persisted to ~/.spotiflac/registry_settings.json on first use, so subsequent runs (CLI, GUI, or Python) pick it up automatically without passing it again — see Extensions.
filename_format str "{title} - {artist}" Format for naming downloaded files. See placeholders below.
use_track_numbers bool False Prefixes the filename with the track number.
use_album_track_numbers bool False Uses the track's original album number instead of the download queue position.
use_artist_subfolders bool False Automatically organizes downloaded files into subfolders by artist.
use_album_subfolders bool False Automatically organizes downloaded files into subfolders by album.
create_playlist_subfolders bool False Creates a subfolder per playlist/album when downloading a collection, in addition to any artist/album subfolders.
first_artist_only bool False Uses only the first artist in tags and filename.
artist_separator str None Custom separator (e.g. ", " or " / ") to join multiple artists into a single string in tags, instead of using standard multi-value fields. Useful for players like Rekordbox.
include_featuring bool False When downloading an artist discography, also includes tracks where the artist appears as a featured artist.
max_concurrent_downloads int 2 How many tracks to download in parallel.
tidal_custom_api str None Optional setting forwarded to the installed tidal-web-family extension, if it supports it. Has no effect on its own — see Passing Settings to an Extension.
timeout_s int None Per-track download timeout in seconds. If a single track download does not complete within this time, the process is terminated and the track is marked as failed. SpotiFLAC then moves on to the next extension or retry. Set to None (default) to disable the timeout.
loop int None Duration in minutes to keep retrying permanently failed tracks after a full session completes.
track_max_retries int 0 Extra download attempts per track when all extensions fail on the first try. Each retry cycles through all configured extensions again with exponential backoff (2 s → 4 s → 8 s …, capped at 30 s).
quality str "LOSSLESS" Requested profile: LOSSLESS or HI_RES_LOSSLESS. DOLBY_ATMOS is also accepted but is Tidal-exclusive — any other provider falls back to HI_RES_LOSSLESS instead. Legacy provider-specific values are accepted and normalized.
allow_fallback bool True For HI_RES_LOSSLESS, allows fallback to LOSSLESS when the higher-resolution tier is unavailable. It never downgrades lossless requests to compressed audio.
log_level int logging.WARNING Python logging level.
embed_lyrics bool True Whether to fetch and embed synchronized lyrics (LRC) into the audio file.
lyrics_providers list ["spotify", "apple", "musixmatch", "lrclib", "amazon"] Priority order of lyrics providers to attempt.
enrich_metadata bool True Enables multi-provider metadata enrichment (HD covers, BPM, labels, etc.).
enrich_providers list ["deezer", "apple", "qobuz", "tidal"] Priority order of metadata providers to attempt. soundcloud is also accepted but isn't on by default.
qobuz_token str None Optional setting forwarded to the installed Qobuz extension, if it supports it. Has no built-in behavior of its own.
qobuz_local_api_url str None Optional setting forwarded to the installed qobuz-web-family extension, if it supports it. Has no effect on its own — see Passing Settings to an Extension.
use_extensions_fallback bool True Whether to automatically fall back to another installed extension for the same alias if one fails. Set to False to use only the extensions explicitly listed in services.
transcode_to str None Converts every finished track to this format. Currently only "mp3" (see MP3 Transcoding). None keeps the extension's original format. Requires ffmpeg.
transcode_bitrate str "320k" Bitrate used by transcode_to, e.g. "320k", "256k", "192k".
transcode_keep_original bool False Keeps the original lossless file next to the converted one. By default the source is deleted once the conversion succeeds.
verify_hires bool False Runs a spectral-analysis QA check after each successful lossless download, flagging files that declare a high sample rate but lack real content above standard-definition frequencies (possible upsampling / fake Hi-Res). Requires the optional librosa/numpy packages (pip install SpotiFLAC[hires]); silently skipped if they're not installed. Never fails or delays a download — see Hi-Res Verification.
post_download_action str "none" Action after all downloads finish: "none", "open_folder", "notify", "command".
post_download_command str "" Shell command to run when post_download_action="command". Supports {folder}, {succeeded}, {skipped}, {failed} placeholders; quote {folder} in your template (e.g. '{folder}') since the substituted path may contain spaces.

Filename Format Placeholders & Custom Formatting

String Template with Placeholders

When customizing the filename_format string, you can use the following dynamic tags:

  • {title} — Track title
  • {artist} — Track artist(s)
  • {album} — Album name
  • {album_artist} — The artist(s) of the entire album
  • {disc} — The disc number
  • {track} — The track's original number in the album
  • {position} — Download queue / playlist position (zero-padded, e.g. 01)
  • {date} — Full release date (e.g., YYYY-MM-DD)
  • {year} — Release year (e.g., YYYY)
  • {isrc} — Track ISRC code
  • {platform} — Source platform (e.g., "tidal", "soundcloud", "youtube") — the extension/service that provided the download
  • {id} — Platform-specific track ID (e.g., Tidal track ID, SoundCloud user ID, YouTube video ID) — useful for tracing a file back to its origin

Examples:

SpotiFLAC(
    url="https://open.spotify.com/track/...",
    output_dir="./downloads",
    services=["ext:tidal-web"],
    # Standard string template
    filename_format="{year} - {album}/{track}. {title}",
)

# Using platform and ID in the filename (flat):
SpotiFLAC(
    url="https://open.spotify.com/playlist/...",
    output_dir="./downloads",
    filename_format="{platform}_{album}_{title}",
)

Custom Function (Lambda) for Advanced Logic

For complex naming rules, pass a callable (function or lambda) instead of a string. The function receives:

  • metadata — the TrackMetadata object
  • platform — the source platform string (e.g., "tidal")
  • native_id — the platform-specific ID, or None if not available
  • **kwargs — additional context

The function must return a filename (without directory or extension — those are added automatically).

Example — use ISRC if available, fall back to platform_id:

from SpotiFLAC import SpotiFLAC

def my_filename_logic(metadata, platform, native_id, **kwargs):
    """
    Prioritize ISRC, then fall back to platform_id for traceability,
    then just use the title.
    """
    if metadata.isrc:
        return metadata.isrc
    
    if native_id:
        return f"{platform}_{native_id}"
    
    return metadata.title

SpotiFLAC(
    url="https://open.spotify.com/album/...",
    output_dir="./downloads",
    services=["ext:tidal-web", "ext:soundcloud-web"],
    filename_format=my_filename_logic,  # Pass the function directly
)

Example — include platform and year in filename (flat):

SpotiFLAC(
    url="https://open.spotify.com/playlist/...",
    output_dir="./downloads",
    filename_format=lambda metadata, platform, native_id, **kw: (
        f"{platform}_{metadata.year or 'unknown'}_{metadata.title}"
    ),
)

CLI Flag Reference

Flag Short Default Description
--service -s ext:tidal-web One or more extensions in priority order, as ext:<id> (or a legacy alias resolved to an installed extension — see Extensions).
--registries None An extension-registry JSON URL to add before running; repeat the flag for each one. Alternative to SPOTIFLAC_REGISTRIES or a .env file. Must be https://. Persisted to ~/.spotiflac/registry_settings.json, so you only need to pass it once — see Extensions.
--filename-format -f {title} - {artist} Filename template with placeholders.
--output-path -o None Exact output file path for single track downloads. Ignored for albums, playlists and discographies.
--quality -q LOSSLESS Requested profile: LOSSLESS or HI_RES_LOSSLESS. DOLBY_ATMOS is also accepted but is Tidal-exclusive — any other provider falls back to HI_RES_LOSSLESS instead. Legacy provider-specific values are accepted and normalized.
--use-track-numbers False Prefix filenames with track numbers.
--use-album-track-numbers False Use the track's original album number instead of queue position.
--use-artist-subfolders False Organize files into per-artist subfolders.
--use-album-subfolders False Organize files into per-album subfolders.
--playlist-subfolders True Create a subfolder for playlist downloads (enabled by default).
--no-playlist-subfolders Keep playlist downloads directly in the output directory instead of a subfolder.
--first-artist-only False Use only the first artist in tags and filename.
--artist-separator None Custom separator for joining multiple artists in tags (e.g. ", " or " / "). Useful for Rekordbox.
--include-featuring False Include tracks where the artist appears as a featured artist. Only applies to artist/discography URLs.
--qobuz-local-api None Optional setting forwarded to the installed Qobuz extension, if it supports it.
--tidal-api None Optional setting forwarded to the installed Tidal extension, if it supports it.
--timeout 180 Maximum seconds allowed for each provider attempt. If a track download stalls or takes longer than this limit, it is forcibly terminated and marked as failed, then SpotiFLAC moves to the next extension or retry.
--loop -l None Keep retrying permanently failed tracks every N minutes.
--watch None Re-run this exact command every N minutes, forever, instead of exiting after one pass. See Watch Mode.
--retries 0 Extra per-track download attempts on failure. Cycles through all configured extensions with exponential backoff.
--max-concurrent 2 How many tracks to download at once. Each track still tries its providers in order/fallback on its own — this only controls how many tracks run simultaneously. Use 1 for fully sequential downloads with no interleaved console output.
--playlist -p None Playlist URL to sync; repeat once per playlist. All tracks go to a single destination folder, shared tracks are downloaded once, and each playlist gets its own M3U file (see Multiple Playlists in One Folder).
--m3u m3u8 Playlist file written for each --playlist: m3u8, m3u or none. Rewritten only when its content changed.
--transcode none Convert every downloaded track to this format: none or mp3. Requires ffmpeg.
--mp3 Shorthand for --transcode mp3.
--transcode-bitrate 320k Bitrate used by --transcode, e.g. 320k, 256k, 192k.
--keep-original False Keep the original lossless file alongside the transcoded one.
--verify-hires False Runs a spectral-analysis QA check after each successful lossless download, flagging files that declare a high sample rate but lack real content above standard-definition frequencies (possible upsampling / fake Hi-Res). Requires the optional librosa/numpy packages (pip install SpotiFLAC[hires]); silently skipped if they're not installed. Never fails or delays a download — see Hi-Res Verification.
--verbose -v False Enable debug logging.
--no-lyrics False Disable lyrics embedding (lyrics are embedded by default).
--lyrics-providers apple lrclib Lyrics provider priority order (CLI default; the Python API default is spotify apple musixmatch lrclib amazon when lyrics_providers is left unset).
--no-enrich False Disable multi-provider metadata enrichment (enrichment is enabled by default).
--enrich-providers deezer apple qobuz tidal Metadata enrichment provider priority order. soundcloud is also accepted but isn't on by default.
--post-action none Action after all downloads finish: none, open_folder, notify, command.
--post-command "" Shell command for --post-action=command. Placeholders: {folder}, {succeeded}, {skipped}, {failed}; quote {folder} in your template (e.g. '{folder}') since the substituted path may contain spaces.
--profile None Load a saved profile. CLI flags override profile values.
--save-profile None Save current CLI configuration as a named profile after the run.
--gui False Launch the GUI as a native window (pywebview). See GUI Mode.
--web False Launch the same GUI as a local web server instead of a native window. See Web Mode.
--host 127.0.0.1 Host to bind --web to. Only change this deliberately — see the security note under Web Mode.
--port 8000 Port to bind --web to.
--web-token None Shared secret required on every --web request. Falls back to SPOTIFLAC_WEB_TOKEN. See Authentication.
--web-multiuser False Require per-account login for --web instead of/alongside --web-token. See Multi-user mode.
--web-user-add Create a --web-multiuser account: --web-user-add USERNAME PASSWORD.
--web-user-remove Delete a --web-multiuser account by username.
--web-user-list List configured --web-multiuser usernames.
--interactive False Launch the interactive step-by-step wizard. See Interactive Mode.
--registry-directories None A directory JSON URL to add before running (lists registries, not extensions — repeat once per URL). Alternative to SPOTIFLAC_REGISTRY_DIRECTORIES. See Extension Discovery.
--trust-key-add Trust a registry-signing public key: --trust-key-add NAME PUBLIC_KEY_B64. See Registry Trust.
--trust-key-remove Remove a trusted key by name.
--trust-key-list List trusted key names/public keys.
--ext-scaffold Generate a new extension skeleton: --ext-scaffold NAME [--runtime python|javascript] [--output-dir DIR]. See Developing Extensions.
--ext-dry-run Validate an extension (directory or packaged ZIP) without installing it or contacting any registry: --ext-dry-run PATH.

MusicBrainz Enrichment

SpotiFLAC automatically queries MusicBrainz in the background (when an ISRC is available) while the audio is being downloaded, adding professional-grade tags at no extra time cost. Fields written when found:

Tag Description
GENRE Genre
ORGANIZATION Record label
BPM Beats per minute
UPC Release barcode / UPC
ISRC Track ISRC code (normalized)
ITUNESADVISORY Set to 1 when the release is marked explicit

Download Validation

After each download, SpotiFLAC validates the file to detect common issues:

  • Preview detection — if the expected duration is ≥ 60 s but the downloaded file is ≤ 35 s, the file is deleted and the download is retried with the next extension.
  • Duration mismatch — for tracks longer than 90 s, a deviation greater than 25% (or 15 s minimum) from the expected duration is treated as a corrupt download and the file is removed.

Want to support the project?

If this software is useful and brings you value, consider supporting the project by buying us a coffee. Your support helps keep development going.

Ko-fi


API Credits

Song.link · MusicBrainz · LRCLIB · Musixmatch · iTunes Search API

Provider-specific credits (Tidal, Qobuz, Amazon Music, Deezer, SoundCloud, Apple Music, Pandora, and any third-party API used to reach them) now belong to whichever extension you install — see that extension's own documentation for its credits and terms.

[!TIP] Star the repo to show support, and click Watch → Custom → Releases on GitHub if you want to be notified as soon as a new release goes out.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spotiflac-3.5.0.tar.gz (904.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spotiflac-3.5.0-py3-none-any.whl (872.6 kB view details)

Uploaded Python 3

File details

Details for the file spotiflac-3.5.0.tar.gz.

File metadata

  • Download URL: spotiflac-3.5.0.tar.gz
  • Upload date:
  • Size: 904.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for spotiflac-3.5.0.tar.gz
Algorithm Hash digest
SHA256 c860615164ecf716378bec2a893c319a92bc4bd7e7dcadb8d09af68e6eab235f
MD5 19cae349f23086123e9038c3ddebf17f
BLAKE2b-256 bd30ef429bd067b7e0c67193cdab6addf42d934fb32c92d2f7ace1a442c593a3

See more details on using hashes here.

File details

Details for the file spotiflac-3.5.0-py3-none-any.whl.

File metadata

  • Download URL: spotiflac-3.5.0-py3-none-any.whl
  • Upload date:
  • Size: 872.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for spotiflac-3.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5cfe18f2834fa6cc69acf8e35872268d75a49af98907ea5c76fe0f4cc3613728
MD5 29eeeda831f4b3639fdfbb80fb2d5389
BLAKE2b-256 3f07ebe287901d3d80d82e4b594f803dd99e5e054577461bdef017c1e649996d

See more details on using hashes here.

Release history Release notifications | RSS feed

4.1.0

2 files

4.0.3

2 files

4.0.2

2 files

4.0.1

2 files

4.0.0

2 files

3.9.0

2 files

3.8.0

2 files

3.7.0

2 files

3.6.0

2 files

This release

3.5.0 This release

2 files

3.1.0

2 files

3.0.9

2 files

3.0.8

2 files

3.0.7

2 files

3.0.6

2 files

3.0.5

2 files

3.0.4

2 files

3.0.3

2 files

3.0.2

2 files

3.0.1

2 files

3.0.0

2 files

1.8.1

2 files

1.8.0

2 files

1.7.9

2 files

1.7.8

2 files

1.7.7

2 files

1.7.6

2 files

1.7.5

2 files

1.7.4

2 files

1.7.3

2 files

1.7.2

2 files

1.7.1

2 files

1.7.0

2 files

1.6.9

2 files

1.6.8

2 files

1.6.7

2 files

1.6.6

2 files

1.6.5

2 files

1.6.4

2 files

1.6.3

2 files

1.6.2

2 files

1.6.1

2 files

1.6.0

2 files

1.5.9

2 files

1.5.8

2 files

1.5.7

2 files

1.5.6

2 files

1.5.5

2 files

1.5.4

2 files

1.5.3

2 files

1.5.2

2 files

1.5.1

2 files

1.5.0

2 files

1.4.9

2 files

1.4.8

2 files

1.4.7

2 files

1.4.6

2 files

1.4.5

2 files

1.4.4

2 files

1.4.3

2 files

1.4.2

2 files

1.4.1

2 files

1.4.0

2 files

1.3.9

2 files

1.3.8

2 files

1.3.7

2 files

1.3.6

2 files

1.3.5

2 files

1.3.4

2 files

1.3.3

2 files

1.3.2

2 files

1.3.1

2 files

1.3.0

2 files

1.2.9

2 files

1.2.8

2 files

1.2.7

2 files

1.2.6

2 files

1.2.5

2 files

1.2.4

2 files

1.2.3

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.9

2 files

1.1.8

2 files

1.1.7

2 files

1.1.6

2 files

1.1.5

2 files

1.1.4

2 files

1.1.3

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.9

2 files

1.0.8

2 files

1.0.7

2 files

1.0.6

2 files

1.0.5

2 files

1.0.4

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

0.9.9

2 files

0.9.8

2 files

0.9.7

2 files

0.9.6

2 files

0.9.5

2 files

0.9.4

2 files

0.9.3

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.9

2 files

0.8.7

2 files

0.8.6

2 files

0.8.5

2 files

0.8.4

2 files

0.8.3

2 files

0.8.2

2 files

0.8.1

2 files

0.8.0

2 files

0.7.9

2 files

0.7.8

2 files

0.7.7

2 files

0.7.6

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.9

2 files

0.6.8

2 files

0.6.7

2 files

0.6.6

2 files

0.6.5

2 files

0.6.4

2 files

0.6.3

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.9

2 files

0.5.8

2 files

0.5.6

2 files

0.5.5

2 files

0.5.4

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.4.9

2 files

0.4.8

2 files

0.4.7

2 files

0.4.6

2 files

0.4.5

2 files

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.9

2 files

0.3.8

2 files

0.3.7

2 files

0.3.6

2 files

0.3.5

2 files

0.3.3

2 files

0.3.2

2 files

0.2.13

2 files

0.2.8

2 files

0.2.7

2 files

0.2.6

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page