Self-hosted service
sjdonado/idonthavespotify avatar
sjdonado/idonthavespotify

I Don't Have Spotify: music link conversion and self-hosting

Effortlessly convert music links between streaming services

1,916 stars56 forksTypeScriptMIT

At a glance

What is it?
I Don't Have Spotify parses a supported streaming-service link, searches destination catalogs, and returns matching track, album, artist, or podcast links. It suits self-hosters, browser users, and Raycast extension users, but catalog matching is not guaranteed across every service.
Who is it for?
Adopt I Don't Have Spotify if you want a self-hosted route from supported music and podcast links to alternative services, and you can accept incomplete matches and runtime-specific behavior. Do not adopt it as a guaranteed cross-platform catalog resolver or as an unauthenticated public API backed by the hosted deployment.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

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

Editorial analysis

Why I Don't Have Spotify separates conversion from matching

I Don't Have Spotify addresses a specific distribution problem: a listener has a link from one music or podcast service but wants a usable alternative on another service. It accepts source links for individual tracks, albums, artists, and podcasts. Playlists are outside the documented feature set, so a playlist link is not a supported conversion case. The returned card can include cover art, an audio preview when available, destination URLs, and a universal URL that lets another person choose a service rather than forcing one provider in the shared link.

The project is aimed at listeners who cross platform boundaries and operators who prefer a self-hosted service. It can run as a compiled Bun executable or a Docker container. There is also a Raycast extension, but its documentation says that it connects to a self-hosted instance because programmatic search is disabled on the public deployment. This makes the project useful as both a browser application and a small service behind private automation, rather than only as a public converter.

The main constraint appears in the output itself. Search coverage varies by service and content type, so IDHS does not promise that every conversion succeeds on every destination. A result has `isVerified` and `notAvailable` flags. The former records a strong match signal, while the latter can disable a low-confidence result. That design exposes uncertainty in the data model instead of presenting every URL as equally authoritative.

The parser, adapter, and fallback pipeline

Conversion is divided into parsers in `src/parsers/` and destination adapters in `src/adapters/`. The documented flow is:

text
Source link → parser → metadata and search query → adapters → destination links

A parser identifies the source platform and extracts normalized metadata such as title, artist, content type, cover art, and any available audio preview. It then creates a search query from those fields. Destination adapters do not need to understand each original URL format. This separation matters when a new source or destination is considered, because parsing and catalog matching can evolve independently.

The adapters search destination services and score candidate matches. Spotify uses the web player's internal GraphQL API rather than the official Web API. Its anonymous token refresh uses a TOTP flow that reads a secret from the player bundle, and no Spotify developer account is required for that path. YouTube Music uses YouTube search, while Apple Music, Deezer, SoundCloud, Qobuz, Bandcamp, Pandora, JioSaavn, and the podcast destinations use their documented lookup methods.

Tidal is the clearest exception. The project lacks the API entitlement needed for direct Tidal search and describes a bot wall on public pages, so MusicBrainz supplies streaming relations. For songs, it first tries to obtain an ISRC from a verified Deezer result, then falls back to title-and-artist search. The README says relation lookups are limited to about three seconds, which is a meaningful response-time constraint rather than a promise of completeness. Invidious is also indirect: its adapter maps a YouTube watch, playlist, or channel path to `https://redirect.invidious.io`, where the visitor selects a live instance.

Running the Bun development server on localhost

Local development requires Bun 1.4.3 or newer. The repository declares that engine requirement in `package.json`, and the README tells operators to create `.env` from the endpoint settings in `.env.test` and the variable reference in `docs/self-hosting.md`. Credentials should remain local. The YouTube adapter specifically requires `YOUTUBE_API_KEY`, available through Google's developer console.

Install the locked project dependencies with:

sh
bun install

Before starting the server, set the local application URL so universal share links point back to the development host. The README gives this value exactly:

sh
APP_URL=http://localhost:3000

That assignment sets a variable for the current shell. The server reads `.env` from its working directory, so the environment and secret file also need to be present where the application is launched.

Start both watched development tasks with:

sh
bun run dev

The `dev` script runs the browser asset watcher and server watcher in parallel. After the watchers report that the server and assets are ready, open `http://localhost:3000`. A first real check is to submit one supported source URL, such as an individual track link, and inspect the result card for destination links and preview data. A playlist should not be used to judge basic support because the README explicitly excludes playlists.

Building the standalone executable and Docker container

The supported self-hosting path is a single compiled executable. It serves HTML, API responses, and embedded static assets without a separate `public/` runtime directory. Browser assets must be built before compiling the server:

sh
bun run build
bun run build:prod
NODE_ENV=production ./dist/idonthavespotify

The default port is 3000, and `PORT` changes it. Self-hosted instances leave browser and API search enabled unless the operator sets `DISABLE_API_SEARCH=true`. Disabling API search rejects `/api/search` with HTTP 403 before upstream calls, but it does not disable browser searches or shared pages. This differs from the public Worker, whose Wrangler configuration enables the restriction explicitly.

The Docker image performs the build in `oven/bun:1.4.3-alpine`, then copies only the compiled executable into `alpine:3.20`. Its runtime exposes port 3000 and starts `idonthavespotify`. The Compose file expects a local `.env` and maps container port 3000 to host port 3000.

There is an operational trap in the build context. Before running a Docker build, exclude secret environment files because the current `.dockerignore` excludes only local variants. The application also has no per-IP rate limiter. The README recommends a rate-limiting reverse proxy or Cloudflare Bot Fight Mode with a Managed Challenge rule for a public instance. Per-service budgets and circuit breakers limit upstream traffic, but the project states that they do not replace edge protection.

Server rendering, htmx fragments, and Workers behavior

The web layer combines nano-jsx, htmx, Stimulus, Howler, and Tailwind-related assets. The server renders the initial HTML. htmx then requests updated result fragments, while Stimulus handles preview playback, sharing, and error behavior. This is a server-centered design rather than a separate SPA with its own application shell.

Universal links use the form `APP_URL?id=<id>`. A shared page returns source metadata first, which allows link-preview crawlers to see useful HTML before the card is populated. Destination links load in a second request. That two-stage flow can improve the appearance of shared previews, but it also means an initially visible card is not necessarily the final set of destinations. A crawler or browser client must account for the subsequent fragment request.

The public deployment on Cloudflare Workers differs from the compiled server. Workers use platform `fetch`, not the self-hosted TLS-impersonating HTTP transport, and their cache is held in memory per isolate. Apple Music on Workers resolves through catalog pages without an audio preview. Bot protection introduces a separate navigation path: because a challenge cannot be completed inside an htmx request, the application navigates to `/verify`, returns to the originating page, and resumes the search.

For programmatic use, `API.md` is the relevant contract. The public `/api/search` endpoint is disabled, so automation consumers need a self-hosted deployment with programmatic search available. Retired email-login endpoints still return HTTP 410, but their implementation and quota storage have been removed. Clients should not treat those compatibility responses as a functioning authentication system.

Matching limits, maintenance signals, and the MIT boundary

The first limitation is media coverage. IDHS supports tracks, albums, artists, and podcasts, but not playlists. The second is destination coverage, because lookup support differs between services and content types. The third is confidence: low-scoring results can be marked unavailable rather than shown, while some fallbacks, such as Tidal through MusicBrainz, depend on external identifiers and time-sensitive search behavior.

Spotify conversion is also tied to an internal web-player GraphQL API rather than the official Web API. The README documents automatic anonymous token refresh through a TOTP flow, but it gives no broader compatibility guarantee for future changes in that player. Service behavior may therefore affect conversion independently of changes in the IDHS code.

The repository is not archived, and the last push was on 2026-10-15. The listed recent releases include `v1.9.0`, `1.8.3`, and `1.8.0`, while `package.json` declares version `1.10.0`. That difference makes release selection less obvious than a single consistent version history. Operators should confirm which tag or package state they intend to deploy rather than assuming the newest visible release label matches the current source version.

The repository is licensed under MIT. That permits broad use, modification, distribution, and commercial use subject to the license conditions, while requiring preservation of its copyright and permission notice. The license does not grant rights to third-party services, APIs, music metadata, cover art, or audio previews. Those external dependencies and service terms remain separate adoption constraints.

Editorial conclusion

Adopt I Don't Have Spotify if you want a self-hosted route from supported music and podcast links to alternative services, and you can accept incomplete matches and runtime-specific behavior. Do not adopt it as a guaranteed cross-platform catalog resolver or as an unauthenticated public API backed by the hosted deployment. Before adoption, verify `API.md`, the required credentials in `docs/self-hosting.md`, the `.dockerignore` handling for secret files, and whether MusicBrainz fallback latency fits your conversion routes.

Frequently asked questions

Does I Don't Have Spotify convert playlists?

No. The README states that individual tracks, albums, artists, and podcasts are supported, but playlists are not.

Can I use the I Don't Have Spotify search API on the public instance?

No. With `DISABLE_API_SEARCH=true`, the public `/api/search` endpoint returns HTTP 403 without calling upstream services. Programmatic clients need a self-hosted instance with API search enabled.

What must I configure before running I Don't Have Spotify locally?

Install Bun 1.4.3 or newer, run the dependency installation, create `.env` from the documented endpoint references, and supply the required credentials. The YouTube adapter needs `YOUTUBE_API_KEY`, and local share links use `APP_URL=http://localhost:3000`.

Why might an I Don't Have Spotify conversion fail on Tidal?

IDHS has no direct Tidal search adapter because the required API entitlement is unavailable and public pages have a bot wall. It uses MusicBrainz fallback, which may not find a relation within the documented lookup window.

Official sources

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

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

Add this badge to your README

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

Was this useful?