Find the streaming link of any track across every major platform — in one line of Python.
Spotify, Apple Music, Deezer, YouTube Music, Qobuz, SoundCloud — one call, every link.
from tunefinder import find_links
find_links("9Lana", "Balalaika")
# {
# "spotify": "https://open.spotify.com/track/...",
# "appleMusic": "https://music.apple.com/...",
# "deezer": "https://www.deezer.com/track/...",
# "youtubeMusic": "https://music.youtube.com/watch?v=...",
# "qobuz": "https://www.qobuz.com/fr-fr/album/...?track_id=...",
# "soundcloud": "https://soundcloud.com/9lana/aovo7ub0aqee",
# }That's it. No API keys, no OAuth, no manual setup — tunefinder searches
DuckDuckGo with platform-specific filters and a scoring system that picks
the original version of a track over remixes, covers and acoustic edits.
- 🎯 Smart version scoring — penalises acoustic / remix / live results when you didn't ask for them
- 🌍 Multi-region fallback — iterates DuckDuckGo regions until a perfect-score match is found
- 🪶 One dependency — just
ddgs, no auth, no API keys - 🔌 Six platforms — Spotify, Apple Music, Deezer, YouTube Music, Qobuz, SoundCloud
- 🧠 Deduplicated results — same URL with multiple snippets keeps only the best-scoring one
- 🐛 Inspectable —
find_data()returns every candidate with scores,print_search_debug()traces the search - 🧪 Typed —
mypy --strictclean and ships PEP 561py.typed
pip install tunefinderOr, to pin to a major version (recommended for production):
pip install "tunefinder>=1.0,<2.0"Installing from source (development or unreleased commits):
pip install git+https://github.com/LouisCourrian/tunefinder.gitfrom tunefinder import find_links
links = find_links("Stromae", "Alors on danse")
print(links["spotify"])
# → https://open.spotify.com/track/...Limit to specific platforms:
find_links("Stromae", "Alors on danse", platforms=["spotify", "deezer"])For audit, JSON export or UI integration, use find_data — it returns
every candidate ranked by score:
import json
from tunefinder import find_data
data = find_data("9Lana", "Balalaika")
print(json.dumps(data, indent=2, ensure_ascii=False)){
"artist": "9Lana",
"title": "Balalaika",
"requested_markers": [],
"platforms": {
"spotify": [
{
"url": "https://open.spotify.com/track/abc",
"score": 100,
"region": "wt-wt",
"result_title": "Balalaika - Single by 9Lana | Spotify",
"result_description": "Listen to Balalaika on Spotify...",
"markers_detected": []
}
],
"deezer": []
}
}The first candidate of each platform list is the one find_links would
return. An empty list means nothing was found for that platform.
from tunefinder import print_search_debug
print_search_debug("9Lana", "Balalaika")Prints the selected candidate plus all alternatives, with their score, region, description, and detected markers (acoustic, live, remix…).
from tunefinder import Config, find_links
config = Config(
regions=("fr-fr", "us-en"), # only these two regions
delay_between_queries=0.5, # be more polite to DuckDuckGo
score_marker_unwanted=-80, # stronger penalty for unwanted versions
)
find_links("Stromae", "Alors on danse", config=config)All Config fields are documented in Config.__doc__.
tunefinder also exposes a small command-line interface — handy for
shell scripts, smoke tests, or one-off lookups without writing Python:
# JSON dict on stdout (compact for piping, indented when stdout is a TTY)
tunefinder "9Lana" "Balalaika"
# Restrict to specific platforms
tunefinder "9Lana" "Balalaika" --platforms spotify deezer
# Full audit: every candidate with scores, as JSON
tunefinder "9Lana" "Balalaika" --data
# Human-readable trace: which candidate won and why
tunefinder "9Lana" "Balalaika" --debug
# Tune the search
tunefinder "Stromae" "Alors on danse" --regions fr-fr us-en --delay 0.5It also runs as python -m tunefinder ... if the entry point is not on
your PATH (useful in unactivated virtual environments).
Run tunefinder --help for the full reference.
DuckDuckGo (and any search engine) returns multiple versions of the same
track: studio, acoustic, live, remixes, covers, slowed/sped-up edits…
A naive site: search picks the first match, which often isn't the one
you want.
tunefinder solves this with a small scoring system:
- Detects whether you asked for a specific version (
"Title - Acoustic"). - Penalises results containing version markers you didn't ask for.
- Bonifies results that match the version markers you did ask for.
- Deduplicates URLs by keeping the most informative snippet per URL.
- Tries multiple DuckDuckGo regions until a perfect match is found.
| Platform | Key in dict | Notes |
|---|---|---|
| Spotify | spotify |
— |
| Apple Music | appleMusic |
— |
| Deezer | deezer |
— |
| YouTube Music | youtubeMusic |
Indexation uneven on DuckDuckGo, some tracks won't surface. |
| Qobuz | qobuz |
Track URL = album page + ?track_id=N query string. |
| SoundCloud | soundcloud |
URLs are /<artist>/<track-slug> — playlists are excluded. |
These services were considered but cannot be supported reliably with DuckDuckGo as a search backend:
| Platform | Why it isn't supported |
|---|---|
| Tidal | Tidal track pages (tidal.com/browse/track/...) are not indexed by DuckDuckGo — no results to score. |
| Amazon Music | Track pages are largely JS-rendered or behind a login wall; DuckDuckGo indexation is poor even when querying every regional TLD via site:A OR site:B. |
| Napster | Since the rebrand, app.napster.com track pages are largely behind a login wall and have very weak SEO indexation. |
If indexation improves for any of these, adding them is a one-entry
change in src/tunefinder/_platforms.py
plus URL patterns in tests/test_platforms.py.
For each requested platform, tunefinder:
- Builds a
site:<domain> "<artist>" "<title>"query. - Sends it to DuckDuckGo via the
ddgspackage, iterating through a list of regions. - Filters returned URLs against a per-platform regex (
spotify.com/track/..., etc.). - Scores each candidate against the requested artist + title and any version markers you asked for.
- Returns the top-scoring URL — and short-circuits as soon as a perfect match is found in any region.
The public API has a small, stable contract — locked at 1.0 and protected by Semantic Versioning afterwards:
- Empty / whitespace / non-string
artistortitle→ValueErrorraised immediately, before any DDGS call. - Unknown platform name in
platforms=[...]→ValueError. - DDGS errors (rate-limit, timeout, network failure, "no results")
→ logged at
WARNINGlevel on thetunefinder._searchlogger; the affected platform simply does not appear in the returned dict (find_links) or has an empty candidate list (find_data). The call never propagates aDDGSExceptionto the caller — partial results are preferred over hard failures.
If you need to react to DDGS warnings, configure logging:
import logging
logging.getLogger("tunefinder._search").setLevel(logging.WARNING)
logging.basicConfig()- DuckDuckGo may rate-limit or change its HTML at any time, which can break the library until updated.
- Searching many tracks back-to-back (dozens or more) will eventually
trigger rate limits. Increase
delay_between_queriesor cache results in your own application. - YouTube Music indexing on DuckDuckGo is uneven. Some official tracks may not surface in the results even though they exist.
- This is not affiliated with Spotify, Apple, Deezer, YouTube, Qobuz, SoundCloud, or DuckDuckGo. All trademarks belong to their respective owners.
git clone https://github.com/LouisCourrian/tunefinder
cd tunefinder
pip install -e ".[dev]"
pytesttunefinder is stable as of 1.0.0. The public API — find_links,
find_data, print_search_debug, Config, PLATFORMS — follows
Semantic Versioning. Pin to a major version
(tunefinder>=1.0,<2.0) and you're good.
See CHANGELOG.md for the full release history and the versioning policy.
- Real package metadata in
pyproject.toml(no more placeholders). - PEP 561
py.typedmarker — type annotations exposed to consumer type checkers (mypy, pyright, …). - GitHub Actions CI —
ruff+mypy --strict+pyteston Python 3.10–3.13 for every push and PR onmain.
- Automated GitHub releases — pushing a
v*.*.*tag publishes a release whose body is extracted fromCHANGELOG.md. - CLI —
tunefinder ARTIST TITLE(orpython -m tunefinder ...) with--data/--debug/--platforms/--regions/--delay/--prettyflags. Output is JSON by default (compact for piping, indented on a TTY).
- Input validation — empty / whitespace / non-string
artistortitleraisesValueErrorbefore any DDGS call. CLI surfaces it as a clean one-line error. - Retry / backoff on rate-limit —
RatelimitExceptionandTimeoutExceptionare retried with exponential backoff. Non- transient errors (e.g. "no results found") are not retried. - Concurrent platform search —
ThreadPoolExecutorruns the 6 platforms in parallel. A full lookup drops from ~20 s to roughly the slowest single platform. - Error contract documented — explicit section in the README
and
find_links' docstring. Locked under SemVer. -
Development Status :: 5 - Production/Stable+ tagv1.0.0.
- Optional in-process TTL cache via
Config(cache_ttl_seconds=...). -
asyncvariant for FastAPI / Starlette consumers. - Native API fallback for Apple Music (iTunes Search) and Deezer (api.deezer.com) — free, no key, more reliable than DDGS for those two.
MIT — see LICENSE.
tunefinder is an independent project, not affiliated with Spotify,
Apple Music, Deezer, YouTube Music, Qobuz, SoundCloud, or DuckDuckGo.
All trademarks belong to their respective owners.