sung.playlists

Build Spotify playlists from human-friendly song descriptors.

Given a list of “song descriptors” — things like "Clocks", "Clocks - Coldplay", ("Believer", "Imagine Dragons"), or {"name": "Fix You", "artist": "Coldplay"} — resolve each to a Spotify track and create a playlist in one call.

Example:

>>> from sung.playlists import playlist_from_songs
>>> playlist, report = playlist_from_songs(  
...     [
...         ("Clocks", "Coldplay"),
...         "Radioactive - Imagine Dragons",
...         {"name": "Believer", "artist": "Imagine Dragons"},
...     ],
...     playlist_name="My Mix",
... )
>>> playlist.playlist_url  
'https://open.spotify.com/playlist/...'

The lower-level resolve_song() returns the chosen match plus the full ranked candidate list, which the CLI uses to surface ambiguous matches.

class sung.playlists.SongMatch(descriptor: ~typing.Any, query_name: str, query_artist: str | None, track_id: str | None, track_name: str | None, artist_names: list = <factory>, album_name: str | None = None, popularity: int | None = None, score: float = 0.0, candidates: list = <factory>, ambiguous: bool = False, not_found: bool = False)[source]

Result of resolving a single song descriptor.

sung.playlists.parse_song_descriptor(descriptor: str | tuple | dict) tuple[str, str | None][source]

Parse a song descriptor into (name, artist_or_None).

Accepts:

  • a plain string: "Clocks"

  • "Title - Artist" or "Title Artist" or "Title by Artist"

  • a 2-tuple/list (name, artist)

  • a dict with name/title and optional artist/artists

>>> parse_song_descriptor("Clocks")
('Clocks', None)
>>> parse_song_descriptor("Clocks - Coldplay")
('Clocks', 'Coldplay')
>>> parse_song_descriptor("Clocks by Coldplay")
('Clocks', 'Coldplay')
>>> parse_song_descriptor(("Believer", "Imagine Dragons"))
('Believer', 'Imagine Dragons')
>>> parse_song_descriptor({"name": "Fix You", "artist": "Coldplay"})
('Fix You', 'Coldplay')
sung.playlists.playlist_from_songs(descriptors: Iterable[str | tuple | dict], playlist_name: str = 'New Playlist', *, public: bool = True, market: str | None = None, search_limit: int = 10, skip_missing: bool = True, client: Any | None = None) tuple[Playlist | None, list[SongMatch]][source]

Search for each song, then create a playlist with the resolved tracks.

Returns (playlist, matches). playlist is None if no songs resolved. matches is the list of SongMatch results — one per descriptor — including any that were not_found or ambiguous.

Use skip_missing=False to raise instead of silently dropping descriptors that could not be resolved.

sung.playlists.resolve_song(descriptor: str | tuple | dict, *, market: str | None = None, search_limit: int = 10, ambiguous_score_gap: float = 10.0, client: Any | None = None) SongMatch[source]

Resolve one song descriptor to a Spotify track via search + ranking.

Returns a SongMatch with the best candidate selected. The match is flagged ambiguous when the top two candidates score within ambiguous_score_gap of each other (callers may want to confirm).

sung.playlists.resolve_songs(descriptors: Iterable[str | tuple | dict], *, market: str | None = None, search_limit: int = 10, client: Any | None = None) list[SongMatch][source]

Resolve a list of descriptors. See resolve_song().