Skip to content

Commit f21682c

Browse files
committed
cache albums to not request an album more than once, faster exporting
1 parent 0deca66 commit f21682c

1 file changed

Lines changed: 49 additions & 24 deletions

File tree

exportify-cli.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
# Max length for playlist name in progress bar
3434
DESC_LENGTH = 21
3535

36+
# ex: "37i9dQZF1DXcBWIGoYBM5M"
37+
PLAYLIST_ID_LENGTH = 22
38+
3639
# Configure logging
3740
logging.basicConfig(
3841
level=logging.WARNING,
@@ -194,6 +197,7 @@ def __init__(
194197
self.with_bar = with_bar
195198
self.exported_playlists = 0
196199
self.exported_tracks = 0
200+
self.album_cache: dict[str, dict] = {}
197201

198202
def _fetch_all_items(
199203
self,
@@ -203,9 +207,12 @@ def _fetch_all_items(
203207
desc: str | None = None,
204208
bar_format: str = DEFAULT_BAR_FORMAT,
205209
show_bar: bool = True,
210+
initial: int = 0,
211+
total_override: int | None = None,
206212
**kwargs: Any,
207213
) -> list[dict]:
208-
"""Fetch all paginated or batched items from a Spotify endpoint.
214+
"""
215+
Fetch all paginated or batched items from a Spotify endpoint.
209216
210217
- If the first positional arg is a list, treats it as an ID list for a batch
211218
endpoint (e.g. `self.spotify.albums`), calls in chunks of 20, and returns
@@ -218,16 +225,21 @@ def _fetch_all_items(
218225
# --- Batch mode (e.g. spotify.albums) ---
219226
if args and isinstance(args[0], list):
220227
id_list: list[str] = args[0]
221-
total = len(id_list)
228+
total = total_override if total_override is not None else len(id_list)
222229
desc_text = desc or fetch_func.__name__
223230
# build 20-item batches
224-
batches = [id_list[i : i + 20] for i in range(0, total, 20)]
231+
batches = [
232+
id_list[i : i + 20] for i in range(0, total, 20) if id_list[i : i + 20]
233+
]
225234

226235
pbar = (
227236
tqdm(total=total, desc=desc_text, unit="album", bar_format=bar_format)
228237
if show_bar and self.with_bar
229238
else None
230239
)
240+
# Advance the bar to reflect items already in cache
241+
if pbar and initial:
242+
pbar.update(initial)
231243

232244
for batch in batches:
233245
results = fetch_func(batch, *args[1:], **kwargs)
@@ -371,30 +383,44 @@ def export_playlist(self, playlist: dict, output_dir: Path) -> None:
371383
# 'video_thumbnail': {'url': None}}
372384

373385
# Batch fetch album details
374-
album_ids = list(
375-
{
376-
i.get("track").get("album").get("id")
377-
for i in items
378-
if i.get("track")
379-
and i.get("track").get("album")
380-
and i.get("track").get("album").get("id")
381-
},
382-
)
383-
album_items = self._fetch_all_items(
384-
self.spotify.albums,
385-
"albums",
386-
album_ids,
387-
desc="Fetching album details: ",
388-
)
389-
albums = {a.get("id"): a for a in album_items if a}
386+
album_ids = {
387+
i.get("track").get("album").get("id")
388+
for i in items
389+
if i.get("track")
390+
and i.get("track").get("album")
391+
and i.get("track").get("album").get("id")
392+
}
393+
394+
# Figure out which albums we haven't fetched yet
395+
ids_to_fetch = [aid for aid in album_ids if aid not in self.album_cache]
396+
already_cached = len(album_ids) - len(ids_to_fetch)
397+
398+
# Fetch only the missing albums
399+
if ids_to_fetch:
400+
new_albums = self._fetch_all_items(
401+
self.spotify.albums,
402+
"albums",
403+
ids_to_fetch,
404+
desc="Fetching album details: ",
405+
initial=already_cached,
406+
total_override=len(album_ids),
407+
)
408+
for alb in new_albums:
409+
if alb and alb.get("id"):
410+
self.album_cache[alb["id"]] = alb
411+
412+
# Build a lookup from cache
413+
albums = {aid: self.album_cache[aid] for aid in album_ids}
390414

391415
# Build export data
392416
export_data = []
393417
for i in items:
394418
track = i.get("track") or {}
395419
album = albums.get(track.get("album", {}).get("id"), {})
396-
artists = [a["name"] for a in track.get("artists", [])]
397-
artist_uris = [a["uri"] for a in track.get("artists", [])]
420+
artists = [a.get("name") for a in track.get("artists", []) if a.get("name")]
421+
artist_uris = [
422+
a.get("uri") for a in track.get("artists", []) if a.get("uri")
423+
]
398424

399425
record = {
400426
"Track URI": track.get("uri"),
@@ -403,8 +429,7 @@ def export_playlist(self, playlist: dict, output_dir: Path) -> None:
403429
"Track Name": track.get("name"),
404430
"Album Name": album.get("name"),
405431
"Artist Name(s)": artists,
406-
"Release Date": album.get("release_date")
407-
or (track.get("release_date")),
432+
"Release Date": album.get("release_date") or track.get("release_date"),
408433
"Duration_ms": track.get("duration_ms"),
409434
"Popularity": track.get("popularity"),
410435
"Added By": i.get("added_by", {}).get("id"),
@@ -613,7 +638,7 @@ def main(
613638
)
614639

615640
# User may be trying to export a playlist they have not saved
616-
elif term.isalnum() and len(term) == 22:
641+
elif term.isalnum() and len(term) == PLAYLIST_ID_LENGTH:
617642
try:
618643
pl = client.playlist(term)
619644
if pl:

0 commit comments

Comments
 (0)