Skip to main content

Slipmat Music Link Converter

A Python library for converting music streaming service links between platforms. Supports bidirectional conversion between Spotify, Apple Music, Tidal, and YouTube Music for songs, albums, and artists.

Features

  • Supported Services: Apple Music, Spotify, Tidal, and YouTube Music
  • Supported Item Types: Songs (Tracks), Albums, Artists
  • Bidirectional Conversion: Convert from any supported service to any other configured service
  • Asynchronous: Built with asyncio for efficient I/O operations
  • Smart Caching: Bounded in-memory LRU cache per converter, keyed by normalized URL
  • Smart YouTube Filtering: Automatically filters out non-music YouTube videos
  • URL Validation: is_song method quickly validates if a URL points to music content
  • Error Handling: Includes specific exceptions for common issues

Installation

Requires Python 3.14 or newer.

uv add slipmat-mlc

Configuration

The library requires API credentials for each service you want to use. These are loaded from environment variables.

  1. Create an .env file in your project root.
  2. Add credentials for the services you want to use:
# Apple Music
APPLE_MUSIC_TEAM_ID="your_apple_developer_team_id"
APPLE_MUSIC_KEY_ID="your_apple_music_key_id"
# The secret key should be the base64-encoded content of your .p8 file:
# uv run python -c "import base64; print(base64.b64encode(open('AuthKey_YOUR_KEY_ID.p8', 'rb').read()).decode())"
APPLE_MUSIC_SECRET_KEY="your_base64_encoded_private_key_content"

# Spotify
SPOTIFY_CLIENT_ID="your_spotify_client_id"
SPOTIFY_CLIENT_SECRET="your_spotify_client_secret"

# Tidal
TIDAL_CLIENT_ID="foo"
TIDAL_CLIENT_SECRET="foo"

# YouTube Music
YOUTUBE_API_KEY="your_youtube_data_api_v3_key"

Basic Usage

import asyncio
from mlc import MusicLinkConverter, Config, Service

async def main():
    # Load configuration
    config = Config.from_env()

    # Create converter
    converter = MusicLinkConverter.create(
        config=config,
        services=[Service.APPLE_MUSIC, Service.SPOTIFY, Service.TIDAL, Service.YOUTUBE_MUSIC]
    )

    # Convert a Spotify track URL
    url = "https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT"
    result = await converter.convert(url)

    # Access the results
    print(f"Original URL: {result.original_url}")
    print(f"Normalized URL: {result.normalized_source_url}")
    print(f"Title: {result.metadata.title}")
    print(f"Artists: {', '.join(result.metadata.artists)}")

    # Alternative links on other services
    for alt in result.alternatives:
        print(f"{alt.service.value}: {alt.url} (Confidence: {alt.confidence:.2f})")

if __name__ == "__main__":
    asyncio.run(main())

Advanced Usage

Resource Lifecycle

If you do not pass your own httpx.AsyncClient, the converter creates and owns one. Close it with await converter.close() or use the async context manager:

import asyncio
from mlc import MusicLinkConverter, Config, Service

async def main():
    config = Config.from_env()
    async with MusicLinkConverter.create(
        config=config,
        services=[Service.SPOTIFY, Service.APPLE_MUSIC, Service.YOUTUBE_MUSIC],
    ) as converter:
        result = await converter.convert("https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT")
        print(result.metadata.title)

asyncio.run(main())

If you pass a client to create(..., client=...), you own its lifecycle and should close it yourself.

Converting Multiple URLs Concurrently

import asyncio
from mlc import MusicLinkConverter, Config, Service

async def convert_multiple(urls: list[str]):
    config = Config.from_env()
    converter = MusicLinkConverter.create(
        config=config,
        services=[Service.SPOTIFY, Service.APPLE_MUSIC, Service.YOUTUBE_MUSIC]
    )

    # Convert URLs concurrently
    results = await asyncio.gather(
        *(converter.convert(url) for url in urls),
        return_exceptions=True
    )

    for url, result in zip(urls, results):
        if isinstance(result, Exception):
            print(f"Error converting {url}: {result}")
        else:
            print(f"{result.metadata.title} by {', '.join(result.metadata.artists)}")
            print(f"  Alternatives: {len(result.alternatives)}")

# Example usage
urls = [
    "https://open.spotify.com/track/0pakiWeYJcqrqka4SAaqa6",
    "https://music.apple.com/us/album/tulivuoria/368880626?i=368880680",
    "https://music.youtube.com/watch?v=zUSeGUsY1zk",
]
asyncio.run(convert_multiple(urls))

Validating Music URLs

Check if a URL points to valid music content. For YouTube, this verifies the video is actually a music video.

import asyncio
from mlc import MusicLinkConverter, Config, Service

async def validate_urls(urls: list[str]):
    config = Config.from_env()
    converter = MusicLinkConverter.create(
        config=config,
        services=[Service.SPOTIFY, Service.APPLE_MUSIC, Service.YOUTUBE_MUSIC]
    )

    for url in urls:
        is_valid = await converter.is_song(url)
        status = "✓ Valid music" if is_valid else "✗ Not music"
        print(f"{url}: {status}")

urls = [
    "https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT",  # Valid track
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",  # Music video
    "https://www.youtube.com/watch?v=someRandomVideo",  # Non-music video
]
asyncio.run(validate_urls(urls))

Caching Behavior

  • Cache is per converter instance and stored in memory only.
  • Entries are keyed by the normalized source URL and use an LRU policy (default max 128).
  • Successful metadata fetches are cached and reused across is_song() and convert().
  • Non-music YouTube results are cached as negative entries to avoid repeat API calls.

Validation and Non-Music Content

  • MusicItemMetadata enforces strict validation; missing values are None (never placeholder URLs).
  • For non-music YouTube videos, is_song() returns False, and convert() returns:
    • metadata.title == "Non-music content"
    • metadata.artists == []
    • alternatives == []
  • Unsupported URLs raise UnsupportedUrlError in convert() and return False in is_song().

Error Handling

from mlc import MusicLinkConverter, Config, Service
from mlc.exceptions import (
    MusicConverterError,
    UnsupportedUrlError
)

async def safe_convert(converter, url):
    try:
        result = await converter.convert(url)
        return result
    except UnsupportedUrlError:
        print(f"URL not supported: {url}")
    except MusicConverterError as e:
        print(f"Conversion error: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")
    return None

API Reference

Core Classes

  • MusicLinkConverter

    • create(config, services, client=None, logger=None) - Create a converter instance
    • async close() - Close the owned HTTP client (no-op if a client was provided)
    • async convert(url) - Convert a music URL to other services
    • async find_all_matches(url) - Fetch complete metadata-backed matches and per-service outcomes
    • async is_song(url) - Check if URL points to valid music content
    • async validate_and_normalize_url(url) - Validate and normalize a URL
  • Config

    • from_env(path=None) - Load configuration from environment variables

Models

  • Service - Enum of supported services: SPOTIFY, APPLE_MUSIC, TIDAL, YOUTUBE_MUSIC
  • ItemType - Enum of item types: SONG, ALBUM, ARTIST
  • ConversionResult - Result from convert():
    • original_url: str - The URL as provided to convert()
    • normalized_source_url: str - Normalized version of the source URL
    • metadata: MusicItemMetadata - Item metadata
    • alternatives: list[ServiceLink] - Links on other services
  • UnifiedConversionResult - Result from find_all_matches():
    • query_url: str - The URL as provided to find_all_matches()
    • matches: list[ServiceMatch] - Source + matched items with full metadata
    • outcomes: list[ServiceLookupOutcome] - Per-service status (SOURCE, MATCHED, NOT_FOUND, SEARCH_ERROR, METADATA_ERROR)
  • MusicItemMetadata - Standardized metadata:
    • title: str
    • artists: list[str]
    • item_type: ItemType
    • album_title: str | None
    • album_id: str | None
    • release_year: int | None
    • duration_s: int | None
    • image_url: str | None
    • isrc: str | None - For songs
    • upc: str | None - For albums
  • ServiceLink - Alternative link:
    • service: Service
    • url: str
    • item_type: ItemType
    • item_id: str
    • confidence: float - Match confidence score

Utilities

  • configure_logging(level) - Set structlog log level ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
  • get_logger(name="mlc") - Get a structlog logger

Supported URL Formats

Spotify

  • https://open.spotify.com/track/{id}
  • https://open.spotify.com/album/{id}
  • https://open.spotify.com/artist/{id}
  • https://open.spotify.com/intl-{locale}/track/{id} (and album/artist variants)

Apple Music

  • https://music.apple.com/{locale}/album/{name}/{id}?i={song_id} (Song)
  • https://music.apple.com/{locale}/album/{name}/{id} (Album)
  • https://music.apple.com/{locale}/artist/{name}/{id} (Artist)
  • https://embed.music.apple.com/{locale}/album/{name}/{id} (Album)
  • https://embed.music.apple.com/{locale}/album/{name}/{id}?i={song_id} (Song)
  • https://embed.music.apple.com/{locale}/artist/{name}/{id} (Artist)

YouTube Music / YouTube

  • https://music.youtube.com/watch?v={id} (Song)
  • https://www.youtube.com/watch?v={id} (Song - must be music video)
  • https://music.youtube.com/playlist?list={id} (Album)
  • https://www.youtube.com/playlist?list={id} (Album)
  • https://music.youtube.com/channel/{id} (Artist)
  • https://www.youtube.com/channel/{id} (Artist)
  • https://music.youtube.com/browse/{id} (Artist, UC... IDs)

Tidal

  • https://tidal.com/track/{id}
  • https://tidal.com/album/{id}
  • https://tidal.com/artist/{id}
  • https://listen.tidal.com/browse/track/{id}
  • https://listen.tidal.com/browse/album/{id}
  • https://listen.tidal.com/browse/artist/{id}
  • https://listen.tidal.com/browse/album/{album_id}/track/{track_id}

Development

Running Tests

uv run pytest

Code Quality

uv run ruff format .
uv run ruff check --fix --extend-fixable F401 .
uv run ty check

Adding a New Service

To add support for a new streaming service:

  1. Add the service to ServiceName enum in src/mlc/datamodels.py
  2. Create a configuration class in src/mlc/config.py
  3. Implement the service adapter in src/mlc/adapters/
  4. Update the factory in src/mlc/adapters/factory.py
  5. Add tests for your implementation
  6. Update documentation

See existing adapters for implementation patterns and best practices.

Release files for slipmat-mlc 2026.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for slipmat-mlc 2026.7
File Size Uploaded
slipmat_mlc-2026.7.tar.gz 57.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for slipmat-mlc 2026.7
File Interpreter ABI Platform
slipmat_mlc-2026.7-py3-none-any.whl Python 3 none any Details

Total release size: 130.7 kB

Release files / slipmat_mlc-2026.7.tar.gz

Download URL slipmat_mlc-2026.7.tar.gz
Size 57.0 kB
Tags Source
SHA-256 checksum
How to use checksums
45a8e6ab423f19a36286f476c1a54a1ac9cef4c68161fd2da4d34a7884cffa76
BLAKE2b-256 checksum
How to use checksums
f1c3eed54e70bc6014f12206d5eaf97d2016cc58fa9ca379bb39dbd5a82caa0e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / slipmat_mlc-2026.7-py3-none-any.whl

Download URL slipmat_mlc-2026.7-py3-none-any.whl
Size 73.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
097ed45d83a6ee3d6970c7424e1c1e30c4e114cafdea852a44ec4324c6acdeae
BLAKE2b-256 checksum
How to use checksums
abaf3b9540a9a6232e8aa5bf6bb1b411271618cd438e42c23818a757ad74c62d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

2026.7 This release

2 release 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