3333# Max length for playlist name in progress bar
3434DESC_LENGTH = 21
3535
36+ # ex: "37i9dQZF1DXcBWIGoYBM5M"
37+ PLAYLIST_ID_LENGTH = 22
38+
3639# Configure logging
3740logging .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