Fix m3u file naming handling
This commit is contained in:
@@ -266,7 +266,10 @@ class EASY_DW:
|
|||||||
self.__track_obj: trackCbObject = preferences.song_metadata
|
self.__track_obj: trackCbObject = preferences.song_metadata
|
||||||
|
|
||||||
# Convert it to the dictionary format needed for legacy functions
|
# Convert it to the dictionary format needed for legacy functions
|
||||||
self.__song_metadata = self._track_object_to_dict(self.__track_obj)
|
artist_separator = getattr(preferences, 'artist_separator', '; ')
|
||||||
|
self.__song_metadata_dict = track_object_to_dict(self.__track_obj, source_type='deezer', artist_separator=artist_separator)
|
||||||
|
# Maintain legacy attribute expected elsewhere
|
||||||
|
self.__song_metadata = self.__song_metadata_dict
|
||||||
self.__download_type = "track"
|
self.__download_type = "track"
|
||||||
|
|
||||||
self.__c_quality = qualities[self.__quality_download]
|
self.__c_quality = qualities[self.__quality_download]
|
||||||
@@ -324,7 +327,8 @@ class EASY_DW:
|
|||||||
It intelligently finds the album information based on the download context.
|
It intelligently finds the album information based on the download context.
|
||||||
"""
|
"""
|
||||||
# Use the unified metadata converter
|
# Use the unified metadata converter
|
||||||
metadata_dict = track_object_to_dict(track_obj, source_type='deezer')
|
artist_separator = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
metadata_dict = track_object_to_dict(track_obj, source_type='deezer', artist_separator=artist_separator)
|
||||||
|
|
||||||
# Check for track_position and disk_number in the original API data
|
# Check for track_position and disk_number in the original API data
|
||||||
# These might be directly available in the infos_dw dictionary for Deezer tracks
|
# These might be directly available in the infos_dw dictionary for Deezer tracks
|
||||||
@@ -345,8 +349,9 @@ class EASY_DW:
|
|||||||
custom_dir_format = getattr(self.__preferences, 'custom_dir_format', None)
|
custom_dir_format = getattr(self.__preferences, 'custom_dir_format', None)
|
||||||
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
||||||
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
||||||
|
self.__song_metadata_dict['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
self.__song_path = set_path(
|
self.__song_path = set_path(
|
||||||
self.__song_metadata,
|
self.__song_metadata_dict,
|
||||||
self.__output_dir,
|
self.__output_dir,
|
||||||
self.__song_quality,
|
self.__song_quality,
|
||||||
self.__file_format,
|
self.__file_format,
|
||||||
@@ -360,7 +365,7 @@ class EASY_DW:
|
|||||||
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
||||||
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
||||||
self.__song_path = set_path(
|
self.__song_path = set_path(
|
||||||
self.__song_metadata,
|
self.__song_metadata_dict,
|
||||||
self.__output_dir,
|
self.__output_dir,
|
||||||
self.__song_quality,
|
self.__song_quality,
|
||||||
self.__file_format,
|
self.__file_format,
|
||||||
@@ -953,7 +958,8 @@ class DW_ALBUM:
|
|||||||
def _album_object_to_dict(self, album_obj: albumCbObject) -> dict:
|
def _album_object_to_dict(self, album_obj: albumCbObject) -> dict:
|
||||||
"""Converts an albumObject to a dictionary for tagging and path generation."""
|
"""Converts an albumObject to a dictionary for tagging and path generation."""
|
||||||
# Use the unified metadata converter
|
# Use the unified metadata converter
|
||||||
return album_object_to_dict(album_obj, source_type='deezer')
|
artist_separator = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
return album_object_to_dict(album_obj, source_type='deezer', artist_separator=artist_separator)
|
||||||
|
|
||||||
def _track_object_to_dict(self, track_obj: any, album_obj: albumCbObject) -> dict:
|
def _track_object_to_dict(self, track_obj: any, album_obj: albumCbObject) -> dict:
|
||||||
"""Converts a track object to a dictionary with album context."""
|
"""Converts a track object to a dictionary with album context."""
|
||||||
@@ -973,10 +979,12 @@ class DW_ALBUM:
|
|||||||
genres=getattr(track_obj, 'genres', [])
|
genres=getattr(track_obj, 'genres', [])
|
||||||
)
|
)
|
||||||
# Use the unified metadata converter
|
# Use the unified metadata converter
|
||||||
return track_object_to_dict(full_track, source_type='deezer')
|
artist_separator = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
return track_object_to_dict(full_track, source_type='deezer', artist_separator=artist_separator)
|
||||||
else:
|
else:
|
||||||
# Use the unified metadata converter
|
# Use the unified metadata converter
|
||||||
return track_object_to_dict(track_obj, source_type='deezer')
|
artist_separator = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
return track_object_to_dict(track_obj, source_type='deezer', artist_separator=artist_separator)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -992,6 +1000,7 @@ class DW_ALBUM:
|
|||||||
self.__recursive_quality = self.__preferences.recursive_quality
|
self.__recursive_quality = self.__preferences.recursive_quality
|
||||||
album_obj: albumCbObject = self.__preferences.song_metadata
|
album_obj: albumCbObject = self.__preferences.song_metadata
|
||||||
self.__song_metadata = self._album_object_to_dict(album_obj)
|
self.__song_metadata = self._album_object_to_dict(album_obj)
|
||||||
|
self.__song_metadata['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
|
||||||
def dw(self) -> Album:
|
def dw(self) -> Album:
|
||||||
from deezspot.deezloader.deegw_api import API_GW
|
from deezspot.deezloader.deegw_api import API_GW
|
||||||
@@ -1139,7 +1148,8 @@ class DW_PLAYLIST:
|
|||||||
|
|
||||||
def _track_object_to_dict(self, track_obj: any) -> dict:
|
def _track_object_to_dict(self, track_obj: any) -> dict:
|
||||||
# Use the unified metadata converter
|
# Use the unified metadata converter
|
||||||
return track_object_to_dict(track_obj, source_type='deezer')
|
artist_separator = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
return track_object_to_dict(track_obj, source_type='deezer', artist_separator=artist_separator)
|
||||||
|
|
||||||
def dw(self) -> Playlist:
|
def dw(self) -> Playlist:
|
||||||
playlist_obj: playlistCbObject = self.__preferences.json_data
|
playlist_obj: playlistCbObject = self.__preferences.json_data
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ class DeeLogin:
|
|||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market,
|
market=stock_market,
|
||||||
playlist_context=None
|
playlist_context=None,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Track:
|
) -> Track:
|
||||||
|
|
||||||
link_is_valid(link_track)
|
link_is_valid(link_track)
|
||||||
@@ -192,6 +193,7 @@ class DeeLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
if playlist_context:
|
if playlist_context:
|
||||||
preferences.json_data = playlist_context['json_data']
|
preferences.json_data = playlist_context['json_data']
|
||||||
@@ -226,7 +228,8 @@ class DeeLogin:
|
|||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market,
|
market=stock_market,
|
||||||
playlist_context=None
|
playlist_context=None,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Album:
|
) -> Album:
|
||||||
|
|
||||||
link_is_valid(link_album)
|
link_is_valid(link_album)
|
||||||
@@ -273,6 +276,7 @@ class DeeLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
if playlist_context:
|
if playlist_context:
|
||||||
preferences.json_data = playlist_context['json_data']
|
preferences.json_data = playlist_context['json_data']
|
||||||
@@ -305,7 +309,8 @@ class DeeLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market
|
market=stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Playlist:
|
) -> Playlist:
|
||||||
|
|
||||||
link_is_valid(link_playlist)
|
link_is_valid(link_playlist)
|
||||||
@@ -339,6 +344,7 @@ class DeeLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
playlist = DW_PLAYLIST(preferences).dw()
|
playlist = DW_PLAYLIST(preferences).dw()
|
||||||
|
|
||||||
@@ -591,7 +597,8 @@ class DeeLogin:
|
|||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market,
|
market=stock_market,
|
||||||
playlist_context=None
|
playlist_context=None,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Track:
|
) -> Track:
|
||||||
|
|
||||||
link_dee = self.convert_spoty_to_dee_link_track(link_track)
|
link_dee = self.convert_spoty_to_dee_link_track(link_track)
|
||||||
@@ -613,7 +620,8 @@ class DeeLogin:
|
|||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market,
|
market=market,
|
||||||
playlist_context=playlist_context
|
playlist_context=playlist_context,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
|
|
||||||
return track
|
return track
|
||||||
@@ -636,7 +644,8 @@ class DeeLogin:
|
|||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market,
|
market=stock_market,
|
||||||
playlist_context=None
|
playlist_context=None,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Album:
|
) -> Album:
|
||||||
|
|
||||||
link_dee = self.convert_spoty_to_dee_link_album(link_album)
|
link_dee = self.convert_spoty_to_dee_link_album(link_album)
|
||||||
@@ -656,7 +665,8 @@ class DeeLogin:
|
|||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market,
|
market=market,
|
||||||
playlist_context=playlist_context
|
playlist_context=playlist_context,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
|
|
||||||
return album
|
return album
|
||||||
@@ -678,7 +688,8 @@ class DeeLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market
|
market=stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Playlist:
|
) -> Playlist:
|
||||||
|
|
||||||
link_is_valid(link_playlist)
|
link_is_valid(link_playlist)
|
||||||
@@ -821,7 +832,8 @@ class DeeLogin:
|
|||||||
custom_track_format=custom_track_format, pad_tracks=pad_tracks,
|
custom_track_format=custom_track_format, pad_tracks=pad_tracks,
|
||||||
initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase,
|
initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase,
|
||||||
max_retries=max_retries, convert_to=convert_to, bitrate=bitrate,
|
max_retries=max_retries, convert_to=convert_to, bitrate=bitrate,
|
||||||
save_cover=save_cover, market=market, playlist_context=playlist_context
|
save_cover=save_cover, market=market, playlist_context=playlist_context,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
tracks.append(downloaded_track)
|
tracks.append(downloaded_track)
|
||||||
|
|
||||||
@@ -898,7 +910,8 @@ class DeeLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market
|
market=stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Track:
|
) -> Track:
|
||||||
|
|
||||||
query = f"track:{song} artist:{artist}"
|
query = f"track:{song} artist:{artist}"
|
||||||
@@ -928,7 +941,8 @@ class DeeLogin:
|
|||||||
convert_to=convert_to,
|
convert_to=convert_to,
|
||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market
|
market=market,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
|
|
||||||
return track
|
return track
|
||||||
@@ -950,7 +964,8 @@ class DeeLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market
|
market=stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Episode:
|
) -> Episode:
|
||||||
|
|
||||||
logger.warning("Episode download logic is not fully refactored and might not work as expected with new reporting.")
|
logger.warning("Episode download logic is not fully refactored and might not work as expected with new reporting.")
|
||||||
@@ -990,6 +1005,7 @@ class DeeLogin:
|
|||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.is_episode = True
|
preferences.is_episode = True
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
episode = DW_EPISODE(preferences).dw()
|
episode = DW_EPISODE(preferences).dw()
|
||||||
|
|
||||||
@@ -1012,7 +1028,8 @@ class DeeLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market
|
market=stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Smart:
|
) -> Smart:
|
||||||
|
|
||||||
link_is_valid(link)
|
link_is_valid(link)
|
||||||
@@ -1040,7 +1057,7 @@ class DeeLogin:
|
|||||||
custom_track_format=custom_track_format, pad_tracks=pad_tracks,
|
custom_track_format=custom_track_format, pad_tracks=pad_tracks,
|
||||||
initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase,
|
initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase,
|
||||||
max_retries=max_retries, convert_to=convert_to, bitrate=bitrate,
|
max_retries=max_retries, convert_to=convert_to, bitrate=bitrate,
|
||||||
save_cover=save_cover, market=market
|
save_cover=save_cover, market=market, artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "track"
|
smart.type = "track"
|
||||||
smart.track = track
|
smart.track = track
|
||||||
@@ -1055,7 +1072,7 @@ class DeeLogin:
|
|||||||
pad_tracks=pad_tracks, initial_retry_delay=initial_retry_delay,
|
pad_tracks=pad_tracks, initial_retry_delay=initial_retry_delay,
|
||||||
retry_delay_increase=retry_delay_increase, max_retries=max_retries,
|
retry_delay_increase=retry_delay_increase, max_retries=max_retries,
|
||||||
convert_to=convert_to, bitrate=bitrate, save_cover=save_cover,
|
convert_to=convert_to, bitrate=bitrate, save_cover=save_cover,
|
||||||
market=market
|
market=market, artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "album"
|
smart.type = "album"
|
||||||
smart.album = album
|
smart.album = album
|
||||||
@@ -1070,7 +1087,7 @@ class DeeLogin:
|
|||||||
pad_tracks=pad_tracks, initial_retry_delay=initial_retry_delay,
|
pad_tracks=pad_tracks, initial_retry_delay=initial_retry_delay,
|
||||||
retry_delay_increase=retry_delay_increase, max_retries=max_retries,
|
retry_delay_increase=retry_delay_increase, max_retries=max_retries,
|
||||||
convert_to=convert_to, bitrate=bitrate, save_cover=save_cover,
|
convert_to=convert_to, bitrate=bitrate, save_cover=save_cover,
|
||||||
market=market
|
market=market, artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "playlist"
|
smart.type = "playlist"
|
||||||
smart.playlist = playlist
|
smart.playlist = playlist
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def _get_best_image_url(images: Any, source_type: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> Dict[str, Any]:
|
def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None, artist_separator: str = "; ") -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Convert a track object to a dictionary format for tagging.
|
Convert a track object to a dictionary format for tagging.
|
||||||
Supports both Spotify and Deezer track objects.
|
Supports both Spotify and Deezer track objects.
|
||||||
@@ -116,6 +116,7 @@ def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> D
|
|||||||
Args:
|
Args:
|
||||||
track_obj: Track object from Spotify or Deezer API
|
track_obj: Track object from Spotify or Deezer API
|
||||||
source_type: Optional source type ('spotify' or 'deezer'). If None, auto-detected.
|
source_type: Optional source type ('spotify' or 'deezer'). If None, auto-detected.
|
||||||
|
artist_separator: Separator string for joining multiple artists
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with standardized metadata tags
|
Dictionary with standardized metadata tags
|
||||||
@@ -142,7 +143,7 @@ def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> D
|
|||||||
|
|
||||||
# Artist information
|
# Artist information
|
||||||
if hasattr(track_obj, 'artists') and track_obj.artists:
|
if hasattr(track_obj, 'artists') and track_obj.artists:
|
||||||
tags['artist'] = "; ".join([getattr(artist, 'name', '') for artist in track_obj.artists])
|
tags['artist'] = artist_separator.join([getattr(artist, 'name', '') for artist in track_obj.artists])
|
||||||
else:
|
else:
|
||||||
tags['artist'] = ''
|
tags['artist'] = ''
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> D
|
|||||||
|
|
||||||
# Album artists
|
# Album artists
|
||||||
if hasattr(album, 'artists') and album.artists:
|
if hasattr(album, 'artists') and album.artists:
|
||||||
tags['ar_album'] = "; ".join([getattr(artist, 'name', '') for artist in album.artists])
|
tags['ar_album'] = artist_separator.join([getattr(artist, 'name', '') for artist in album.artists])
|
||||||
else:
|
else:
|
||||||
tags['ar_album'] = ''
|
tags['ar_album'] = ''
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> D
|
|||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
|
||||||
def album_object_to_dict(album_obj: Any, source_type: Optional[str] = None) -> Dict[str, Any]:
|
def album_object_to_dict(album_obj: Any, source_type: Optional[str] = None, artist_separator: str = "; ") -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Convert an album object to a dictionary format for tagging.
|
Convert an album object to a dictionary format for tagging.
|
||||||
Supports both Spotify and Deezer album objects.
|
Supports both Spotify and Deezer album objects.
|
||||||
@@ -218,6 +219,7 @@ def album_object_to_dict(album_obj: Any, source_type: Optional[str] = None) -> D
|
|||||||
Args:
|
Args:
|
||||||
album_obj: Album object from Spotify or Deezer API
|
album_obj: Album object from Spotify or Deezer API
|
||||||
source_type: Optional source type ('spotify' or 'deezer'). If None, auto-detected.
|
source_type: Optional source type ('spotify' or 'deezer'). If None, auto-detected.
|
||||||
|
artist_separator: Separator string for joining multiple album artists
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with standardized metadata tags
|
Dictionary with standardized metadata tags
|
||||||
@@ -236,7 +238,7 @@ def album_object_to_dict(album_obj: Any, source_type: Optional[str] = None) -> D
|
|||||||
|
|
||||||
# Album artists
|
# Album artists
|
||||||
if hasattr(album_obj, 'artists') and album_obj.artists:
|
if hasattr(album_obj, 'artists') and album_obj.artists:
|
||||||
tags['ar_album'] = "; ".join([getattr(artist, 'name', '') for artist in album_obj.artists])
|
tags['ar_album'] = artist_separator.join([getattr(artist, 'name', '') for artist in album_obj.artists])
|
||||||
else:
|
else:
|
||||||
tags['ar_album'] = ''
|
tags['ar_album'] = ''
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from unicodedata import normalize
|
||||||
from os import makedirs
|
from os import makedirs
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -43,6 +44,11 @@ def __check_dir(directory):
|
|||||||
def sanitize_name(string, max_length=200):
|
def sanitize_name(string, max_length=200):
|
||||||
"""Sanitize a string for use as a filename or directory name.
|
"""Sanitize a string for use as a filename or directory name.
|
||||||
|
|
||||||
|
This version maps filesystem-conflicting ASCII characters to Unicode
|
||||||
|
lookalikes (mostly fullwidth forms) rather than dropping or replacing
|
||||||
|
with ASCII fallbacks. This preserves readability while avoiding
|
||||||
|
path-separator or Windows-invalid characters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
string: The string to sanitize
|
string: The string to sanitize
|
||||||
max_length: Maximum length for the resulting string
|
max_length: Maximum length for the resulting string
|
||||||
@@ -56,20 +62,22 @@ def sanitize_name(string, max_length=200):
|
|||||||
# Convert to string if not already
|
# Convert to string if not already
|
||||||
string = str(string)
|
string = str(string)
|
||||||
|
|
||||||
# Enhance character replacement for filenames
|
# Map invalid/reserved characters to Unicode fullwidth or similar lookalikes
|
||||||
|
# to avoid filesystem conflicts while keeping readability.
|
||||||
|
# Windows-invalid: < > : " / \ | ? * and control chars
|
||||||
replacements = {
|
replacements = {
|
||||||
"\\": "-", # Backslash to hyphen
|
"\\": "\", # U+FF3C FULLWIDTH REVERSE SOLIDUS
|
||||||
"/": "-", # Forward slash to hyphen
|
"/": "/", # U+FF0F FULLWIDTH SOLIDUS
|
||||||
":": "-", # Colon to hyphen
|
":": ":", # U+FF1A FULLWIDTH COLON
|
||||||
"*": "+", # Asterisk to plus
|
"*": "*", # U+FF0A FULLWIDTH ASTERISK
|
||||||
"?": "", # Question mark removed
|
"?": "?", # U+FF1F FULLWIDTH QUESTION MARK
|
||||||
"\"": "'", # Double quote to single quote
|
"\"": """, # U+FF02 FULLWIDTH QUOTATION MARK
|
||||||
"<": "[", # Less than to open bracket
|
"<": "<", # U+FF1C FULLWIDTH LESS-THAN SIGN
|
||||||
">": "]", # Greater than to close bracket
|
">": ">", # U+FF1E FULLWIDTH GREATER-THAN SIGN
|
||||||
"|": "-", # Pipe to hyphen
|
"|": "|", # U+FF5C FULLWIDTH VERTICAL LINE
|
||||||
"&": "and", # Ampersand to 'and'
|
"&": "&", # U+FF06 FULLWIDTH AMPERSAND
|
||||||
"$": "s", # Dollar to 's'
|
"$": "$", # U+FF04 FULLWIDTH DOLLAR SIGN
|
||||||
";": ",", # Semicolon to comma
|
";": ";", # U+FF1B FULLWIDTH SEMICOLON
|
||||||
"\t": " ", # Tab to space
|
"\t": " ", # Tab to space
|
||||||
"\n": " ", # Newline to space
|
"\n": " ", # Newline to space
|
||||||
"\r": " ", # Carriage return to space
|
"\r": " ", # Carriage return to space
|
||||||
@@ -99,6 +107,10 @@ def sanitize_name(string, max_length=200):
|
|||||||
if not string:
|
if not string:
|
||||||
string = "Unknown"
|
string = "Unknown"
|
||||||
|
|
||||||
|
# Normalize to NFC to keep composed characters stable but avoid
|
||||||
|
# compatibility decomposition that might revert fullwidth mappings.
|
||||||
|
string = normalize('NFC', string)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
# Keep the original function name for backward compatibility
|
# Keep the original function name for backward compatibility
|
||||||
@@ -127,6 +139,11 @@ def apply_custom_format(format_str, metadata: dict, pad_tracks=True) -> str:
|
|||||||
def replacer(match):
|
def replacer(match):
|
||||||
full_key = match.group(1) # e.g., "artist", "ar_album_1"
|
full_key = match.group(1) # e.g., "artist", "ar_album_1"
|
||||||
|
|
||||||
|
# Allow custom artist/album-artist separator to be provided via metadata
|
||||||
|
separator = metadata.get('artist_separator', ';')
|
||||||
|
if not isinstance(separator, str) or separator == "":
|
||||||
|
separator = ';'
|
||||||
|
|
||||||
# Check for specific indexed placeholders: artist_INDEX or ar_album_INDEX
|
# Check for specific indexed placeholders: artist_INDEX or ar_album_INDEX
|
||||||
# Allows %artist_1%, %ar_album_1%, etc.
|
# Allows %artist_1%, %ar_album_1%, etc.
|
||||||
indexed_artist_match = re.fullmatch(r'(artist|ar_album)_(\d+)', full_key)
|
indexed_artist_match = re.fullmatch(r'(artist|ar_album)_(\d+)', full_key)
|
||||||
@@ -143,8 +160,8 @@ def apply_custom_format(format_str, metadata: dict, pad_tracks=True) -> str:
|
|||||||
items = []
|
items = []
|
||||||
|
|
||||||
if isinstance(raw_value, str):
|
if isinstance(raw_value, str):
|
||||||
# Split semicolon-separated strings and strip whitespace
|
# Split by provided separator and strip whitespace
|
||||||
items = [item.strip() for item in raw_value.split(';') if item.strip()]
|
items = [item.strip() for item in raw_value.split(separator) if item.strip()]
|
||||||
elif isinstance(raw_value, list):
|
elif isinstance(raw_value, list):
|
||||||
# Convert all items to string, strip whitespace
|
# Convert all items to string, strip whitespace
|
||||||
items = [str(item).strip() for item in raw_value if str(item).strip()]
|
items = [str(item).strip() for item in raw_value if str(item).strip()]
|
||||||
@@ -193,13 +210,14 @@ def __get_dir(song_metadata, output_dir, custom_dir_format=None, pad_tracks=True
|
|||||||
__check_dir(output_dir)
|
__check_dir(output_dir)
|
||||||
return output_dir
|
return output_dir
|
||||||
|
|
||||||
# Apply the custom format string.
|
# Apply formatting per path component so only slashes from the format
|
||||||
# pad_tracks is passed along in case 'tracknum' or 'discnum' are used in dir format.
|
# create directories; slashes from data are sanitized inside components.
|
||||||
formatted_path_segment = apply_custom_format(custom_dir_format, song_metadata, pad_tracks)
|
format_parts = custom_dir_format.split("/")
|
||||||
|
formatted_parts = [
|
||||||
# Sanitize each component of the formatted path segment
|
apply_custom_format(part, song_metadata, pad_tracks) for part in format_parts
|
||||||
|
]
|
||||||
sanitized_path_segment = "/".join(
|
sanitized_path_segment = "/".join(
|
||||||
sanitize_name(part) for part in formatted_path_segment.split("/")
|
sanitize_name(part) for part in formatted_parts
|
||||||
)
|
)
|
||||||
|
|
||||||
# Join with the base output directory
|
# Join with the base output directory
|
||||||
|
|||||||
@@ -10,74 +10,74 @@ from deezspot.models.download import Track
|
|||||||
def create_m3u_file(output_dir: str, playlist_name: str) -> str:
|
def create_m3u_file(output_dir: str, playlist_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Creates an m3u playlist file with the proper header.
|
Creates an m3u playlist file with the proper header.
|
||||||
|
Returns full path to the m3u file.
|
||||||
Args:
|
|
||||||
output_dir: Base output directory
|
|
||||||
playlist_name: Name of the playlist (will be sanitized)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Full path to the created m3u file
|
|
||||||
"""
|
"""
|
||||||
playlist_m3u_dir = os.path.join(output_dir, "playlists")
|
playlist_m3u_dir = os.path.join(output_dir, "playlists")
|
||||||
os.makedirs(playlist_m3u_dir, exist_ok=True)
|
os.makedirs(playlist_m3u_dir, exist_ok=True)
|
||||||
|
|
||||||
playlist_name_sanitized = sanitize_name(playlist_name)
|
playlist_name_sanitized = sanitize_name(playlist_name)
|
||||||
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
|
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
|
||||||
|
# Always ensure header exists (idempotent)
|
||||||
if not os.path.exists(m3u_path):
|
if not os.path.exists(m3u_path):
|
||||||
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
|
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
|
||||||
m3u_file.write("#EXTM3U\n")
|
m3u_file.write("#EXTM3U\n")
|
||||||
logger.debug(f"Created m3u playlist file: {m3u_path}")
|
logger.debug(f"Created m3u playlist file: {m3u_path}")
|
||||||
|
|
||||||
return m3u_path
|
return m3u_path
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_m3u_header(m3u_path: str) -> None:
|
||||||
|
"""Ensure an existing m3u has the header; create if missing."""
|
||||||
|
if not os.path.exists(m3u_path):
|
||||||
|
os.makedirs(os.path.dirname(m3u_path), exist_ok=True)
|
||||||
|
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
|
||||||
|
m3u_file.write("#EXTM3U\n")
|
||||||
|
|
||||||
|
|
||||||
|
# Prefer the actual file that exists on disk; if the stored path doesn't exist,
|
||||||
|
# attempt to find the same basename with a different extension (e.g., due to conversion).
|
||||||
|
_AUDIO_EXTS_TRY = [
|
||||||
|
".flac", ".mp3", ".m4a", ".aac", ".alac", ".ogg", ".opus", ".wav", ".aiff"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_existing_song_path(song_path: str) -> Union[str, None]:
|
||||||
|
if not song_path:
|
||||||
|
return None
|
||||||
|
if os.path.exists(song_path):
|
||||||
|
return song_path
|
||||||
|
base, _ = os.path.splitext(song_path)
|
||||||
|
for ext in _AUDIO_EXTS_TRY:
|
||||||
|
candidate = base + ext
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_track_duration_seconds(track: Track) -> int:
|
def _get_track_duration_seconds(track: Track) -> int:
|
||||||
"""
|
|
||||||
Extract track duration in seconds from track metadata.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
track: Track object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Duration in seconds, defaults to 0 if not available
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Try to get duration from tags first
|
|
||||||
if hasattr(track, 'tags') and track.tags:
|
if hasattr(track, 'tags') and track.tags:
|
||||||
if 'duration' in track.tags:
|
if 'duration' in track.tags:
|
||||||
return int(float(track.tags['duration']))
|
return int(float(track.tags['duration']))
|
||||||
elif 'length' in track.tags:
|
elif 'length' in track.tags:
|
||||||
return int(float(track.tags['length']))
|
return int(float(track.tags['length']))
|
||||||
|
|
||||||
# Try to get from song_metadata if available
|
|
||||||
if hasattr(track, 'song_metadata') and hasattr(track.song_metadata, 'duration_ms'):
|
if hasattr(track, 'song_metadata') and hasattr(track.song_metadata, 'duration_ms'):
|
||||||
return int(track.song_metadata.duration_ms / 1000)
|
return int(track.song_metadata.duration_ms / 1000)
|
||||||
|
|
||||||
# Fallback to 0 if no duration found
|
|
||||||
return 0
|
return 0
|
||||||
except (ValueError, AttributeError, TypeError):
|
except (ValueError, AttributeError, TypeError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _get_track_info(track: Track) -> tuple:
|
def _get_track_info(track: Track) -> tuple:
|
||||||
"""
|
|
||||||
Extract artist and title information from track.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
track: Track object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (artist, title) strings
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
if hasattr(track, 'tags') and track.tags:
|
if hasattr(track, 'tags') and track.tags:
|
||||||
artist = track.tags.get('artist', 'Unknown Artist')
|
artist = track.tags.get('artist', 'Unknown Artist')
|
||||||
title = track.tags.get('music', track.tags.get('title', 'Unknown Title'))
|
title = track.tags.get('music', track.tags.get('title', 'Unknown Title'))
|
||||||
return artist, title
|
return artist, title
|
||||||
elif hasattr(track, 'song_metadata'):
|
elif hasattr(track, 'song_metadata'):
|
||||||
|
sep = ", "
|
||||||
|
if hasattr(track, 'tags') and track.tags:
|
||||||
|
sep = track.tags.get('artist_separator', sep)
|
||||||
if hasattr(track.song_metadata, 'artists') and track.song_metadata.artists:
|
if hasattr(track.song_metadata, 'artists') and track.song_metadata.artists:
|
||||||
artist = ', '.join([a.name for a in track.song_metadata.artists])
|
artist = sep.join([a.name for a in track.song_metadata.artists])
|
||||||
else:
|
else:
|
||||||
artist = 'Unknown Artist'
|
artist = 'Unknown Artist'
|
||||||
title = getattr(track.song_metadata, 'title', 'Unknown Title')
|
title = getattr(track.song_metadata, 'title', 'Unknown Title')
|
||||||
@@ -89,40 +89,28 @@ def _get_track_info(track: Track) -> tuple:
|
|||||||
|
|
||||||
|
|
||||||
def append_track_to_m3u(m3u_path: str, track: Union[str, Track]) -> None:
|
def append_track_to_m3u(m3u_path: str, track: Union[str, Track]) -> None:
|
||||||
"""
|
"""Append a single track to m3u with EXTINF and a resolved path."""
|
||||||
Appends a single track to an existing m3u file with extended format.
|
ensure_m3u_header(m3u_path)
|
||||||
|
|
||||||
Args:
|
|
||||||
m3u_path: Full path to the m3u file
|
|
||||||
track: Track object or string path to track file
|
|
||||||
"""
|
|
||||||
if isinstance(track, str):
|
if isinstance(track, str):
|
||||||
# Legacy support for string paths
|
resolved = _resolve_existing_song_path(track)
|
||||||
track_path = track
|
if not resolved:
|
||||||
if not track_path or not os.path.exists(track_path):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
playlist_m3u_dir = os.path.dirname(m3u_path)
|
playlist_m3u_dir = os.path.dirname(m3u_path)
|
||||||
relative_path = os.path.relpath(track_path, start=playlist_m3u_dir)
|
relative_path = os.path.relpath(resolved, start=playlist_m3u_dir)
|
||||||
|
|
||||||
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
|
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
|
||||||
m3u_file.write(f"{relative_path}\n")
|
m3u_file.write(f"{relative_path}\n")
|
||||||
else:
|
else:
|
||||||
# Track object with full metadata
|
|
||||||
if (not isinstance(track, Track) or
|
if (not isinstance(track, Track) or
|
||||||
not track.success or
|
not track.success or
|
||||||
not hasattr(track, 'song_path') or
|
not hasattr(track, 'song_path')):
|
||||||
not track.song_path or
|
return
|
||||||
not os.path.exists(track.song_path)):
|
resolved = _resolve_existing_song_path(track.song_path)
|
||||||
|
if not resolved:
|
||||||
return
|
return
|
||||||
|
|
||||||
playlist_m3u_dir = os.path.dirname(m3u_path)
|
playlist_m3u_dir = os.path.dirname(m3u_path)
|
||||||
relative_path = os.path.relpath(track.song_path, start=playlist_m3u_dir)
|
relative_path = os.path.relpath(resolved, start=playlist_m3u_dir)
|
||||||
|
|
||||||
# Get track metadata
|
|
||||||
duration = _get_track_duration_seconds(track)
|
duration = _get_track_duration_seconds(track)
|
||||||
artist, title = _get_track_info(track)
|
artist, title = _get_track_info(track)
|
||||||
|
|
||||||
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
|
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
|
||||||
m3u_file.write(f"#EXTINF:{duration},{artist} - {title}\n")
|
m3u_file.write(f"#EXTINF:{duration},{artist} - {title}\n")
|
||||||
m3u_file.write(f"{relative_path}\n")
|
m3u_file.write(f"{relative_path}\n")
|
||||||
@@ -130,57 +118,19 @@ def append_track_to_m3u(m3u_path: str, track: Union[str, Track]) -> None:
|
|||||||
|
|
||||||
def write_tracks_to_m3u(output_dir: str, playlist_name: str, tracks: List[Track]) -> str:
|
def write_tracks_to_m3u(output_dir: str, playlist_name: str, tracks: List[Track]) -> str:
|
||||||
"""
|
"""
|
||||||
Creates an m3u file and writes all successful tracks to it at once using extended format.
|
Legacy batch method. Creates an m3u and writes provided tracks.
|
||||||
|
Prefer progressive usage: create_m3u_file(...) once, then append_track_to_m3u(...) per track.
|
||||||
Args:
|
|
||||||
output_dir: Base output directory
|
|
||||||
playlist_name: Name of the playlist (will be sanitized)
|
|
||||||
tracks: List of Track objects
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Full path to the created m3u file
|
|
||||||
"""
|
"""
|
||||||
playlist_m3u_dir = os.path.join(output_dir, "playlists")
|
playlist_m3u_dir = os.path.join(output_dir, "playlists")
|
||||||
os.makedirs(playlist_m3u_dir, exist_ok=True)
|
os.makedirs(playlist_m3u_dir, exist_ok=True)
|
||||||
|
m3u_path = os.path.join(playlist_m3u_dir, f"{sanitize_name(playlist_name)}.m3u")
|
||||||
playlist_name_sanitized = sanitize_name(playlist_name)
|
ensure_m3u_header(m3u_path)
|
||||||
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
|
|
||||||
|
|
||||||
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
|
|
||||||
m3u_file.write("#EXTM3U\n")
|
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
if (isinstance(track, Track) and
|
append_track_to_m3u(m3u_path, track)
|
||||||
track.success and
|
|
||||||
hasattr(track, 'song_path') and
|
|
||||||
track.song_path and
|
|
||||||
os.path.exists(track.song_path)):
|
|
||||||
|
|
||||||
relative_song_path = os.path.relpath(track.song_path, start=playlist_m3u_dir)
|
|
||||||
|
|
||||||
# Get track metadata
|
|
||||||
duration = _get_track_duration_seconds(track)
|
|
||||||
artist, title = _get_track_info(track)
|
|
||||||
|
|
||||||
# Write EXTINF line with duration and metadata
|
|
||||||
m3u_file.write(f"#EXTINF:{duration},{artist} - {title}\n")
|
|
||||||
m3u_file.write(f"{relative_song_path}\n")
|
|
||||||
|
|
||||||
logger.info(f"Created m3u playlist file at: {m3u_path}")
|
logger.info(f"Created m3u playlist file at: {m3u_path}")
|
||||||
return m3u_path
|
return m3u_path
|
||||||
|
|
||||||
|
|
||||||
def get_m3u_path(output_dir: str, playlist_name: str) -> str:
|
def get_m3u_path(output_dir: str, playlist_name: str) -> str:
|
||||||
"""
|
|
||||||
Get the expected path for an m3u file without creating it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_dir: Base output directory
|
|
||||||
playlist_name: Name of the playlist (will be sanitized)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Full path where the m3u file would be located
|
|
||||||
"""
|
|
||||||
playlist_m3u_dir = os.path.join(output_dir, "playlists")
|
playlist_m3u_dir = os.path.join(output_dir, "playlists")
|
||||||
playlist_name_sanitized = sanitize_name(playlist_name)
|
return os.path.join(playlist_m3u_dir, f"{sanitize_name(playlist_name)}.m3u")
|
||||||
return os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
|
|
||||||
@@ -13,11 +13,13 @@ class Preferences:
|
|||||||
self.recursive_download = None
|
self.recursive_download = None
|
||||||
self.not_interface = None
|
self.not_interface = None
|
||||||
self.make_zip = None
|
self.make_zip = None
|
||||||
self.real_time_dl = None ,
|
self.real_time_dl = None
|
||||||
self.custom_dir_format = None,
|
self.custom_dir_format = None
|
||||||
self.custom_track_format = None,
|
self.custom_track_format = None
|
||||||
self.pad_tracks = True # Default to padded track numbers (01, 02, etc.)
|
self.pad_tracks = True # Default to padded track numbers (01, 02, etc.)
|
||||||
self.initial_retry_delay = 30 # Default initial retry delay in seconds
|
self.initial_retry_delay = 30 # Default initial retry delay in seconds
|
||||||
self.retry_delay_increase = 30 # Default increase in delay between retries in seconds
|
self.retry_delay_increase = 30 # Default increase in delay between retries in seconds
|
||||||
self.max_retries = 5 # Default maximum number of retries per track
|
self.max_retries = 5 # Default maximum number of retries per track
|
||||||
self.save_cover: bool = False # Option to save a cover.jpg image
|
self.save_cover: bool = False # Option to save a cover.jpg image
|
||||||
|
# New: artist separator for joining multiple artists or album artists
|
||||||
|
self.artist_separator: str = "; "
|
||||||
@@ -104,7 +104,13 @@ class EASY_DW:
|
|||||||
self.__link = preferences.link
|
self.__link = preferences.link
|
||||||
self.__output_dir = preferences.output_dir
|
self.__output_dir = preferences.output_dir
|
||||||
self.__song_metadata = preferences.song_metadata
|
self.__song_metadata = preferences.song_metadata
|
||||||
self.__song_metadata_dict = _track_object_to_dict(self.__song_metadata)
|
# Convert song metadata to dict with configured artist separator
|
||||||
|
artist_separator = getattr(preferences, 'artist_separator', '; ')
|
||||||
|
if parent == 'album' and hasattr(self.__song_metadata, 'album'):
|
||||||
|
# When iterating album tracks later we will still need the separator, but for initial dict, use track conversion
|
||||||
|
self.__song_metadata_dict = track_object_to_dict(self.__song_metadata, source_type='spotify', artist_separator=artist_separator)
|
||||||
|
else:
|
||||||
|
self.__song_metadata_dict = track_object_to_dict(self.__song_metadata, source_type='spotify', artist_separator=artist_separator)
|
||||||
self.__not_interface = preferences.not_interface
|
self.__not_interface = preferences.not_interface
|
||||||
self.__quality_download = preferences.quality_download or "NORMAL"
|
self.__quality_download = preferences.quality_download or "NORMAL"
|
||||||
self.__recursive_download = preferences.recursive_download
|
self.__recursive_download = preferences.recursive_download
|
||||||
@@ -136,6 +142,8 @@ class EASY_DW:
|
|||||||
custom_dir_format = getattr(self.__preferences, 'custom_dir_format', None)
|
custom_dir_format = getattr(self.__preferences, 'custom_dir_format', None)
|
||||||
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
||||||
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
||||||
|
# Ensure the separator is available to formatting utils for indexed placeholders
|
||||||
|
self.__song_metadata_dict['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
self.__song_path = set_path(
|
self.__song_path = set_path(
|
||||||
self.__song_metadata_dict,
|
self.__song_metadata_dict,
|
||||||
self.__output_dir,
|
self.__output_dir,
|
||||||
@@ -150,6 +158,7 @@ class EASY_DW:
|
|||||||
custom_dir_format = getattr(self.__preferences, 'custom_dir_format', None)
|
custom_dir_format = getattr(self.__preferences, 'custom_dir_format', None)
|
||||||
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
custom_track_format = getattr(self.__preferences, 'custom_track_format', None)
|
||||||
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
pad_tracks = getattr(self.__preferences, 'pad_tracks', True)
|
||||||
|
self.__song_metadata_dict['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
self.__song_path = set_path(
|
self.__song_path = set_path(
|
||||||
self.__song_metadata_dict,
|
self.__song_metadata_dict,
|
||||||
self.__output_dir,
|
self.__output_dir,
|
||||||
@@ -200,7 +209,7 @@ class EASY_DW:
|
|||||||
parent_info = {
|
parent_info = {
|
||||||
"type": "album",
|
"type": "album",
|
||||||
"title": album_meta.title,
|
"title": album_meta.title,
|
||||||
"artist": "; ".join([a.name for a in album_meta.artists]),
|
"artist": getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in album_meta.artists]),
|
||||||
"total_tracks": total_tracks_val,
|
"total_tracks": total_tracks_val,
|
||||||
"url": f"https://open.spotify.com/album/{album_meta.ids.spotify if album_meta.ids else ''}"
|
"url": f"https://open.spotify.com/album/{album_meta.ids.spotify if album_meta.ids else ''}"
|
||||||
}
|
}
|
||||||
@@ -313,7 +322,7 @@ class EASY_DW:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
song_title = self.__song_metadata.title
|
song_title = self.__song_metadata.title
|
||||||
artist_name = "; ".join([a.name for a in self.__song_metadata.artists])
|
artist_name = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
error_message = f"Download failed for '{song_title}' by '{artist_name}' (URL: {self.__link}). Original error: {str(e)}"
|
error_message = f"Download failed for '{song_title}' by '{artist_name}' (URL: {self.__link}). Original error: {str(e)}"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -334,7 +343,7 @@ class EASY_DW:
|
|||||||
# This handles cases where download_try didn't raise an exception but self.__c_track.success is still False.
|
# This handles cases where download_try didn't raise an exception but self.__c_track.success is still False.
|
||||||
if hasattr(self, '_EASY_DW__c_track') and self.__c_track and not self.__c_track.success:
|
if hasattr(self, '_EASY_DW__c_track') and self.__c_track and not self.__c_track.success:
|
||||||
song_title = self.__song_metadata.title
|
song_title = self.__song_metadata.title
|
||||||
artist_name = "; ".join([a.name for a in self.__song_metadata.artists])
|
artist_name = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
original_error_msg = getattr(self.__c_track, 'error_message', "Download failed for an unspecified reason after attempt.")
|
original_error_msg = getattr(self.__c_track, 'error_message', "Download failed for an unspecified reason after attempt.")
|
||||||
error_msg_template = "Cannot download '{title}' by '{artist}'. Reason: {reason}"
|
error_msg_template = "Cannot download '{title}' by '{artist}'. Reason: {reason}"
|
||||||
final_error_msg = error_msg_template.format(title=song_title, artist=artist_name, reason=original_error_msg)
|
final_error_msg = error_msg_template.format(title=song_title, artist=artist_name, reason=original_error_msg)
|
||||||
@@ -362,7 +371,7 @@ class EASY_DW:
|
|||||||
def download_try(self) -> Track:
|
def download_try(self) -> Track:
|
||||||
current_title = self.__song_metadata.title
|
current_title = self.__song_metadata.title
|
||||||
current_album = self.__song_metadata.album.title if self.__song_metadata.album else ''
|
current_album = self.__song_metadata.album.title if self.__song_metadata.album else ''
|
||||||
current_artist = "; ".join([a.name for a in self.__song_metadata.artists])
|
current_artist = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
|
|
||||||
# Call the new check_track_exists function from skip_detection.py
|
# Call the new check_track_exists function from skip_detection.py
|
||||||
# It needs: original_song_path, title, album, convert_to, logger
|
# It needs: original_song_path, title, album, convert_to, logger
|
||||||
@@ -571,7 +580,7 @@ class EASY_DW:
|
|||||||
os.remove(self.__song_path)
|
os.remove(self.__song_path)
|
||||||
# Add track info to exception
|
# Add track info to exception
|
||||||
track_name = self.__song_metadata.title
|
track_name = self.__song_metadata.title
|
||||||
artist_name = "; ".join([a.name for a in self.__song_metadata.artists])
|
artist_name = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
final_error_msg = f"Maximum retry limit reached for '{track_name}' by '{artist_name}' (local: {max_retries}, global: {GLOBAL_MAX_RETRIES}). Last error: {str(e)}"
|
final_error_msg = f"Maximum retry limit reached for '{track_name}' by '{artist_name}' (local: {max_retries}, global: {GLOBAL_MAX_RETRIES}). Last error: {str(e)}"
|
||||||
# Store error on track object
|
# Store error on track object
|
||||||
if hasattr(self, '_EASY_DW__c_track') and self.__c_track:
|
if hasattr(self, '_EASY_DW__c_track') and self.__c_track:
|
||||||
@@ -736,7 +745,7 @@ class EASY_DW:
|
|||||||
if os.path.exists(self.__song_path):
|
if os.path.exists(self.__song_path):
|
||||||
os.remove(self.__song_path) # Clean up partial file
|
os.remove(self.__song_path) # Clean up partial file
|
||||||
track_name = self.__song_metadata.title
|
track_name = self.__song_metadata.title
|
||||||
artist_name = "; ".join([a.name for a in self.__song_metadata.artists])
|
artist_name = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
final_error_msg = f"Maximum retry limit reached for '{track_name}' by '{artist_name}' (local: {max_retries}, global: {GLOBAL_MAX_RETRIES}). Last error: {str(e)}"
|
final_error_msg = f"Maximum retry limit reached for '{track_name}' by '{artist_name}' (local: {max_retries}, global: {GLOBAL_MAX_RETRIES}). Last error: {str(e)}"
|
||||||
if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode:
|
if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode:
|
||||||
self.__c_episode.success = False
|
self.__c_episode.success = False
|
||||||
@@ -788,7 +797,7 @@ class EASY_DW:
|
|||||||
pass
|
pass
|
||||||
unregister_active_download(self.__song_path)
|
unregister_active_download(self.__song_path)
|
||||||
episode_title = self.__song_metadata.title
|
episode_title = self.__song_metadata.title
|
||||||
artist_name = "; ".join([a.name for a in self.__song_metadata.artists])
|
artist_name = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
final_error_msg = f"Error during real-time download for episode '{episode_title}' by '{artist_name}' (URL: {self.__link}). Error: {str(e_realtime)}"
|
final_error_msg = f"Error during real-time download for episode '{episode_title}' by '{artist_name}' (URL: {self.__link}). Error: {str(e_realtime)}"
|
||||||
logger.error(final_error_msg)
|
logger.error(final_error_msg)
|
||||||
if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode:
|
if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode:
|
||||||
@@ -814,7 +823,7 @@ class EASY_DW:
|
|||||||
pass
|
pass
|
||||||
unregister_active_download(self.__song_path)
|
unregister_active_download(self.__song_path)
|
||||||
episode_title = self.__song_metadata.title
|
episode_title = self.__song_metadata.title
|
||||||
artist_name = "; ".join([a.name for a in self.__song_metadata.artists])
|
artist_name = getattr(self.__preferences, 'artist_separator', '; ').join([a.name for a in self.__song_metadata.artists])
|
||||||
final_error_msg = f"Error during standard download for episode '{episode_title}' by '{artist_name}' (URL: {self.__link}). Error: {str(e_standard)}"
|
final_error_msg = f"Error during standard download for episode '{episode_title}' by '{artist_name}' (URL: {self.__link}). Error: {str(e_standard)}"
|
||||||
logger.error(final_error_msg)
|
logger.error(final_error_msg)
|
||||||
if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode:
|
if hasattr(self, '_EASY_DW__c_episode') and self.__c_episode:
|
||||||
@@ -953,7 +962,8 @@ class DW_ALBUM:
|
|||||||
album.upc = album_obj.ids.upc
|
album.upc = album_obj.ids.upc
|
||||||
tracks = album.tracks
|
tracks = album.tracks
|
||||||
album.md5_image = self.__ids
|
album.md5_image = self.__ids
|
||||||
album.tags = _album_object_to_dict(self.__album_metadata) # For top-level album tags if needed
|
album.tags = album_object_to_dict(self.__album_metadata, source_type='spotify', artist_separator=getattr(self.__preferences, 'artist_separator', '; ')) # For top-level album tags if needed
|
||||||
|
album.tags['artist_separator'] = getattr(self.__preferences, 'artist_separator', '; ')
|
||||||
|
|
||||||
album_base_directory = get_album_directory(
|
album_base_directory = get_album_directory(
|
||||||
album.tags,
|
album.tags,
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ class SpoLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market: list[str] | None = stock_market
|
market: list[str] | None = stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Track:
|
) -> Track:
|
||||||
song_metadata = None
|
song_metadata = None
|
||||||
try:
|
try:
|
||||||
@@ -108,7 +109,7 @@ class SpoLogin:
|
|||||||
if song_metadata is None:
|
if song_metadata is None:
|
||||||
raise Exception(f"Could not retrieve metadata for track {link_track}. It might not be available or an API error occurred.")
|
raise Exception(f"Could not retrieve metadata for track {link_track}. It might not be available or an API error occurred.")
|
||||||
|
|
||||||
logger.info(f"Starting download for track: {song_metadata.title} - {'; '.join([a.name for a in song_metadata.artists])}")
|
logger.info(f"Starting download for track: {song_metadata.title} - {artist_separator.join([a.name for a in song_metadata.artists])}")
|
||||||
|
|
||||||
preferences = Preferences()
|
preferences = Preferences()
|
||||||
preferences.real_time_dl = real_time_dl
|
preferences.real_time_dl = real_time_dl
|
||||||
@@ -135,6 +136,7 @@ class SpoLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
track = DW_TRACK(preferences).dw()
|
track = DW_TRACK(preferences).dw()
|
||||||
|
|
||||||
@@ -179,7 +181,8 @@ class SpoLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market: list[str] | None = stock_market
|
market: list[str] | None = stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Album:
|
) -> Album:
|
||||||
try:
|
try:
|
||||||
link_is_valid(link_album)
|
link_is_valid(link_album)
|
||||||
@@ -192,7 +195,7 @@ class SpoLogin:
|
|||||||
if song_metadata is None:
|
if song_metadata is None:
|
||||||
raise Exception(f"Could not process album metadata for {link_album}. It might not be available in the specified market(s) or an API error occurred.")
|
raise Exception(f"Could not process album metadata for {link_album}. It might not be available in the specified market(s) or an API error occurred.")
|
||||||
|
|
||||||
logger.info(f"Starting download for album: {song_metadata.title} - {'; '.join([a.name for a in song_metadata.artists])}")
|
logger.info(f"Starting download for album: {song_metadata.title} - {artist_separator.join([a.name for a in song_metadata.artists])}")
|
||||||
|
|
||||||
preferences = Preferences()
|
preferences = Preferences()
|
||||||
preferences.real_time_dl = real_time_dl
|
preferences.real_time_dl = real_time_dl
|
||||||
@@ -221,6 +224,7 @@ class SpoLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
album = DW_ALBUM(preferences).dw()
|
album = DW_ALBUM(preferences).dw()
|
||||||
|
|
||||||
@@ -251,7 +255,8 @@ class SpoLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market: list[str] | None = stock_market
|
market: list[str] | None = stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Playlist:
|
) -> Playlist:
|
||||||
try:
|
try:
|
||||||
link_is_valid(link_playlist)
|
link_is_valid(link_playlist)
|
||||||
@@ -327,6 +332,7 @@ class SpoLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
playlist = DW_PLAYLIST(preferences).dw()
|
playlist = DW_PLAYLIST(preferences).dw()
|
||||||
|
|
||||||
@@ -356,7 +362,8 @@ class SpoLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market: list[str] | None = stock_market
|
market: list[str] | None = stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Episode:
|
) -> Episode:
|
||||||
try:
|
try:
|
||||||
link_is_valid(link_episode)
|
link_is_valid(link_episode)
|
||||||
@@ -396,6 +403,7 @@ class SpoLogin:
|
|||||||
preferences.bitrate = bitrate
|
preferences.bitrate = bitrate
|
||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
|
preferences.artist_separator = artist_separator
|
||||||
|
|
||||||
episode = DW_EPISODE(preferences).dw()
|
episode = DW_EPISODE(preferences).dw()
|
||||||
|
|
||||||
@@ -428,7 +436,8 @@ class SpoLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
market: list[str] | None = stock_market,
|
market: list[str] | None = stock_market,
|
||||||
save_cover=stock_save_cover
|
save_cover=stock_save_cover,
|
||||||
|
artist_separator: str = "; "
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Download all albums (or a subset based on album_type and limit) from an artist.
|
Download all albums (or a subset based on album_type and limit) from an artist.
|
||||||
@@ -468,7 +477,8 @@ class SpoLogin:
|
|||||||
convert_to=convert_to,
|
convert_to=convert_to,
|
||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
market=market,
|
market=market,
|
||||||
save_cover=save_cover
|
save_cover=save_cover,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
downloaded_albums.append(downloaded_album)
|
downloaded_albums.append(downloaded_album)
|
||||||
return downloaded_albums
|
return downloaded_albums
|
||||||
@@ -495,7 +505,8 @@ class SpoLogin:
|
|||||||
convert_to=None,
|
convert_to=None,
|
||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market: list[str] | None = stock_market
|
market: list[str] | None = stock_market,
|
||||||
|
artist_separator: str = "; "
|
||||||
) -> Smart:
|
) -> Smart:
|
||||||
try:
|
try:
|
||||||
link_is_valid(link)
|
link_is_valid(link)
|
||||||
@@ -528,7 +539,8 @@ class SpoLogin:
|
|||||||
convert_to=convert_to,
|
convert_to=convert_to,
|
||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market
|
market=market,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "track"
|
smart.type = "track"
|
||||||
smart.track = track
|
smart.track = track
|
||||||
@@ -554,7 +566,8 @@ class SpoLogin:
|
|||||||
convert_to=convert_to,
|
convert_to=convert_to,
|
||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market
|
market=market,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "album"
|
smart.type = "album"
|
||||||
smart.album = album
|
smart.album = album
|
||||||
@@ -580,7 +593,8 @@ class SpoLogin:
|
|||||||
convert_to=convert_to,
|
convert_to=convert_to,
|
||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market
|
market=market,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "playlist"
|
smart.type = "playlist"
|
||||||
smart.playlist = playlist
|
smart.playlist = playlist
|
||||||
@@ -605,7 +619,8 @@ class SpoLogin:
|
|||||||
convert_to=convert_to,
|
convert_to=convert_to,
|
||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market
|
market=market,
|
||||||
|
artist_separator=artist_separator
|
||||||
)
|
)
|
||||||
smart.type = "episode"
|
smart.type = "episode"
|
||||||
smart.episode = episode
|
smart.episode = episode
|
||||||
|
|||||||
Reference in New Issue
Block a user