First steps to spotify-tagging when using spo-dee flow
This commit is contained in:
@@ -44,7 +44,7 @@ from deezspot.libutils.progress_reporter import (
|
|||||||
report_album_initializing, report_album_done, report_playlist_initializing, report_playlist_done
|
report_album_initializing, report_album_done, report_playlist_initializing, report_playlist_done
|
||||||
)
|
)
|
||||||
from deezspot.libutils.taggers import (
|
from deezspot.libutils.taggers import (
|
||||||
enhance_metadata_with_image, add_deezer_enhanced_metadata, process_and_tag_track,
|
enhance_metadata_with_image, add_deezer_enhanced_metadata, add_spotify_enhanced_metadata, process_and_tag_track,
|
||||||
save_cover_image_for_track
|
save_cover_image_for_track
|
||||||
)
|
)
|
||||||
from mutagen.flac import FLAC
|
from mutagen.flac import FLAC
|
||||||
@@ -264,10 +264,16 @@ class EASY_DW:
|
|||||||
else:
|
else:
|
||||||
# Get the track object from preferences
|
# Get the track object from preferences
|
||||||
self.__track_obj: trackCbObject = preferences.song_metadata
|
self.__track_obj: trackCbObject = preferences.song_metadata
|
||||||
|
# If spotify metadata flag is set and a spotify track object is provided, prefer it for tagging
|
||||||
|
if getattr(preferences, 'spotify_metadata', False) and getattr(preferences, 'spotify_track_obj', None):
|
||||||
|
self.__track_obj = preferences.spotify_track_obj
|
||||||
|
|
||||||
# Convert it to the dictionary format needed for legacy functions
|
# Convert it to the dictionary format needed for legacy functions
|
||||||
artist_separator = getattr(preferences, 'artist_separator', '; ')
|
artist_separator = getattr(preferences, 'artist_separator', '; ')
|
||||||
self.__song_metadata_dict = track_object_to_dict(self.__track_obj, source_type='deezer', artist_separator=artist_separator)
|
# Auto-select source type based on preference
|
||||||
|
use_spotify = getattr(preferences, 'spotify_metadata', False)
|
||||||
|
source_type = 'spotify' if use_spotify else 'deezer'
|
||||||
|
self.__song_metadata_dict = track_object_to_dict(self.__track_obj, source_type=source_type, artist_separator=artist_separator)
|
||||||
# Maintain legacy attribute expected elsewhere
|
# Maintain legacy attribute expected elsewhere
|
||||||
self.__song_metadata = self.__song_metadata_dict
|
self.__song_metadata = self.__song_metadata_dict
|
||||||
self.__download_type = "track"
|
self.__download_type = "track"
|
||||||
@@ -283,7 +289,7 @@ class EASY_DW:
|
|||||||
current_track_val = None
|
current_track_val = None
|
||||||
total_tracks_val = None
|
total_tracks_val = None
|
||||||
|
|
||||||
if self.__parent == "playlist" and hasattr(self.__preferences, "json_data"):
|
if self.__parent == "playlist" and hasattr(self.__preferences, "json_data") and self.__preferences.json_data:
|
||||||
playlist_data = self.__preferences.json_data
|
playlist_data = self.__preferences.json_data
|
||||||
|
|
||||||
if isinstance(playlist_data, dict):
|
if isinstance(playlist_data, dict):
|
||||||
@@ -307,7 +313,7 @@ class EASY_DW:
|
|||||||
total_tracks_val = getattr(self.__preferences, 'total_tracks', 0)
|
total_tracks_val = getattr(self.__preferences, 'total_tracks', 0)
|
||||||
current_track_val = getattr(self.__preferences, 'track_number', 0)
|
current_track_val = getattr(self.__preferences, 'track_number', 0)
|
||||||
|
|
||||||
elif self.__parent == "album" and hasattr(self.__preferences, "json_data"):
|
elif self.__parent == "album" and hasattr(self.__preferences, "json_data") and self.__preferences.json_data:
|
||||||
album_data = self.__preferences.json_data
|
album_data = self.__preferences.json_data
|
||||||
album_id = album_data.ids.deezer
|
album_id = album_data.ids.deezer
|
||||||
parent_obj = albumCbObject(
|
parent_obj = albumCbObject(
|
||||||
@@ -400,11 +406,18 @@ class EASY_DW:
|
|||||||
|
|
||||||
def easy_dw(self) -> Track:
|
def easy_dw(self) -> Track:
|
||||||
# Get image URL and enhance metadata
|
# Get image URL and enhance metadata
|
||||||
|
pic = None
|
||||||
if self.__infos_dw.get('__TYPE__') == 'episode':
|
if self.__infos_dw.get('__TYPE__') == 'episode':
|
||||||
pic = self.__infos_dw.get('EPISODE_IMAGE_MD5', '')
|
pic = self.__infos_dw.get('EPISODE_IMAGE_MD5', '')
|
||||||
|
image = API.choose_img(pic)
|
||||||
else:
|
else:
|
||||||
pic = self.__infos_dw['ALB_PICTURE']
|
# If using Spotify metadata, prefer the best Spotify image URL from the track object
|
||||||
image = API.choose_img(pic)
|
if getattr(self.__preferences, 'spotify_metadata', False) and hasattr(self.__track_obj, 'album') and getattr(self.__track_obj.album, 'images', None):
|
||||||
|
from deezspot.libutils.metadata_converter import _get_best_image_url
|
||||||
|
image = _get_best_image_url(self.__track_obj.album.images, 'spotify')
|
||||||
|
else:
|
||||||
|
pic = self.__infos_dw['ALB_PICTURE']
|
||||||
|
image = API.choose_img(pic)
|
||||||
self.__song_metadata['image'] = image
|
self.__song_metadata['image'] = image
|
||||||
|
|
||||||
# Process image data using unified utility
|
# Process image data using unified utility
|
||||||
@@ -567,21 +580,30 @@ class EASY_DW:
|
|||||||
|
|
||||||
# If we reach here, the item should be successful and not skipped.
|
# If we reach here, the item should be successful and not skipped.
|
||||||
if current_item.success:
|
if current_item.success:
|
||||||
if self.__infos_dw.get('__TYPE__') != 'episode': # Assuming pic is for tracks
|
if self.__infos_dw.get('__TYPE__') != 'episode' and pic: # Assuming pic is for tracks
|
||||||
current_item.md5_image = pic # Set md5_image for tracks
|
current_item.md5_image = pic # Set md5_image for tracks
|
||||||
# Apply tags using unified utility with Deezer enhancements
|
# Apply tags using unified utility with Deezer or Spotify enhancements
|
||||||
from deezspot.deezloader.deegw_api import API_GW
|
from deezspot.deezloader.deegw_api import API_GW
|
||||||
enhanced_metadata = add_deezer_enhanced_metadata(
|
use_spotify = getattr(self.__preferences, 'spotify_metadata', False)
|
||||||
self.__song_metadata,
|
if use_spotify:
|
||||||
self.__infos_dw,
|
enhanced_metadata = add_spotify_enhanced_metadata(self.__song_metadata, self.__track_obj)
|
||||||
self.__ids,
|
process_and_tag_track(
|
||||||
API_GW
|
track=current_item,
|
||||||
)
|
metadata_dict=enhanced_metadata,
|
||||||
process_and_tag_track(
|
source_type='spotify'
|
||||||
track=current_item,
|
)
|
||||||
metadata_dict=enhanced_metadata,
|
else:
|
||||||
source_type='deezer'
|
enhanced_metadata = add_deezer_enhanced_metadata(
|
||||||
)
|
self.__song_metadata,
|
||||||
|
self.__infos_dw,
|
||||||
|
self.__ids,
|
||||||
|
API_GW
|
||||||
|
)
|
||||||
|
process_and_tag_track(
|
||||||
|
track=current_item,
|
||||||
|
metadata_dict=enhanced_metadata,
|
||||||
|
source_type='deezer'
|
||||||
|
)
|
||||||
|
|
||||||
return current_item
|
return current_item
|
||||||
|
|
||||||
@@ -763,20 +785,31 @@ class EASY_DW:
|
|||||||
|
|
||||||
# Add Deezer-specific enhanced metadata and apply tags
|
# Add Deezer-specific enhanced metadata and apply tags
|
||||||
from deezspot.deezloader.deegw_api import API_GW
|
from deezspot.deezloader.deegw_api import API_GW
|
||||||
enhanced_metadata = add_deezer_enhanced_metadata(
|
# Build metadata: if using Spotify metadata, enhance for Spotify; else Deezer
|
||||||
self.__song_metadata,
|
use_spotify = getattr(self.__preferences, 'spotify_metadata', False)
|
||||||
self.__infos_dw,
|
if use_spotify:
|
||||||
self.__ids,
|
enhanced_metadata = add_spotify_enhanced_metadata(self.__song_metadata, self.__track_obj)
|
||||||
API_GW
|
process_and_tag_track(
|
||||||
)
|
track=self.__c_track,
|
||||||
|
metadata_dict=enhanced_metadata,
|
||||||
# Apply tags using unified utility
|
source_type='spotify',
|
||||||
process_and_tag_track(
|
save_cover=getattr(self.__preferences, 'save_cover', False)
|
||||||
track=self.__c_track,
|
)
|
||||||
metadata_dict=enhanced_metadata,
|
else:
|
||||||
source_type='deezer',
|
enhanced_metadata = add_deezer_enhanced_metadata(
|
||||||
save_cover=getattr(self.__preferences, 'save_cover', False)
|
self.__song_metadata,
|
||||||
)
|
self.__infos_dw,
|
||||||
|
self.__ids,
|
||||||
|
API_GW
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply tags using unified utility
|
||||||
|
process_and_tag_track(
|
||||||
|
track=self.__c_track,
|
||||||
|
metadata_dict=enhanced_metadata,
|
||||||
|
source_type='deezer',
|
||||||
|
save_cover=getattr(self.__preferences, 'save_cover', False)
|
||||||
|
)
|
||||||
|
|
||||||
if self.__convert_to:
|
if self.__convert_to:
|
||||||
format_name, bitrate = self._parse_format_string(self.__convert_to)
|
format_name, bitrate = self._parse_format_string(self.__convert_to)
|
||||||
@@ -800,19 +833,28 @@ class EASY_DW:
|
|||||||
logger.error(f"Audio conversion error: {str(conv_error)}. Proceeding with original format.")
|
logger.error(f"Audio conversion error: {str(conv_error)}. Proceeding with original format.")
|
||||||
register_active_download(path_before_conversion)
|
register_active_download(path_before_conversion)
|
||||||
|
|
||||||
# Apply tags using unified utility with Deezer enhancements
|
# Apply tags using unified utility with Deezer or Spotify enhancements
|
||||||
from deezspot.deezloader.deegw_api import API_GW
|
from deezspot.deezloader.deegw_api import API_GW
|
||||||
enhanced_metadata = add_deezer_enhanced_metadata(
|
use_spotify = getattr(self.__preferences, 'spotify_metadata', False)
|
||||||
self.__song_metadata,
|
if use_spotify:
|
||||||
self.__infos_dw,
|
enhanced_metadata = add_spotify_enhanced_metadata(self.__song_metadata, self.__track_obj)
|
||||||
self.__ids,
|
process_and_tag_track(
|
||||||
API_GW
|
track=self.__c_track,
|
||||||
)
|
metadata_dict=enhanced_metadata,
|
||||||
process_and_tag_track(
|
source_type='spotify'
|
||||||
track=self.__c_track,
|
)
|
||||||
metadata_dict=enhanced_metadata,
|
else:
|
||||||
source_type='deezer'
|
enhanced_metadata = add_deezer_enhanced_metadata(
|
||||||
)
|
self.__song_metadata,
|
||||||
|
self.__infos_dw,
|
||||||
|
self.__ids,
|
||||||
|
API_GW
|
||||||
|
)
|
||||||
|
process_and_tag_track(
|
||||||
|
track=self.__c_track,
|
||||||
|
metadata_dict=enhanced_metadata,
|
||||||
|
source_type='deezer'
|
||||||
|
)
|
||||||
self.__c_track.success = True
|
self.__c_track.success = True
|
||||||
unregister_active_download(self.__song_path)
|
unregister_active_download(self.__song_path)
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ from deezspot.models.callback.common import IDs
|
|||||||
from deezspot.models.callback.user import userObject
|
from deezspot.models.callback.user import userObject
|
||||||
|
|
||||||
|
|
||||||
|
def _sim(a: str, b: str) -> float:
|
||||||
|
a = (a or '').strip().lower()
|
||||||
|
b = (b or '').strip().lower()
|
||||||
|
if not a or not b:
|
||||||
|
return 0.0
|
||||||
|
return SequenceMatcher(None, a, b).ratio()
|
||||||
|
|
||||||
API()
|
API()
|
||||||
|
|
||||||
# Create a logger for the deezspot library
|
# Create a logger for the deezspot library
|
||||||
@@ -126,7 +133,8 @@ class DeeLogin:
|
|||||||
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 = "; "
|
artist_separator: str = "; ",
|
||||||
|
spotify_metadata: bool = False
|
||||||
) -> Track:
|
) -> Track:
|
||||||
|
|
||||||
link_is_valid(link_track)
|
link_is_valid(link_track)
|
||||||
@@ -147,7 +155,7 @@ class DeeLogin:
|
|||||||
report_progress(reporter=self.progress_reporter, callback_obj=callback_obj)
|
report_progress(reporter=self.progress_reporter, callback_obj=callback_obj)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get standardized track object using our enhanced API module
|
# Default: Get standardized Deezer track object for tagging
|
||||||
track_obj = API.get_track(ids)
|
track_obj = API.get_track(ids)
|
||||||
except (NoDataApi, MarketAvailabilityError) as e:
|
except (NoDataApi, MarketAvailabilityError) as e:
|
||||||
# Try to get fallback track information
|
# Try to get fallback track information
|
||||||
@@ -173,10 +181,16 @@ class DeeLogin:
|
|||||||
report_error(e, ids, link_track)
|
report_error(e, ids, link_track)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# If requested and provided via context, override with Spotify metadata for tagging
|
||||||
|
if spotify_metadata and playlist_context and playlist_context.get('spotify_track_obj'):
|
||||||
|
track_obj_for_tagging = playlist_context.get('spotify_track_obj')
|
||||||
|
else:
|
||||||
|
track_obj_for_tagging = track_obj
|
||||||
|
|
||||||
# Set up download preferences
|
# Set up download preferences
|
||||||
preferences = Preferences()
|
preferences = Preferences()
|
||||||
preferences.link = link_track
|
preferences.link = link_track
|
||||||
preferences.song_metadata = track_obj # Use our standardized track object
|
preferences.song_metadata = track_obj_for_tagging # Use selected track object (Spotify or Deezer) for tagging
|
||||||
preferences.quality_download = quality_download
|
preferences.quality_download = quality_download
|
||||||
preferences.output_dir = output_dir
|
preferences.output_dir = output_dir
|
||||||
preferences.ids = ids
|
preferences.ids = ids
|
||||||
@@ -194,15 +208,17 @@ class DeeLogin:
|
|||||||
preferences.save_cover = save_cover
|
preferences.save_cover = save_cover
|
||||||
preferences.market = market
|
preferences.market = market
|
||||||
preferences.artist_separator = artist_separator
|
preferences.artist_separator = artist_separator
|
||||||
|
preferences.spotify_metadata = bool(spotify_metadata)
|
||||||
|
preferences.spotify_track_obj = playlist_context.get('spotify_track_obj') if (playlist_context and playlist_context.get('spotify_track_obj')) else None
|
||||||
|
|
||||||
if playlist_context:
|
if playlist_context:
|
||||||
preferences.json_data = playlist_context['json_data']
|
preferences.json_data = playlist_context.get('json_data')
|
||||||
preferences.track_number = playlist_context['track_number']
|
preferences.track_number = playlist_context.get('track_number')
|
||||||
preferences.total_tracks = playlist_context['total_tracks']
|
preferences.total_tracks = playlist_context.get('total_tracks')
|
||||||
preferences.spotify_url = playlist_context['spotify_url']
|
preferences.spotify_url = playlist_context.get('spotify_url')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parent = 'playlist' if playlist_context else None
|
parent = 'playlist' if (playlist_context and playlist_context.get('json_data')) else None
|
||||||
track = DW_TRACK(preferences, parent=parent).dw()
|
track = DW_TRACK(preferences, parent=parent).dw()
|
||||||
return track
|
return track
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -393,111 +409,58 @@ class DeeLogin:
|
|||||||
def convert_spoty_to_dee_link_track(self, link_track):
|
def convert_spoty_to_dee_link_track(self, link_track):
|
||||||
link_is_valid(link_track)
|
link_is_valid(link_track)
|
||||||
ids = get_ids(link_track)
|
ids = get_ids(link_track)
|
||||||
|
|
||||||
|
# Attempt via ISRC first
|
||||||
track_json = Spo.get_track(ids)
|
track_json = Spo.get_track(ids)
|
||||||
external_ids = track_json.get('external_ids')
|
if not track_json:
|
||||||
|
raise TrackNotFound(url=link_track, message="Spotify track metadata fetch failed.")
|
||||||
if not external_ids or 'isrc' not in external_ids:
|
external_ids = track_json.get('external_ids') or {}
|
||||||
msg = f"⚠ The track '{track_json.get('name', 'Unknown Track')}' has no ISRC and can't be converted to Deezer link :( ⚠"
|
spo_isrc = (external_ids.get('isrc') or '').upper()
|
||||||
logger.warning(msg)
|
|
||||||
raise TrackNotFound(url=link_track, message=msg)
|
|
||||||
|
|
||||||
def _sim(a: str, b: str) -> float:
|
|
||||||
a = (a or '').strip().lower()
|
|
||||||
b = (b or '').strip().lower()
|
|
||||||
if not a or not b:
|
|
||||||
return 0.0
|
|
||||||
return SequenceMatcher(None, a, b).ratio()
|
|
||||||
|
|
||||||
spo_title = track_json.get('name', '')
|
spo_title = track_json.get('name', '')
|
||||||
spo_album_title = (track_json.get('album') or {}).get('name', '')
|
spo_album_title = (track_json.get('album') or {}).get('name', '')
|
||||||
spo_tracknum = int(track_json.get('track_number') or 0)
|
spo_tracknum = int(track_json.get('track_number') or 0)
|
||||||
spo_isrc = (external_ids.get('isrc') or '').upper()
|
|
||||||
|
|
||||||
# 1) Primary attempt: /track/isrc:CODE then validate with strict checks
|
|
||||||
try:
|
try:
|
||||||
dz = API.get_track_json(f"isrc:{spo_isrc}")
|
dz = API.get_track_json(f"isrc:{spo_isrc}")
|
||||||
|
if dz and dz.get('id'):
|
||||||
|
dz_json = dz
|
||||||
|
tn = (dz_json.get('track_position') or dz_json.get('track_number') or 0)
|
||||||
|
title_match = max(
|
||||||
|
_sim(spo_title, dz_json.get('title', '')),
|
||||||
|
_sim(spo_title, dz_json.get('title_short', '')),
|
||||||
|
)
|
||||||
|
album_match = _sim(spo_album_title, (dz_json.get('album') or {}).get('title', ''))
|
||||||
|
if title_match >= 0.90 and album_match >= 0.90 and tn == spo_tracknum:
|
||||||
|
return f"https://www.deezer.com/track/{dz_json.get('id')}"
|
||||||
except Exception:
|
except Exception:
|
||||||
dz = {}
|
pass
|
||||||
|
|
||||||
def _track_ok(dz_json: dict) -> bool:
|
# Fallback: search by title + album
|
||||||
if not dz_json or not dz_json.get('id'):
|
|
||||||
return False
|
|
||||||
title_match = max(
|
|
||||||
_sim(spo_title, dz_json.get('title', '')),
|
|
||||||
_sim(spo_title, dz_json.get('title_short', '')),
|
|
||||||
)
|
|
||||||
album_match = _sim(spo_album_title, (dz_json.get('album') or {}).get('title', ''))
|
|
||||||
tn = int(dz_json.get('track_position') or dz_json.get('track_number') or 0)
|
|
||||||
return title_match >= 0.90 and album_match >= 0.90 and tn == spo_tracknum
|
|
||||||
|
|
||||||
if _track_ok(dz):
|
|
||||||
deezer_id = dz['id']
|
|
||||||
return f"https://www.deezer.com/track/{deezer_id}"
|
|
||||||
|
|
||||||
# 2) Fallback: search tracks by "title album" and validate, minimizing extra calls
|
|
||||||
query = f'"{spo_title} {spo_album_title}"'
|
query = f'"{spo_title} {spo_album_title}"'
|
||||||
try:
|
try:
|
||||||
candidates = API.search_tracks_raw(query, limit=5)
|
candidates = API.search_tracks_raw(query, limit=5)
|
||||||
except Exception:
|
except Exception:
|
||||||
candidates = []
|
candidates = []
|
||||||
|
|
||||||
for cand in candidates:
|
for cand in candidates:
|
||||||
title_match = max(
|
if max(_sim(spo_title, cand.get('title', '')), _sim(spo_title, cand.get('title_short', ''))) < 0.90:
|
||||||
_sim(spo_title, cand.get('title', '')),
|
|
||||||
_sim(spo_title, cand.get('title_short', '')),
|
|
||||||
)
|
|
||||||
album_match = _sim(spo_album_title, (cand.get('album') or {}).get('title', ''))
|
|
||||||
if title_match < 0.90 or album_match < 0.90:
|
|
||||||
continue
|
continue
|
||||||
c_id = cand.get('id')
|
c_id = cand.get('id')
|
||||||
if not c_id:
|
if not c_id:
|
||||||
continue
|
continue
|
||||||
# Fetch details only for promising candidates to check track number and ISRC
|
|
||||||
try:
|
try:
|
||||||
dzc = API.get_track_json(str(c_id))
|
dzc = API.get_track_json(str(c_id))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
tn = int(dzc.get('track_position') or dzc.get('track_number') or 0)
|
# Validate using track number and ISRC to be safe
|
||||||
|
tn = (dzc.get('track_position') or dzc.get('track_number') or 0)
|
||||||
if tn != spo_tracknum:
|
if tn != spo_tracknum:
|
||||||
continue
|
continue
|
||||||
if (dzc.get('isrc') or '').upper() != spo_isrc:
|
t_isrc = (dzc.get('isrc') or '').upper()
|
||||||
|
if spo_isrc and t_isrc and t_isrc != spo_isrc:
|
||||||
continue
|
continue
|
||||||
return f"https://www.deezer.com/track/{dzc['id']}"
|
return f"https://www.deezer.com/track/{c_id}"
|
||||||
|
|
||||||
# 3) Last resort: search albums by album title; inspect tracks to find exact track number
|
|
||||||
try:
|
|
||||||
album_candidates = API.search_albums_raw(f'"{spo_album_title}"', limit=5)
|
|
||||||
except Exception:
|
|
||||||
album_candidates = []
|
|
||||||
|
|
||||||
for alb in album_candidates:
|
|
||||||
if _sim(spo_album_title, alb.get('title', '')) < 0.90:
|
|
||||||
continue
|
|
||||||
alb_id = alb.get('id')
|
|
||||||
if not alb_id:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
# Use standardized album object to get detailed tracks (includes ISRC in IDs)
|
|
||||||
full_album = API.get_album(alb_id)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
# full_album.tracks is a list of trackAlbumObject with ids.isrc and track_number
|
|
||||||
for t in getattr(full_album, 'tracks', []) or []:
|
|
||||||
try:
|
|
||||||
t_title = getattr(t, 'title', '')
|
|
||||||
t_num = int(getattr(t, 'track_number', 0))
|
|
||||||
t_isrc = (getattr(getattr(t, 'ids', None), 'isrc', '') or '').upper()
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if t_num != spo_tracknum:
|
|
||||||
continue
|
|
||||||
if _sim(spo_title, t_title) < 0.90:
|
|
||||||
continue
|
|
||||||
if t_isrc != spo_isrc:
|
|
||||||
continue
|
|
||||||
return f"https://www.deezer.com/track/{getattr(getattr(t, 'ids', None), 'deezer', '')}"
|
|
||||||
|
|
||||||
raise TrackNotFound(url=link_track, message=f"Failed to find Deezer equivalent for ISRC {spo_isrc} from Spotify track {link_track}")
|
raise TrackNotFound(url=link_track, message=f"Failed to find Deezer equivalent for ISRC {spo_isrc} from Spotify track {link_track}")
|
||||||
|
|
||||||
def convert_isrc_to_dee_link_track(self, isrc_code: str) -> str:
|
def convert_isrc_to_dee_link_track(self, isrc_code: str) -> str:
|
||||||
@@ -526,36 +489,27 @@ class DeeLogin:
|
|||||||
def convert_spoty_to_dee_link_album(self, link_album):
|
def convert_spoty_to_dee_link_album(self, link_album):
|
||||||
link_is_valid(link_album)
|
link_is_valid(link_album)
|
||||||
ids = get_ids(link_album)
|
ids = get_ids(link_album)
|
||||||
link_dee = None
|
|
||||||
|
|
||||||
spotify_album_data = Spo.get_album(ids)
|
spotify_album_data = Spo.get_album(ids)
|
||||||
|
if not spotify_album_data:
|
||||||
def _sim(a: str, b: str) -> float:
|
raise AlbumNotFound(f"Failed to fetch Spotify album metadata for {link_album}")
|
||||||
a = (a or '').strip().lower()
|
|
||||||
b = (b or '').strip().lower()
|
|
||||||
if not a or not b:
|
|
||||||
return 0.0
|
|
||||||
return SequenceMatcher(None, a, b).ratio()
|
|
||||||
|
|
||||||
spo_album_title = spotify_album_data.get('name', '')
|
spo_album_title = spotify_album_data.get('name', '')
|
||||||
# Prefer main artist name for better search, if available
|
|
||||||
spo_artists = spotify_album_data.get('artists') or []
|
spo_artists = spotify_album_data.get('artists') or []
|
||||||
spo_main_artist = (spo_artists[0].get('name') if spo_artists else '') or ''
|
spo_main_artist = (spo_artists[0].get('name') if spo_artists else '') or ''
|
||||||
external_ids = spotify_album_data.get('external_ids') or {}
|
external_ids = spotify_album_data.get('external_ids') or {}
|
||||||
spo_upc = str(external_ids.get('upc') or '').strip()
|
spo_upc = str(external_ids.get('upc') or '').strip()
|
||||||
|
|
||||||
# 1) Primary attempt: /album/upc:CODE then validate title similarity
|
# Try UPC first
|
||||||
dz_album = {}
|
|
||||||
if spo_upc:
|
if spo_upc:
|
||||||
try:
|
try:
|
||||||
dz_album = API.get_album_json(f"upc:{spo_upc}")
|
dz_album = API.get_album_json(f"upc:{spo_upc}")
|
||||||
|
if dz_album.get('id') and _sim(spo_album_title, dz_album.get('title', '')) >= 0.90:
|
||||||
|
return f"https://www.deezer.com/album/{dz_album.get('id')}"
|
||||||
except Exception:
|
except Exception:
|
||||||
dz_album = {}
|
pass
|
||||||
if dz_album.get('id') and _sim(spo_album_title, dz_album.get('title', '')) >= 0.90:
|
|
||||||
link_dee = f"https://www.deezer.com/album/{dz_album['id']}"
|
# Fallback: title search
|
||||||
return link_dee
|
|
||||||
|
|
||||||
# 2) Fallback: search albums by album title (+ main artist) and confirm UPC
|
|
||||||
q = f'"{spo_album_title}" {spo_main_artist}'.strip()
|
q = f'"{spo_album_title}" {spo_main_artist}'.strip()
|
||||||
try:
|
try:
|
||||||
candidates = API.search_albums_raw(q, limit=5)
|
candidates = API.search_albums_raw(q, limit=5)
|
||||||
@@ -598,11 +552,26 @@ class DeeLogin:
|
|||||||
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 = "; "
|
artist_separator: str = "; ",
|
||||||
|
spotify_metadata: bool = False
|
||||||
) -> Track:
|
) -> Track:
|
||||||
|
|
||||||
link_dee = self.convert_spoty_to_dee_link_track(link_track)
|
link_dee = self.convert_spoty_to_dee_link_track(link_track)
|
||||||
|
|
||||||
|
# If requested, prepare Spotify track object for tagging in preferences via playlist_context
|
||||||
|
if spotify_metadata:
|
||||||
|
try:
|
||||||
|
from deezspot.spotloader.__spo_api__ import tracking as spo_tracking
|
||||||
|
spo_ids = get_ids(link_track)
|
||||||
|
spo_track_obj = spo_tracking(spo_ids)
|
||||||
|
if spo_track_obj:
|
||||||
|
if playlist_context is None:
|
||||||
|
playlist_context = {}
|
||||||
|
playlist_context = dict(playlist_context)
|
||||||
|
playlist_context['spotify_track_obj'] = spo_track_obj
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
track = self.download_trackdee(
|
track = self.download_trackdee(
|
||||||
link_dee,
|
link_dee,
|
||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
@@ -621,7 +590,8 @@ class DeeLogin:
|
|||||||
save_cover=save_cover,
|
save_cover=save_cover,
|
||||||
market=market,
|
market=market,
|
||||||
playlist_context=playlist_context,
|
playlist_context=playlist_context,
|
||||||
artist_separator=artist_separator
|
artist_separator=artist_separator,
|
||||||
|
spotify_metadata=spotify_metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
return track
|
return track
|
||||||
@@ -689,7 +659,8 @@ class DeeLogin:
|
|||||||
bitrate=None,
|
bitrate=None,
|
||||||
save_cover=stock_save_cover,
|
save_cover=stock_save_cover,
|
||||||
market=stock_market,
|
market=stock_market,
|
||||||
artist_separator: str = "; "
|
artist_separator: str = "; ",
|
||||||
|
spotify_metadata: bool = False
|
||||||
) -> Playlist:
|
) -> Playlist:
|
||||||
|
|
||||||
link_is_valid(link_playlist)
|
link_is_valid(link_playlist)
|
||||||
@@ -818,12 +789,19 @@ class DeeLogin:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist_context = {
|
playlist_ctx = {
|
||||||
'json_data': playlist_json,
|
'json_data': playlist_json,
|
||||||
'track_number': index,
|
'track_number': index,
|
||||||
'total_tracks': total_tracks,
|
'total_tracks': total_tracks,
|
||||||
'spotify_url': link_track
|
'spotify_url': link_track
|
||||||
}
|
}
|
||||||
|
# Attach Spotify track object for tagging if requested
|
||||||
|
if spotify_metadata:
|
||||||
|
try:
|
||||||
|
from deezspot.spotloader.__spo_api__ import json_to_track_playlist_object
|
||||||
|
playlist_ctx['spotify_track_obj'] = json_to_track_playlist_object(track_info)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
downloaded_track = self.download_trackspo(
|
downloaded_track = self.download_trackspo(
|
||||||
link_track,
|
link_track,
|
||||||
output_dir=output_dir, quality_download=quality_download,
|
output_dir=output_dir, quality_download=quality_download,
|
||||||
@@ -832,35 +810,27 @@ 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_ctx,
|
||||||
artist_separator=artist_separator
|
artist_separator=artist_separator, spotify_metadata=spotify_metadata
|
||||||
)
|
)
|
||||||
tracks.append(downloaded_track)
|
tracks.append(downloaded_track)
|
||||||
|
|
||||||
# After download, check status for summary
|
# After download, check status for summary
|
||||||
track_obj_for_cb = trackCbObject(title=track_name, artists=[artistTrackObject(name=artist_name)])
|
|
||||||
if getattr(downloaded_track, 'was_skipped', False):
|
if getattr(downloaded_track, 'was_skipped', False):
|
||||||
skipped_tracks_cb.append(track_obj_for_cb)
|
skipped_tracks_cb.append(playlist_obj.tracks[index-1])
|
||||||
elif downloaded_track.success:
|
elif downloaded_track.success:
|
||||||
successful_tracks_cb.append(track_obj_for_cb)
|
successful_tracks_cb.append(playlist_obj.tracks[index-1])
|
||||||
else:
|
else:
|
||||||
failed_tracks_cb.append(failedTrackObject(
|
failed_tracks_cb.append(failedTrackObject(track=playlist_obj.tracks[index-1], reason=getattr(downloaded_track, 'error_message', 'Unknown reason')))
|
||||||
track=track_obj_for_cb,
|
except Exception as e:
|
||||||
reason=getattr(downloaded_track, 'error_message', 'Unknown reason')
|
logger.error(f"Track '{track_name}' in playlist '{playlist_obj.title}' failed: {e}")
|
||||||
))
|
failed_tracks_cb.append(failedTrackObject(track=playlist_obj.tracks[index-1], reason=str(e)))
|
||||||
|
current_track_object = Track({'music': track_name, 'artist': artist_name}, None, None, None, link_track, None)
|
||||||
|
current_track_object.success = False
|
||||||
|
current_track_object.error_message = str(e)
|
||||||
|
tracks.append(current_track_object)
|
||||||
|
|
||||||
except (TrackNotFound, NoDataApi) as e:
|
# Finalize summary and callbacks (existing logic continues below in file)...
|
||||||
logger.error(f"Failed to download track: {track_name} - {artist_name}: {e}")
|
|
||||||
failed_track_obj = trackCbObject(title=track_name, artists=[artistTrackObject(name=artist_name)])
|
|
||||||
failed_tracks_cb.append(failedTrackObject(track=failed_track_obj, reason=str(e)))
|
|
||||||
# Create a placeholder for the failed item
|
|
||||||
failed_track = Track(
|
|
||||||
tags={'name': track_name, 'artist': artist_name},
|
|
||||||
song_path=None, file_format=None, quality=None, link=link_track, ids=None
|
|
||||||
)
|
|
||||||
failed_track.success = False
|
|
||||||
failed_track.error_message = str(e)
|
|
||||||
tracks.append(failed_track)
|
|
||||||
|
|
||||||
total_from_spotify = playlist_json['tracks']['total']
|
total_from_spotify = playlist_json['tracks']['total']
|
||||||
processed_count = len(successful_tracks_cb) + len(skipped_tracks_cb) + len(failed_tracks_cb)
|
processed_count = len(successful_tracks_cb) + len(skipped_tracks_cb) + len(failed_tracks_cb)
|
||||||
|
|||||||
@@ -22,4 +22,8 @@ class Preferences:
|
|||||||
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
|
# New: artist separator for joining multiple artists or album artists
|
||||||
self.artist_separator: str = "; "
|
self.artist_separator: str = "; "
|
||||||
|
# New: when True, use Spotify metadata for tagging in spo flows
|
||||||
|
self.spotify_metadata: bool = False
|
||||||
|
# New: optional Spotify trackObject to use when spotify_metadata is True
|
||||||
|
self.spotify_track_obj = None
|
||||||
Reference in New Issue
Block a user