Skip to main content

Find streaming links (Spotify, Apple Music, Deezer, YouTube Music, Qobuz, SoundCloud) for any track.

Project description

🎵 tunefinder

Find the streaming link of any track across every major platform — in one line of Python.

CI Latest release Python versions License: MIT Tests

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.

✨ Features

  • 🎯 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
  • 🐛 Inspectablefind_data() returns every candidate with scores, print_search_debug() traces the search
  • 🧪 Typedmypy --strict clean and ships PEP 561 py.typed

📦 Installation

pip install git+https://github.com/LouisCourrian/tunefinder.git

🚀 Quick start

Find a link

from 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"])

Get all candidates with scores

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.

Debug a tricky result

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…).

Tune the search

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__.

🛠️ CLI

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.5

It 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.

🎯 Why not just search?

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.

🎚️ Supported platforms

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.

🚫 Unsupported platforms

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.

🔍 How does it actually work?

For each requested platform, tunefinder:

  1. Builds a site:<domain> "<artist>" "<title>" query.
  2. Sends it to DuckDuckGo via the ddgs package, iterating through a list of regions.
  3. Filters returned URLs against a per-platform regex (spotify.com/track/..., etc.).
  4. Scores each candidate against the requested artist + title and any version markers you asked for.
  5. Returns the top-scoring URL — and short-circuits as soon as a perfect match is found in any region.

📜 Error contract

The public API has a small, stable contract — locked at 1.0 and protected by Semantic Versioning afterwards:

  • Empty / whitespace / non-string artist or titleValueError raised immediately, before any DDGS call.
  • Unknown platform name in platforms=[...]ValueError.
  • DDGS errors (rate-limit, timeout, network failure, "no results") → logged at WARNING level on the tunefinder._search logger; 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 a DDGSException to 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()

⚠️ Limitations

  • 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_queries or 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.

🧪 Try it

git clone https://github.com/LouisCourrian/tunefinder
cd tunefinder
pip install -e ".[dev]"
pytest

✅ Status

tunefinder 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.

🗺️ Release history

Done in 0.3

  • Real package metadata in pyproject.toml (no more placeholders).
  • PEP 561 py.typed marker — type annotations exposed to consumer type checkers (mypy, pyright, …).
  • GitHub Actions CIruff + mypy --strict + pytest on Python 3.10–3.13 for every push and PR on main.

Done in 0.4

  • Automated GitHub releases — pushing a v*.*.* tag publishes a release whose body is extracted from CHANGELOG.md.
  • CLItunefinder ARTIST TITLE (or python -m tunefinder ...) with --data / --debug / --platforms / --regions / --delay / --pretty flags. Output is JSON by default (compact for piping, indented on a TTY).

Done in 1.0

  • Input validation — empty / whitespace / non-string artist or title raises ValueError before any DDGS call. CLI surfaces it as a clean one-line error.
  • Retry / backoff on rate-limitRatelimitException and TimeoutException are retried with exponential backoff. Non- transient errors (e.g. "no results found") are not retried.
  • Concurrent platform searchThreadPoolExecutor runs 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 + tag v1.0.0.

Considered for later

  • Optional in-process TTL cache via Config(cache_ttl_seconds=...).
  • async variant 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.

📄 License

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.

Project details


Download files

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

Source Distribution

tunefinder-1.0.0.tar.gz (24.5 kB view details)

Uploaded Source

Built Distribution

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

tunefinder-1.0.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file tunefinder-1.0.0.tar.gz.

File metadata

  • Download URL: tunefinder-1.0.0.tar.gz
  • Upload date:
  • Size: 24.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunefinder-1.0.0.tar.gz
Algorithm Hash digest
SHA256 c645b5bde0deee6d24f1eac60e077c17b138254155d90400cda165b24428e7d6
MD5 a56d215143776006a829060d0676dd9c
BLAKE2b-256 4df7d23bc6bdbdb8c82819a27ede5718d5010e1dc4586119ba2caa916bed16a7

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunefinder-1.0.0.tar.gz:

Publisher: release.yml on LouisCourrian/tunefinder

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tunefinder-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: tunefinder-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 20.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunefinder-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b625d5ed1498016e2c0c4ede6da7a5098c7eadf227af3d61d8cd0b64da21f367
MD5 29aa80a57e9162bb465524a771bf7881
BLAKE2b-256 b233afb3a7d9d9c5203e8024701ed8b2ea38ab5bfa66ac0968a8d8068dd64daf

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunefinder-1.0.0-py3-none-any.whl:

Publisher: release.yml on LouisCourrian/tunefinder

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page