@@ -34,6 +34,7 @@ class MonitoredPlaylist:
3434 track_count : int = 0
3535 source : str = "tidal" # "tidal" or "listenbrainz"
3636 extra_config : Dict [str , Any ] = None # e.g. { "lb_username": "...", "lb_type": "..." }
37+ use_playlist_folder : bool = False
3738
3839class PlaylistManager :
3940 _instance = None
@@ -76,8 +77,8 @@ def get_monitored_playlists(self) -> List[Dict]:
7677 def get_playlist (self , uuid : str ) -> Optional [MonitoredPlaylist ]:
7778 return next ((p for p in self ._playlists if p .uuid == uuid ), None )
7879
79- def add_monitored_playlist (self , uuid : str , name : str , frequency : str = "manual" , quality : str = "LOSSLESS" , source : str = "tidal" , extra_config : Dict = None ) -> tuple [MonitoredPlaylist , bool ]:
80- logger .info (f"Adding/Updating playlist: { uuid } - { name } (Freq: { frequency } , Qual: { quality } , Source: { source } )" )
80+ def add_monitored_playlist (self , uuid : str , name : str , frequency : str = "manual" , quality : str = "LOSSLESS" , source : str = "tidal" , extra_config : Dict = None , use_playlist_folder : bool = False ) -> tuple [MonitoredPlaylist , bool ]:
81+ logger .info (f"Adding/Updating playlist: { uuid } - { name } (Freq: { frequency } , Qual: { quality } , Source: { source } , Folder: { use_playlist_folder } )" )
8182 # Check if exists
8283 existing = self .get_playlist (uuid )
8384 if existing :
@@ -86,6 +87,7 @@ def add_monitored_playlist(self, uuid: str, name: str, frequency: str = "manual"
8687 existing .quality = quality
8788 existing .source = source
8889 existing .extra_config = extra_config
90+ existing .use_playlist_folder = use_playlist_folder
8991 # Start sync immediately? No, caller decides.
9092 self ._save_state ()
9193 logger .info (f"Playlist { uuid } updated. Current list size: { len (self ._playlists )} " )
@@ -103,7 +105,8 @@ def add_monitored_playlist(self, uuid: str, name: str, frequency: str = "manual"
103105 sync_frequency = frequency ,
104106 quality = quality ,
105107 source = source ,
106- extra_config = extra_config
108+ extra_config = extra_config ,
109+ use_playlist_folder = use_playlist_folder
107110 )
108111 self ._playlists .append (playlist )
109112 self ._save_state ()
@@ -249,6 +252,16 @@ async def _process_playlist_items(self, playlist: MonitoredPlaylist, raw_items:
249252 m3u8_lines = ["#EXTM3U" , f"# Source: { playlist .source } " ]
250253 items_to_download = []
251254
255+ org_template = settings .organization_template
256+ group_compilations = settings .group_compilations
257+
258+ if playlist .use_playlist_folder :
259+ safe_pl_name = sanitize_path_component (playlist .name )
260+ # Use 'tidaloader_playlists' explicitly to match PLAYLISTS_DIR logic
261+ # This makes the path relative to DOWNLOAD_DIR be: tidaloader_playlists/PlaylistName/Track - Title
262+ org_template = f"tidaloader_playlists/{ safe_pl_name } /{{TrackNumber}} - {{Title}}"
263+ group_compilations = False
264+
252265 for i , item in enumerate (raw_items ):
253266 # Robust extraction logic mirrored from search.py
254267 track = item .get ('item' , item ) if isinstance (item , dict ) else item
@@ -293,29 +306,29 @@ async def _process_playlist_items(self, playlist: MonitoredPlaylist, raw_items:
293306
294307 # Check FLAC (Common default for lossless)
295308 metadata ['file_ext' ] = '.flac'
296- rel_flac = get_output_relative_path (metadata )
309+ rel_flac = get_output_relative_path (metadata , template = org_template , group_compilations = group_compilations )
297310 if (DOWNLOAD_DIR / rel_flac ).exists ():
298311 logger .info (f"Found existing file (FLAC): { rel_flac } " )
299312 found_rel_path = rel_flac
300313 else :
301314 # logger.debug(f"File not found at: {DOWNLOAD_DIR / rel_flac}")
302315 # Check M4A
303316 metadata ['file_ext' ] = '.m4a'
304- rel_m4a = get_output_relative_path (metadata )
317+ rel_m4a = get_output_relative_path (metadata , template = org_template , group_compilations = group_compilations )
305318 if (DOWNLOAD_DIR / rel_m4a ).exists ():
306319 logger .info (f"Found existing file (M4A): { rel_m4a } " )
307320 found_rel_path = rel_m4a
308321 else :
309322 # Check MP3
310323 metadata ['file_ext' ] = '.mp3'
311- rel_mp3 = get_output_relative_path (metadata )
324+ rel_mp3 = get_output_relative_path (metadata , template = org_template , group_compilations = group_compilations )
312325 if (DOWNLOAD_DIR / rel_mp3 ).exists ():
313326 logger .info (f"Found existing file (MP3): { rel_mp3 } " )
314327 found_rel_path = rel_mp3
315328 # Check OPUS
316329 else :
317330 metadata ['file_ext' ] = '.opus'
318- rel_opus = get_output_relative_path (metadata )
331+ rel_opus = get_output_relative_path (metadata , template = org_template , group_compilations = group_compilations )
319332 if (DOWNLOAD_DIR / rel_opus ).exists ():
320333 logger .info (f"Found existing file (OPUS): { rel_opus } " )
321334 found_rel_path = rel_opus
@@ -346,8 +359,8 @@ async def _process_playlist_items(self, playlist: MonitoredPlaylist, raw_items:
346359 tidal_artist_id = str (artist_data .get ('id' )) if artist_data .get ('id' ) else None ,
347360 tidal_album_id = str (album_data .get ('id' )) if album_data .get ('id' ) else None ,
348361 auto_clean = True ,
349- organization_template = settings . organization_template ,
350- group_compilations = settings . group_compilations ,
362+ organization_template = org_template ,
363+ group_compilations = group_compilations ,
351364 run_beets = settings .run_beets ,
352365 embed_lyrics = settings .embed_lyrics
353366 ))
@@ -357,7 +370,7 @@ async def _process_playlist_items(self, playlist: MonitoredPlaylist, raw_items:
357370 target_ext = '.m4a'
358371
359372 metadata ['file_ext' ] = target_ext
360- predicted_path = get_output_relative_path (metadata )
373+ predicted_path = get_output_relative_path (metadata , template = org_template , group_compilations = group_compilations )
361374
362375 duration = track .get ('duration' , - 1 )
363376 m3u8_lines .append (f"#EXTINF:{ duration } ,{ artist_name } - { title } " )
0 commit comments