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/titleand optionalartist/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).playlistisNoneif no songs resolved.matchesis the list ofSongMatchresults — one per descriptor — including any that werenot_foundorambiguous.Use
skip_missing=Falseto 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
SongMatchwith the best candidate selected. The match is flaggedambiguouswhen the top two candidates score withinambiguous_score_gapof 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().