Unified a bunch of download logic, fixed m3u not registering file conversion

This commit is contained in:
Xoconoch
2025-08-01 15:57:51 -06:00
parent 24cf97c032
commit 2057c9c7e8
8 changed files with 1272 additions and 527 deletions

View File

@@ -37,6 +37,17 @@ from deezspot.libutils.utils import (
save_cover_image, save_cover_image,
__get_dir as get_album_directory, __get_dir as get_album_directory,
) )
from deezspot.libutils.write_m3u import create_m3u_file, append_track_to_m3u
from deezspot.libutils.metadata_converter import track_object_to_dict, album_object_to_dict
from deezspot.libutils.progress_reporter import (
report_track_initializing, report_track_skipped, report_track_retrying,
report_track_realtime_progress, report_track_error, report_track_done,
report_album_initializing, report_album_done, report_playlist_initializing, report_playlist_done
)
from deezspot.libutils.taggers import (
enhance_metadata_with_image, add_deezer_enhanced_metadata, process_and_tag_track,
save_cover_image_for_track
)
from mutagen.flac import FLAC from mutagen.flac import FLAC
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
from mutagen.id3 import ID3 from mutagen.id3 import ID3
@@ -65,76 +76,21 @@ from deezspot.models.callback.playlist import playlistObject as playlistCbObject
from deezspot.models.callback.common import IDs from deezspot.models.callback.common import IDs
from deezspot.models.callback.user import userObject from deezspot.models.callback.user import userObject
# Use unified metadata converter
def _track_object_to_dict(track_obj: trackCbObject) -> dict: def _track_object_to_dict(track_obj: trackCbObject) -> dict:
""" """
Convert a track object to a dictionary format for tagging. Convert a track object to a dictionary format for tagging.
Similar to spotloader's approach for consistent metadata handling. Similar to spotloader's approach for consistent metadata handling.
""" """
if not track_obj: return track_object_to_dict(track_obj, source_type='deezer')
return {}
tags = {}
# Track details
tags['music'] = track_obj.title
tags['tracknum'] = track_obj.track_number if track_obj.track_number is not None else 0
tags['discnum'] = track_obj.disc_number if track_obj.disc_number is not None else 1
tags['duration'] = track_obj.duration_ms // 1000 if track_obj.duration_ms else 0
tags['explicit'] = track_obj.explicit
if track_obj.ids:
tags['ids'] = track_obj.ids.deezer
tags['isrc'] = track_obj.ids.isrc
tags['artist'] = "; ".join([artist.name for artist in track_obj.artists])
# Album details
if track_obj.album:
album = track_obj.album
tags['album'] = album.title
tags['ar_album'] = "; ".join([artist.name for artist in album.artists])
tags['nb_tracks'] = album.total_tracks
if album.release_date:
tags['year'] = album.release_date.get('year', 0)
if album.ids:
tags['upc'] = album.ids.upc
tags['album_id'] = album.ids.deezer
tags['genre'] = "; ".join(album.genres) if album.genres else ""
# Extract image information if available
if hasattr(album, 'images') and album.images:
tags['image'] = album.images
return tags
# Use unified metadata converter
def _album_object_to_dict(album_obj: albumCbObject) -> dict: def _album_object_to_dict(album_obj: albumCbObject) -> dict:
""" """
Convert an album object to a dictionary format for tagging. Convert an album object to a dictionary format for tagging.
Similar to spotloader's approach for consistent metadata handling. Similar to spotloader's approach for consistent metadata handling.
""" """
if not album_obj: return album_object_to_dict(album_obj, source_type='deezer')
return {}
tags = {}
# Album details
tags['album'] = album_obj.title
tags['ar_album'] = "; ".join([artist.name for artist in album_obj.artists])
tags['nb_tracks'] = album_obj.total_tracks
if album_obj.release_date:
tags['year'] = album_obj.release_date.get('year', 0)
if album_obj.ids:
tags['upc'] = album_obj.ids.upc
tags['album_id'] = album_obj.ids.deezer
if hasattr(album_obj, 'images') and album_obj.images:
tags['image'] = album_obj.images
tags['genre'] = "; ".join(album_obj.genres) if album_obj.genres else ""
return tags
class Download_JOB: class Download_JOB:
progress_reporter = None progress_reporter = None
@@ -367,17 +323,8 @@ class EASY_DW:
expected by legacy tagging and path functions. expected by legacy tagging and path functions.
It intelligently finds the album information based on the download context. It intelligently finds the album information based on the download context.
""" """
# Use the new global helper function for basic conversion # Use the unified metadata converter
metadata_dict = _track_object_to_dict(track_obj) metadata_dict = track_object_to_dict(track_obj, source_type='deezer')
# Add Deezer-specific metadata handling that isn't in the basic helper
# For full album downloads, use complete album data from preferences if available
if self.__parent == 'album' and hasattr(self.__preferences, 'json_data'):
album_data_source = self.__preferences.json_data
# Update album-related fields with more complete information
if hasattr(album_data_source, 'label'):
metadata_dict['label'] = album_data_source.label
# 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
@@ -387,13 +334,6 @@ class EASY_DW:
if 'disk_number' in self.__infos_dw: if 'disk_number' in self.__infos_dw:
metadata_dict['discnum'] = self.__infos_dw['disk_number'] metadata_dict['discnum'] = self.__infos_dw['disk_number']
# Check for contributors from original API data
if 'contributors' in self.__infos_dw:
# Filter for main artists only
main_artists = [c['name'] for c in self.__infos_dw['contributors'] if c.get('role') == 'Main']
if main_artists:
metadata_dict['album_artist'] = "; ".join(main_artists)
return metadata_dict return metadata_dict
def __set_quality(self) -> None: def __set_quality(self) -> None:
@@ -454,12 +394,16 @@ class EASY_DW:
self.__c_episode.set_fallback_ids(self.__fallback_ids) self.__c_episode.set_fallback_ids(self.__fallback_ids)
def easy_dw(self) -> Track: def easy_dw(self) -> Track:
# Get image URL and enhance metadata
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', '')
else: else:
pic = self.__infos_dw['ALB_PICTURE'] pic = self.__infos_dw['ALB_PICTURE']
image = API.choose_img(pic) image = API.choose_img(pic)
self.__song_metadata['image'] = image self.__song_metadata['image'] = image
# Process image data using unified utility
self.__song_metadata = enhance_metadata_with_image(self.__song_metadata)
song = f"{self.__song_metadata['music']} - {self.__song_metadata['artist']}" song = f"{self.__song_metadata['music']} - {self.__song_metadata['artist']}"
# Check if track already exists based on metadata # Check if track already exists based on metadata
@@ -489,33 +433,16 @@ class EASY_DW:
parent_obj, current_track_val, total_tracks_val = self._get_parent_context() parent_obj, current_track_val, total_tracks_val = self._get_parent_context()
status_obj = skippedObject( # Report track skipped status
ids=self.__track_obj.ids, report_track_skipped(
track_obj=self.__track_obj,
reason=f"Track already exists in desired format at {existing_file_path}", reason=f"Track already exists in desired format at {existing_file_path}",
convert_to=self.__convert_to, preferences=self.__preferences,
bitrate=self.__bitrate parent_obj=parent_obj,
)
callback_obj = trackCallbackObject(
track=self.__track_obj,
status_info=status_obj,
parent=parent_obj,
current_track=current_track_val, current_track=current_track_val,
total_tracks=total_tracks_val total_tracks=total_tracks_val
) )
if self.__parent is None:
summary = summaryObject(
skipped_tracks=[self.__track_obj],
total_skipped=1
)
status_obj.summary = summary
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
)
skipped_item = Track( skipped_item = Track(
self.__song_metadata, self.__song_metadata,
existing_file_path, # Use the path of the existing file existing_file_path, # Use the path of the existing file
@@ -624,7 +551,19 @@ class EASY_DW:
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': # 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
write_tags(current_item) # Apply tags using unified utility with Deezer enhancements
from deezspot.deezloader.dee_api import API_GW
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
@@ -710,21 +649,15 @@ class EASY_DW:
try: try:
self.__write_track() self.__write_track()
status_obj = initializingObject(ids=self.__track_obj.ids) # Report track initialization status
report_track_initializing(
callback_obj = trackCallbackObject( track_obj=self.__track_obj,
track=self.__track_obj, preferences=self.__preferences,
status_info=status_obj, parent_obj=parent_obj,
parent=parent_obj,
current_track=current_track_val, current_track=current_track_val,
total_tracks=total_tracks_val total_tracks=total_tracks_val
) )
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
)
register_active_download(self.__song_path) register_active_download(self.__song_path)
try: try:
decryptfile(c_crypted_audio, self.__fallback_ids, self.__song_path) decryptfile(c_crypted_audio, self.__fallback_ids, self.__song_path)
@@ -761,16 +694,22 @@ class EASY_DW:
raise TrackNotFound(f"Failed to process {self.__song_path}. Error: {str(e_decrypt)}") from e_decrypt raise TrackNotFound(f"Failed to process {self.__song_path}. Error: {str(e_decrypt)}") from e_decrypt
self.__add_more_tags() # Add Deezer-specific enhanced metadata and apply tags
self.__c_track.tags = self.__song_metadata from deezspot.deezloader.dee_api import API_GW
enhanced_metadata = add_deezer_enhanced_metadata(
self.__song_metadata,
self.__infos_dw,
self.__ids,
API_GW
)
if self.__preferences.save_cover and self.__song_metadata.get('image'): # Apply tags using unified utility
try: process_and_tag_track(
track_directory = os.path.dirname(self.__song_path) track=self.__c_track,
save_cover_image(self.__song_metadata['image'], track_directory, "cover.jpg") metadata_dict=enhanced_metadata,
logger.info(f"Saved cover image for track in {track_directory}") source_type='deezer',
except Exception as e_img_save: save_cover=getattr(self.__preferences, 'save_cover', False)
logger.warning(f"Failed to save cover image for track: {e_img_save}") )
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)
@@ -794,8 +733,19 @@ 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
write_tags(self.__c_track) from deezspot.deezloader.dee_api import API_GW
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)
@@ -896,7 +846,19 @@ class EASY_DW:
self.__c_track.success = True self.__c_track.success = True
self.__write_episode() self.__write_episode()
write_tags(self.__c_track) # Apply tags using unified utility with Deezer enhancements
from deezspot.deezloader.dee_api import API_GW
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'
)
return self.__c_track return self.__c_track
@@ -954,49 +916,7 @@ class EASY_DW:
return format_name, bitrate return format_name, bitrate
def __add_more_tags(self) -> None: # Removed __add_more_tags() - now handled by unified libutils/taggers.py
"""
Add Deezer-specific metadata to the song metadata dictionary.
This preserves Deezer's unique features like lyrics and contributors.
"""
contributors = self.__infos_dw.get('SNG_CONTRIBUTORS', {})
# Add contributor information
self.__song_metadata['author'] = "; ".join(contributors.get('author', []))
self.__song_metadata['composer'] = "; ".join(contributors.get('composer', []))
self.__song_metadata['lyricist'] = "; ".join(contributors.get('lyricist', []))
if contributors.get('composerlyricist'):
self.__song_metadata['composer'] = "; ".join(contributors.get('composerlyricist', []))
# Add version information if available
self.__song_metadata['version'] = self.__infos_dw.get('VERSION', '')
# Initialize lyric fields
self.__song_metadata['lyric'] = ""
self.__song_metadata['copyright'] = ""
self.__song_metadata['lyric_sync'] = []
# Add lyrics if available
if self.__infos_dw.get('LYRICS_ID', 0) != 0:
try:
lyrics_data = API_GW.get_lyric(self.__ids)
if lyrics_data and "LYRICS_TEXT" in lyrics_data:
self.__song_metadata['lyric'] = lyrics_data["LYRICS_TEXT"]
if lyrics_data and "LYRICS_SYNC_JSON" in lyrics_data:
self.__song_metadata['lyric_sync'] = trasform_sync_lyric(
lyrics_data['LYRICS_SYNC_JSON']
)
except Exception as e:
logger.warning(f"Failed to retrieve lyrics: {str(e)}")
# Extract album artist from contributors with 'Main' role
if 'contributors' in self.__infos_dw:
main_artists = [c['name'] for c in self.__infos_dw['contributors'] if c.get('role') == 'Main']
if main_artists:
self.__song_metadata['album_artist'] = "; ".join(main_artists)
class DW_TRACK: class DW_TRACK:
def __init__( def __init__(
@@ -1030,8 +950,8 @@ class DW_TRACK:
class DW_ALBUM: 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 new global helper function # Use the unified metadata converter
return _album_object_to_dict(album_obj) return album_object_to_dict(album_obj, source_type='deezer')
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."""
@@ -1050,24 +970,11 @@ class DW_ALBUM:
album=album_obj, # Use the parent album album=album_obj, # Use the parent album
genres=getattr(track_obj, 'genres', []) genres=getattr(track_obj, 'genres', [])
) )
# Now use the new track object with the global helper # Use the unified metadata converter
track_dict = _track_object_to_dict(full_track) return track_object_to_dict(full_track, source_type='deezer')
else: else:
# Use the global helper function with additional album context # Use the unified metadata converter
track_dict = _track_object_to_dict(track_obj) return track_object_to_dict(track_obj, source_type='deezer')
# Add album-specific context that might not be in the track object
if album_obj:
track_dict['album'] = album_obj.title
track_dict['album_artist'] = "; ".join([artist.name for artist in album_obj.artists])
track_dict['upc'] = album_obj.ids.upc if album_obj.ids else None
track_dict['genre'] = "; ".join(album_obj.genres)
# Preserve ISRC if available in the track object
if track_obj.ids and track_obj.ids.isrc and not track_dict.get('isrc'):
track_dict['isrc'] = track_obj.ids.isrc
return track_dict
def __init__( def __init__(
self, self,
@@ -1087,13 +994,8 @@ class DW_ALBUM:
def dw(self) -> Album: def dw(self) -> Album:
# Report album initializing status # Report album initializing status
album_obj = self.__preferences.json_data album_obj = self.__preferences.json_data
status_obj = initializingObject(ids=album_obj.ids) # Report album initialization status
callback_obj = albumCallbackObject(album=album_obj, status_info=status_obj) report_album_initializing(album_obj)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
)
infos_dw = API_GW.get_album_data(self.__ids)['data'] infos_dw = API_GW.get_album_data(self.__ids)['data']
md5_image = infos_dw[0]['ALB_PICTURE'] md5_image = infos_dw[0]['ALB_PICTURE']
@@ -1127,7 +1029,6 @@ class DW_ALBUM:
total_tracks = len(infos_dw) total_tracks = len(infos_dw)
for a, album_track_obj in enumerate(album_obj.tracks): for a, album_track_obj in enumerate(album_obj.tracks):
track_number = a + 1
c_infos_dw_item = infos_dw[a] c_infos_dw_item = infos_dw[a]
# Update track object with proper track position and disc number from API # Update track object with proper track position and disc number from API
@@ -1138,7 +1039,7 @@ class DW_ALBUM:
# Ensure we have valid values, not None # Ensure we have valid values, not None
if album_track_obj.track_number is None: if album_track_obj.track_number is None:
album_track_obj.track_number = track_number album_track_obj.track_number = a + 1 # Fallback to sequential if API doesn't provide
if album_track_obj.disc_number is None: if album_track_obj.disc_number is None:
album_track_obj.disc_number = 1 album_track_obj.disc_number = 1
@@ -1162,7 +1063,7 @@ class DW_ALBUM:
c_preferences.song_metadata = full_track_obj c_preferences.song_metadata = full_track_obj
c_preferences.ids = full_track_obj.ids.deezer c_preferences.ids = full_track_obj.ids.deezer
c_preferences.track_number = track_number c_preferences.track_number = a + 1 # For progress reporting only
c_preferences.total_tracks = total_tracks c_preferences.total_tracks = total_tracks
c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}" c_preferences.link = f"https://deezer.com/track/{c_preferences.ids}"
@@ -1214,12 +1115,8 @@ class DW_ALBUM:
total_failed=len(failed_tracks_cb) total_failed=len(failed_tracks_cb)
) )
status_obj_done = doneObject(ids=album_obj.ids, summary=summary_obj) # Report album completion status
callback_obj_done = albumCallbackObject(album=album_obj, status_info=status_obj_done) report_album_done(album_obj, summary_obj)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj_done
)
return album return album
@@ -1238,22 +1135,8 @@ class DW_PLAYLIST:
self.__quality_download = self.__preferences.quality_download self.__quality_download = self.__preferences.quality_download
def _track_object_to_dict(self, track_obj: any) -> dict: def _track_object_to_dict(self, track_obj: any) -> dict:
return { # Use the unified metadata converter
"music": track_obj.title, return track_object_to_dict(track_obj, source_type='deezer')
"artist": "; ".join([artist.name for artist in track_obj.artists]),
"album": track_obj.album.title,
"tracknum": 0,
"discnum": 0,
"duration": track_obj.duration_ms // 1000 if track_obj.duration_ms else 0,
"year": 0,
"explicit": False,
"isrc": track_obj.ids.isrc if hasattr(track_obj.ids, 'isrc') else None,
"album_artist": "",
"upc": None,
"label": "",
"genre": "",
"image": None,
}
def dw(self) -> Playlist: def dw(self) -> Playlist:
playlist_obj: playlistCbObject = self.__preferences.json_data playlist_obj: playlistCbObject = self.__preferences.json_data
@@ -1271,12 +1154,7 @@ class DW_PLAYLIST:
playlist = Playlist() playlist = Playlist()
tracks = playlist.tracks tracks = playlist.tracks
playlist_m3u_dir = os.path.join(self.__output_dir, "playlists") m3u_path = create_m3u_file(self.__output_dir, playlist_obj.title)
os.makedirs(playlist_m3u_dir, exist_ok=True)
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
if not os.path.exists(m3u_path):
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
m3u_file.write("#EXTM3U\n")
medias = Download_JOB.check_sources(infos_dw, self.__quality_download) medias = Download_JOB.check_sources(infos_dw, self.__quality_download)
@@ -1341,12 +1219,7 @@ class DW_PLAYLIST:
if current_track_object: if current_track_object:
tracks.append(current_track_object) tracks.append(current_track_object)
if current_track_object.success and hasattr(current_track_object, 'song_path') and current_track_object.song_path: if current_track_object.success and hasattr(current_track_object, 'song_path') and current_track_object.song_path:
relative_song_path = os.path.relpath( append_track_to_m3u(m3u_path, current_track_object.song_path)
current_track_object.song_path,
start=playlist_m3u_dir
)
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
m3u_file.write(f"{relative_song_path}\n")
if self.__make_zip: if self.__make_zip:
zip_name = f"{self.__output_dir}/{playlist_obj.title} [playlist {self.__ids}]" zip_name = f"{self.__output_dir}/{playlist_obj.title} [playlist {self.__ids}]"

View File

@@ -796,16 +796,8 @@ class DeeLogin:
callback_obj_done = playlistCallbackObject(playlist=playlist_obj, status_info=status_obj_done) callback_obj_done = playlistCallbackObject(playlist=playlist_obj, status_info=status_obj_done)
report_progress(reporter=self.progress_reporter, callback_obj=callback_obj_done) report_progress(reporter=self.progress_reporter, callback_obj=callback_obj_done)
playlist_m3u_dir = os.path.join(output_dir, "playlists") from deezspot.libutils.write_m3u import write_tracks_to_m3u
os.makedirs(playlist_m3u_dir, exist_ok=True) m3u_path = write_tracks_to_m3u(output_dir, playlist_obj.title, tracks)
m3u_path = os.path.join(playlist_m3u_dir, f"{sanitize_name(playlist_obj.title)}.m3u")
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
m3u_file.write("#EXTM3U\n")
for track in tracks:
if isinstance(track, Track) and track.success and hasattr(track, 'song_path') and track.song_path:
relative_song_path = os.path.relpath(track.song_path, start=playlist_m3u_dir)
m3u_file.write(f"{relative_song_path}\n")
logger.info(f"Created m3u playlist file at: {m3u_path}")
if make_zip: if make_zip:
zip_name = f"{output_dir}/playlist_{sanitize_name(playlist_obj.title)}.zip" zip_name = f"{output_dir}/playlist_{sanitize_name(playlist_obj.title)}.zip"

View File

@@ -0,0 +1,278 @@
#!/usr/bin/python3
from datetime import datetime
from typing import Dict, Any, Optional, Union
def _detect_source_type(obj: Any) -> str:
"""
Auto-detect whether an object is from Spotify or Deezer based on its IDs.
Args:
obj: Track or album object with ids attribute
Returns:
str: 'spotify' or 'deezer'
"""
if hasattr(obj, 'ids') and obj.ids:
if hasattr(obj.ids, 'spotify') and obj.ids.spotify:
return 'spotify'
elif hasattr(obj.ids, 'deezer') and obj.ids.deezer:
return 'deezer'
return 'spotify' # Default fallback
def _get_platform_id(ids_obj: Any, source_type: str) -> Optional[str]:
"""
Get the appropriate platform ID based on source type.
Args:
ids_obj: IDs object containing platform-specific IDs
source_type: 'spotify' or 'deezer'
Returns:
Platform-specific ID or None
"""
if not ids_obj:
return None
if source_type == 'spotify':
return getattr(ids_obj, 'spotify', None)
else: # deezer
return getattr(ids_obj, 'deezer', None)
def _format_release_date(release_date: Any, source_type: str) -> Optional[datetime]:
"""
Format release date based on source type and data structure.
Args:
release_date: Release date object/dict from API
source_type: 'spotify' or 'deezer'
Returns:
datetime object or None
"""
if not release_date:
return None
try:
if source_type == 'spotify' and isinstance(release_date, dict):
# Spotify format: create datetime object
if 'year' in release_date:
return datetime(
year=release_date['year'],
month=release_date.get('month', 1),
day=release_date.get('day', 1)
)
elif source_type == 'deezer':
# Deezer format: extract year only
if isinstance(release_date, dict) and 'year' in release_date:
return datetime(year=release_date['year'], month=1, day=1)
elif hasattr(release_date, 'year'):
return datetime(year=release_date.year, month=1, day=1)
except (ValueError, TypeError, AttributeError):
pass
return None
def _get_best_image_url(images: Any, source_type: str) -> Optional[str]:
"""
Get the best image URL based on source type and format.
Args:
images: Images data from API
source_type: 'spotify' or 'deezer'
Returns:
Best image URL or None
"""
if not images:
return None
if source_type == 'spotify' and isinstance(images, list):
# Spotify: find highest resolution image
best_image = max(images, key=lambda i: i.get('height', 0) * i.get('width', 0), default=None)
return best_image.get('url') if best_image else None
elif source_type == 'deezer':
# Deezer: images might be direct URL or object
if isinstance(images, str):
return images
elif isinstance(images, list) and images:
return images[0] if isinstance(images[0], str) else None
elif hasattr(images, 'url'):
return images.url
return None
def track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> Dict[str, Any]:
"""
Convert a track object to a dictionary format for tagging.
Supports both Spotify and Deezer track objects.
Args:
track_obj: Track object from Spotify or Deezer API
source_type: Optional source type ('spotify' or 'deezer'). If None, auto-detected.
Returns:
Dictionary with standardized metadata tags
"""
if not track_obj:
return {}
# Auto-detect source if not specified
if source_type is None:
source_type = _detect_source_type(track_obj)
tags = {}
# Basic track details
tags['music'] = getattr(track_obj, 'title', '')
tags['tracknum'] = getattr(track_obj, 'track_number', 0) if getattr(track_obj, 'track_number', None) is not None else 0
tags['discnum'] = getattr(track_obj, 'disc_number', 1) if getattr(track_obj, 'disc_number', None) is not None else 1
tags['duration'] = (getattr(track_obj, 'duration_ms', 0) // 1000) if getattr(track_obj, 'duration_ms', None) else 0
# Platform-specific IDs
if hasattr(track_obj, 'ids') and track_obj.ids:
tags['ids'] = _get_platform_id(track_obj.ids, source_type)
tags['isrc'] = getattr(track_obj.ids, 'isrc', None)
# Artist information
if hasattr(track_obj, 'artists') and track_obj.artists:
tags['artist'] = "; ".join([getattr(artist, 'name', '') for artist in track_obj.artists])
else:
tags['artist'] = ''
# Explicit flag (mainly for Deezer)
if hasattr(track_obj, 'explicit'):
tags['explicit'] = track_obj.explicit
# Album details
if hasattr(track_obj, 'album') and track_obj.album:
album = track_obj.album
tags['album'] = getattr(album, 'title', '')
# Album artists
if hasattr(album, 'artists') and album.artists:
tags['ar_album'] = "; ".join([getattr(artist, 'name', '') for artist in album.artists])
else:
tags['ar_album'] = ''
tags['nb_tracks'] = getattr(album, 'total_tracks', 0)
# Calculate total discs from all tracks in album for proper metadata
if hasattr(album, 'tracks') and album.tracks:
disc_numbers = [getattr(track, 'disc_number', 1) for track in album.tracks if hasattr(track, 'disc_number')]
tags['nb_discs'] = max(disc_numbers, default=1)
else:
tags['nb_discs'] = 1
# Release date handling
release_date = getattr(album, 'release_date', None)
tags['year'] = _format_release_date(release_date, source_type)
# Platform-specific album IDs
if hasattr(album, 'ids') and album.ids:
tags['upc'] = getattr(album.ids, 'upc', None)
tags['album_id'] = _get_platform_id(album.ids, source_type)
# Image handling
if hasattr(album, 'images'):
tags['image'] = _get_best_image_url(album.images, source_type)
else:
tags['image'] = None
# Additional album metadata
tags['label'] = getattr(album, 'label', '')
tags['copyright'] = getattr(album, 'copyright', '')
# Genre handling (more common in Deezer)
if hasattr(album, 'genres') and album.genres:
tags['genre'] = "; ".join(album.genres) if isinstance(album.genres, list) else str(album.genres)
else:
tags['genre'] = ""
# Default/Placeholder values for compatibility
tags['bpm'] = tags.get('bpm', 'Unknown')
tags['gain'] = tags.get('gain', 'Unknown')
tags['lyric'] = tags.get('lyric', '')
tags['author'] = tags.get('author', '')
tags['composer'] = tags.get('composer', '')
tags['lyricist'] = tags.get('lyricist', '')
tags['version'] = tags.get('version', '')
return tags
def album_object_to_dict(album_obj: Any, source_type: Optional[str] = None) -> Dict[str, Any]:
"""
Convert an album object to a dictionary format for tagging.
Supports both Spotify and Deezer album objects.
Args:
album_obj: Album object from Spotify or Deezer API
source_type: Optional source type ('spotify' or 'deezer'). If None, auto-detected.
Returns:
Dictionary with standardized metadata tags
"""
if not album_obj:
return {}
# Auto-detect source if not specified
if source_type is None:
source_type = _detect_source_type(album_obj)
tags = {}
# Basic album details
tags['album'] = getattr(album_obj, 'title', '')
# Album artists
if hasattr(album_obj, 'artists') and album_obj.artists:
tags['ar_album'] = "; ".join([getattr(artist, 'name', '') for artist in album_obj.artists])
else:
tags['ar_album'] = ''
tags['nb_tracks'] = getattr(album_obj, 'total_tracks', 0)
# Release date handling
release_date = getattr(album_obj, 'release_date', None)
tags['year'] = _format_release_date(release_date, source_type)
# Platform-specific album IDs
if hasattr(album_obj, 'ids') and album_obj.ids:
tags['upc'] = getattr(album_obj.ids, 'upc', None)
tags['album_id'] = _get_platform_id(album_obj.ids, source_type)
# Image handling
if hasattr(album_obj, 'images'):
tags['image'] = _get_best_image_url(album_obj.images, source_type)
else:
tags['image'] = None
# Additional metadata
tags['label'] = getattr(album_obj, 'label', '')
tags['copyright'] = getattr(album_obj, 'copyright', '')
# Genre handling (more common in Deezer)
if hasattr(album_obj, 'genres') and album_obj.genres:
tags['genre'] = "; ".join(album_obj.genres) if isinstance(album_obj.genres, list) else str(album_obj.genres)
else:
tags['genre'] = ""
return tags
# Backward compatibility aliases for easy migration
def _track_object_to_dict(track_obj: Any, source_type: Optional[str] = None) -> Dict[str, Any]:
"""Legacy alias for track_object_to_dict"""
return track_object_to_dict(track_obj, source_type)
def _album_object_to_dict(album_obj: Any, source_type: Optional[str] = None) -> Dict[str, Any]:
"""Legacy alias for album_object_to_dict"""
return album_object_to_dict(album_obj, source_type)

View File

@@ -0,0 +1,369 @@
#!/usr/bin/python3
from typing import Optional, Any, Dict, List
from deezspot.libutils.logging_utils import report_progress
from deezspot.models.callback import (
trackCallbackObject, albumCallbackObject, playlistCallbackObject,
initializingObject, skippedObject, retryingObject, realTimeObject,
errorObject, doneObject, summaryObject
)
def _get_reporter():
"""Get the active progress reporter from Download_JOB"""
# Import here to avoid circular imports
try:
from deezspot.spotloader.__download__ import Download_JOB as SpotifyDJ
if hasattr(SpotifyDJ, 'progress_reporter') and SpotifyDJ.progress_reporter:
return SpotifyDJ.progress_reporter
except ImportError:
pass
try:
from deezspot.deezloader.__download__ import Download_JOB as DeezerDJ
if hasattr(DeezerDJ, 'progress_reporter') and DeezerDJ.progress_reporter:
return DeezerDJ.progress_reporter
except ImportError:
pass
return None
def report_track_initializing(
track_obj: Any,
preferences: Any,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None
) -> None:
"""
Report track initialization status.
Args:
track_obj: Track object being initialized
preferences: Preferences object with convert_to/bitrate info
parent_obj: Parent object (album/playlist) if applicable
current_track: Current track number for progress
total_tracks: Total tracks for progress
"""
status_obj = initializingObject(
ids=getattr(track_obj, 'ids', None),
convert_to=getattr(preferences, 'convert_to', None),
bitrate=getattr(preferences, 'bitrate', None)
)
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track or getattr(preferences, 'track_number', None),
total_tracks=total_tracks,
parent=parent_obj
)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_track_skipped(
track_obj: Any,
reason: str,
preferences: Any,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None
) -> None:
"""
Report track skipped status.
Args:
track_obj: Track object being skipped
reason: Reason for skipping
preferences: Preferences object with convert_to/bitrate info
parent_obj: Parent object (album/playlist) if applicable
current_track: Current track number for progress
total_tracks: Total tracks for progress
"""
status_obj = skippedObject(
ids=getattr(track_obj, 'ids', None),
reason=reason,
convert_to=getattr(preferences, 'convert_to', None),
bitrate=getattr(preferences, 'bitrate', None)
)
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track or getattr(preferences, 'track_number', None),
total_tracks=total_tracks,
parent=parent_obj
)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_track_retrying(
track_obj: Any,
retry_count: int,
seconds_left: int,
error: str,
preferences: Any,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None
) -> None:
"""
Report track retry status.
Args:
track_obj: Track object being retried
retry_count: Current retry attempt number
seconds_left: Seconds until next retry
error: Error that caused the retry
preferences: Preferences object with convert_to/bitrate info
parent_obj: Parent object (album/playlist) if applicable
current_track: Current track number for progress
total_tracks: Total tracks for progress
"""
status_obj = retryingObject(
ids=getattr(track_obj, 'ids', None),
retry_count=retry_count,
seconds_left=seconds_left,
error=error,
convert_to=getattr(preferences, 'convert_to', None),
bitrate=getattr(preferences, 'bitrate', None)
)
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track or getattr(preferences, 'track_number', None),
total_tracks=total_tracks,
parent=parent_obj
)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_track_realtime_progress(
track_obj: Any,
time_elapsed: int,
progress: int,
preferences: Any,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None
) -> None:
"""
Report real-time track download progress.
Args:
track_obj: Track object being downloaded
time_elapsed: Milliseconds elapsed
progress: Progress percentage (0-100)
preferences: Preferences object with convert_to/bitrate info
parent_obj: Parent object (album/playlist) if applicable
current_track: Current track number for progress
total_tracks: Total tracks for progress
"""
status_obj = realTimeObject(
ids=getattr(track_obj, 'ids', None),
time_elapsed=time_elapsed,
progress=progress,
convert_to=getattr(preferences, 'convert_to', None),
bitrate=getattr(preferences, 'bitrate', None)
)
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track or getattr(preferences, 'track_number', None),
total_tracks=total_tracks,
parent=parent_obj
)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_track_error(
track_obj: Any,
error: str,
preferences: Any,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None
) -> None:
"""
Report track error status.
Args:
track_obj: Track object that errored
error: Error message
preferences: Preferences object with convert_to/bitrate info
parent_obj: Parent object (album/playlist) if applicable
current_track: Current track number for progress
total_tracks: Total tracks for progress
"""
status_obj = errorObject(
ids=getattr(track_obj, 'ids', None),
error=error,
convert_to=getattr(preferences, 'convert_to', None),
bitrate=getattr(preferences, 'bitrate', None)
)
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track or getattr(preferences, 'track_number', None),
total_tracks=total_tracks,
parent=parent_obj
)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_track_done(
track_obj: Any,
preferences: Any,
summary: Optional[summaryObject] = None,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None
) -> None:
"""
Report track completion status.
Args:
track_obj: Track object that completed
preferences: Preferences object with convert_to/bitrate info
summary: Optional summary object for single track downloads
parent_obj: Parent object (album/playlist) if applicable
current_track: Current track number for progress
total_tracks: Total tracks for progress
"""
status_obj = doneObject(
ids=getattr(track_obj, 'ids', None),
summary=summary,
convert_to=getattr(preferences, 'convert_to', None),
bitrate=getattr(preferences, 'bitrate', None)
)
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track or getattr(preferences, 'track_number', None),
total_tracks=total_tracks,
parent=parent_obj
)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_album_initializing(album_obj: Any) -> None:
"""
Report album initialization status.
Args:
album_obj: Album object being initialized
"""
status_obj = initializingObject(ids=getattr(album_obj, 'ids', None))
callback_obj = albumCallbackObject(album=album_obj, status_info=status_obj)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_album_done(album_obj: Any, summary: summaryObject) -> None:
"""
Report album completion status.
Args:
album_obj: Album object that completed
summary: Summary of track download results
"""
status_obj = doneObject(ids=getattr(album_obj, 'ids', None), summary=summary)
callback_obj = albumCallbackObject(album=album_obj, status_info=status_obj)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_playlist_initializing(playlist_obj: Any) -> None:
"""
Report playlist initialization status.
Args:
playlist_obj: Playlist object being initialized
"""
status_obj = initializingObject(ids=getattr(playlist_obj, 'ids', None))
callback_obj = playlistCallbackObject(playlist=playlist_obj, status_info=status_obj)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
def report_playlist_done(playlist_obj: Any, summary: summaryObject) -> None:
"""
Report playlist completion status.
Args:
playlist_obj: Playlist object that completed
summary: Summary of track download results
"""
status_obj = doneObject(ids=getattr(playlist_obj, 'ids', None), summary=summary)
callback_obj = playlistCallbackObject(playlist=playlist_obj, status_info=status_obj)
reporter = _get_reporter()
if reporter:
report_progress(reporter=reporter, callback_obj=callback_obj)
# Convenience function for generic track reporting
def report_track_status(
status_type: str,
track_obj: Any,
preferences: Any,
parent_obj: Optional[Any] = None,
current_track: Optional[int] = None,
total_tracks: Optional[int] = None,
**kwargs
) -> None:
"""
Generic track status reporting function.
Args:
status_type: Type of status ('initializing', 'skipped', 'retrying', 'error', 'done', 'realtime')
track_obj: Track object
preferences: Preferences object
parent_obj: Parent object if applicable
current_track: Current track number
total_tracks: Total tracks
**kwargs: Additional parameters specific to status type
"""
if status_type == 'initializing':
report_track_initializing(track_obj, preferences, parent_obj, current_track, total_tracks)
elif status_type == 'skipped':
report_track_skipped(track_obj, kwargs.get('reason', 'Unknown'), preferences, parent_obj, current_track, total_tracks)
elif status_type == 'retrying':
report_track_retrying(track_obj, kwargs.get('retry_count', 0), kwargs.get('seconds_left', 0),
kwargs.get('error', 'Unknown'), preferences, parent_obj, current_track, total_tracks)
elif status_type == 'error':
report_track_error(track_obj, kwargs.get('error', 'Unknown'), preferences, parent_obj, current_track, total_tracks)
elif status_type == 'done':
report_track_done(track_obj, preferences, kwargs.get('summary'), parent_obj, current_track, total_tracks)
elif status_type == 'realtime':
report_track_realtime_progress(track_obj, kwargs.get('time_elapsed', 0), kwargs.get('progress', 0),
preferences, parent_obj, current_track, total_tracks)

View File

@@ -4,6 +4,7 @@ import os
from mutagen import File from mutagen import File
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
from mutagen.oggvorbis import OggVorbis from mutagen.oggvorbis import OggVorbis
from mutagen.oggopus import OggOpus
from mutagen.flac import FLAC from mutagen.flac import FLAC
from mutagen.mp3 import MP3 # Added for explicit MP3 type checking from mutagen.mp3 import MP3 # Added for explicit MP3 type checking
# from mutagen.mp4 import MP4 # MP4 is usually handled by File for .m4a # from mutagen.mp4 import MP4 # MP4 is usually handled by File for .m4a
@@ -52,6 +53,9 @@ def read_metadata_from_file(file_path, logger):
elif isinstance(audio, OggVorbis): # OGG elif isinstance(audio, OggVorbis): # OGG
title = audio.get('TITLE', [None])[0] # Vorbis tags are case-insensitive but typically uppercase title = audio.get('TITLE', [None])[0] # Vorbis tags are case-insensitive but typically uppercase
album = audio.get('ALBUM', [None])[0] album = audio.get('ALBUM', [None])[0]
elif isinstance(audio, OggOpus): # OPUS
title = audio.get('TITLE', [None])[0] # Opus files use Vorbis comments, similar to OGG
album = audio.get('ALBUM', [None])[0]
elif isinstance(audio, FLAC): # FLAC elif isinstance(audio, FLAC): # FLAC
title = audio.get('TITLE', [None])[0] title = audio.get('TITLE', [None])[0]
album = audio.get('ALBUM', [None])[0] album = audio.get('ALBUM', [None])[0]

View File

@@ -0,0 +1,321 @@
#!/usr/bin/python3
import os
from typing import Dict, Any, Optional, Union
from deezspot.libutils.utils import request
from deezspot.libutils.logging_utils import logger
from deezspot.__taggers__ import write_tags
from deezspot.models.download import Track, Episode
def fetch_and_process_image(image_url_or_bytes: Union[str, bytes, None]) -> Optional[bytes]:
"""
Fetch and process image data from URL or return bytes directly.
Args:
image_url_or_bytes: Image URL string, bytes, or None
Returns:
Image bytes or None if failed/not available
"""
if not image_url_or_bytes:
return None
if isinstance(image_url_or_bytes, bytes):
return image_url_or_bytes
if isinstance(image_url_or_bytes, str):
try:
response = request(image_url_or_bytes)
return response.content
except Exception as e:
logger.warning(f"Failed to fetch image from URL {image_url_or_bytes}: {e}")
return None
return None
def enhance_metadata_with_image(metadata_dict: Dict[str, Any]) -> Dict[str, Any]:
"""
Enhance metadata dictionary by fetching image data if image URL is present.
Args:
metadata_dict: Metadata dictionary potentially containing image URL
Returns:
Enhanced metadata dictionary with image bytes
"""
image_url = metadata_dict.get('image')
if image_url:
image_bytes = fetch_and_process_image(image_url)
if image_bytes:
metadata_dict['image'] = image_bytes
return metadata_dict
def add_deezer_enhanced_metadata(
metadata_dict: Dict[str, Any],
infos_dw: Dict[str, Any],
track_ids: str,
api_gw_instance: Any = None
) -> Dict[str, Any]:
"""
Add Deezer-specific enhanced metadata including contributors, lyrics, and version info.
Args:
metadata_dict: Base metadata dictionary
infos_dw: Deezer track information dictionary
track_ids: Track IDs for lyrics fetching
api_gw_instance: API gateway instance for lyrics retrieval
Returns:
Enhanced metadata dictionary with Deezer-specific data
"""
# Add contributor information
contributors = infos_dw.get('SNG_CONTRIBUTORS', {})
metadata_dict['author'] = "; ".join(contributors.get('author', []))
metadata_dict['composer'] = "; ".join(contributors.get('composer', []))
metadata_dict['lyricist'] = "; ".join(contributors.get('lyricist', []))
# Handle composer+lyricist combination
if contributors.get('composerlyricist'):
metadata_dict['composer'] = "; ".join(contributors.get('composerlyricist', []))
# Add version information
metadata_dict['version'] = infos_dw.get('VERSION', '')
# Initialize lyric fields
metadata_dict['lyric'] = ""
metadata_dict['copyright'] = ""
metadata_dict['lyric_sync'] = []
# Add lyrics if available and API instance provided
if api_gw_instance and infos_dw.get('LYRICS_ID', 0) != 0:
try:
lyrics_data = api_gw_instance.get_lyric(track_ids)
if lyrics_data and "LYRICS_TEXT" in lyrics_data:
metadata_dict['lyric'] = lyrics_data["LYRICS_TEXT"]
if lyrics_data and "LYRICS_SYNC_JSON" in lyrics_data:
# Import here to avoid circular imports
from deezspot.libutils.utils import trasform_sync_lyric
metadata_dict['lyric_sync'] = trasform_sync_lyric(
lyrics_data['LYRICS_SYNC_JSON']
)
except Exception as e:
logger.warning(f"Failed to retrieve lyrics: {str(e)}")
# Extract album artist from contributors with 'Main' role
if 'contributors' in infos_dw:
main_artists = [c['name'] for c in infos_dw['contributors'] if c.get('role') == 'Main']
if main_artists:
metadata_dict['album_artist'] = "; ".join(main_artists)
return metadata_dict
def add_spotify_enhanced_metadata(metadata_dict: Dict[str, Any], track_obj: Any) -> Dict[str, Any]:
"""
Add Spotify-specific enhanced metadata.
Args:
metadata_dict: Base metadata dictionary
track_obj: Spotify track object
Returns:
Enhanced metadata dictionary with Spotify-specific data
"""
# Spotify tracks already have most metadata from the unified converter
# Add any Spotify-specific enhancements here if needed in the future
# Ensure image is processed
return enhance_metadata_with_image(metadata_dict)
def prepare_track_metadata(
metadata_dict: Dict[str, Any],
source_type: str = 'unknown',
enhanced_data: Optional[Dict[str, Any]] = None,
api_instance: Any = None,
track_ids: Optional[str] = None
) -> Dict[str, Any]:
"""
Prepare and enhance track metadata for tagging based on source type.
Args:
metadata_dict: Base metadata dictionary
source_type: Source type ('spotify', 'deezer', or 'unknown')
enhanced_data: Additional source-specific data (infos_dw for Deezer)
api_instance: API instance for additional data fetching
track_ids: Track IDs for API calls
Returns:
Fully prepared metadata dictionary
"""
# Always process images first
metadata_dict = enhance_metadata_with_image(metadata_dict)
if source_type == 'deezer' and enhanced_data:
metadata_dict = add_deezer_enhanced_metadata(
metadata_dict,
enhanced_data,
track_ids or '',
api_instance
)
elif source_type == 'spotify':
metadata_dict = add_spotify_enhanced_metadata(metadata_dict, enhanced_data)
return metadata_dict
def apply_tags_to_track(track: Track, metadata_dict: Dict[str, Any]) -> None:
"""
Apply metadata tags to a track object and write them to the file.
Args:
track: Track object to tag
metadata_dict: Metadata dictionary containing tag information
"""
if not track or not metadata_dict:
return
try:
track.tags = metadata_dict
write_tags(track)
logger.debug(f"Successfully applied tags to track: {metadata_dict.get('music', 'Unknown')}")
except Exception as e:
logger.error(f"Failed to apply tags to track: {e}")
def apply_tags_to_episode(episode: Episode, metadata_dict: Dict[str, Any]) -> None:
"""
Apply metadata tags to an episode object and write them to the file.
Args:
episode: Episode object to tag
metadata_dict: Metadata dictionary containing tag information
"""
if not episode or not metadata_dict:
return
try:
episode.tags = metadata_dict
write_tags(episode)
logger.debug(f"Successfully applied tags to episode: {metadata_dict.get('music', 'Unknown')}")
except Exception as e:
logger.error(f"Failed to apply tags to episode: {e}")
def save_cover_image_for_track(
metadata_dict: Dict[str, Any],
track_path: str,
save_cover: bool = False,
cover_filename: str = "cover.jpg"
) -> None:
"""
Save cover image for a track if requested and image data is available.
Args:
metadata_dict: Metadata dictionary potentially containing image data
track_path: Path to the track file
save_cover: Whether to save cover image
cover_filename: Filename for the cover image
"""
if not save_cover or not metadata_dict.get('image'):
return
try:
from deezspot.libutils.utils import save_cover_image
track_directory = os.path.dirname(track_path)
# Handle both URL and bytes
image_data = metadata_dict['image']
if isinstance(image_data, str):
image_bytes = fetch_and_process_image(image_data)
else:
image_bytes = image_data
if image_bytes:
save_cover_image(image_bytes, track_directory, cover_filename)
logger.info(f"Saved cover image for track in {track_directory}")
except Exception as e:
logger.warning(f"Failed to save cover image for track: {e}")
# Convenience function that combines metadata preparation and tagging
def process_and_tag_track(
track: Track,
metadata_dict: Dict[str, Any],
source_type: str = 'unknown',
enhanced_data: Optional[Dict[str, Any]] = None,
api_instance: Any = None,
track_ids: Optional[str] = None,
save_cover: bool = False
) -> None:
"""
Complete metadata processing and tagging workflow for a track.
Args:
track: Track object to process
metadata_dict: Base metadata dictionary
source_type: Source type ('spotify', 'deezer', or 'unknown')
enhanced_data: Additional source-specific data
api_instance: API instance for additional data fetching
track_ids: Track IDs for API calls
save_cover: Whether to save cover image
"""
# Prepare enhanced metadata
prepared_metadata = prepare_track_metadata(
metadata_dict,
source_type,
enhanced_data,
api_instance,
track_ids
)
# Apply tags to track
apply_tags_to_track(track, prepared_metadata)
# Save cover image if requested
if hasattr(track, 'song_path') and track.song_path:
save_cover_image_for_track(prepared_metadata, track.song_path, save_cover)
def process_and_tag_episode(
episode: Episode,
metadata_dict: Dict[str, Any],
source_type: str = 'unknown',
enhanced_data: Optional[Dict[str, Any]] = None,
api_instance: Any = None,
track_ids: Optional[str] = None,
save_cover: bool = False
) -> None:
"""
Complete metadata processing and tagging workflow for an episode.
Args:
episode: Episode object to process
metadata_dict: Base metadata dictionary
source_type: Source type ('spotify', 'deezer', or 'unknown')
enhanced_data: Additional source-specific data
api_instance: API instance for additional data fetching
track_ids: Track IDs for API calls
save_cover: Whether to save cover image
"""
# Prepare enhanced metadata
prepared_metadata = prepare_track_metadata(
metadata_dict,
source_type,
enhanced_data,
api_instance,
track_ids
)
# Apply tags to episode
apply_tags_to_episode(episode, prepared_metadata)
# Save cover image if requested
if hasattr(episode, 'episode_path') and episode.episode_path:
save_cover_image_for_track(prepared_metadata, episode.episode_path, save_cover)

View File

@@ -0,0 +1,101 @@
#!/usr/bin/python3
import os
from typing import List, Union
from deezspot.libutils.utils import sanitize_name
from deezspot.libutils.logging_utils import logger
from deezspot.models.download import Track
def create_m3u_file(output_dir: str, playlist_name: str) -> str:
"""
Creates an m3u playlist file with the proper header.
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")
os.makedirs(playlist_m3u_dir, exist_ok=True)
playlist_name_sanitized = sanitize_name(playlist_name)
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
if not os.path.exists(m3u_path):
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
m3u_file.write("#EXTM3U\n")
logger.debug(f"Created m3u playlist file: {m3u_path}")
return m3u_path
def append_track_to_m3u(m3u_path: str, track_path: str) -> None:
"""
Appends a single track path to an existing m3u file.
Args:
m3u_path: Full path to the m3u file
track_path: Full path to the track file
"""
if not track_path or not os.path.exists(track_path):
return
playlist_m3u_dir = os.path.dirname(m3u_path)
relative_path = os.path.relpath(track_path, start=playlist_m3u_dir)
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
m3u_file.write(f"{relative_path}\n")
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.
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")
os.makedirs(playlist_m3u_dir, exist_ok=True)
playlist_name_sanitized = sanitize_name(playlist_name)
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:
if (isinstance(track, Track) and
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)
m3u_file.write(f"{relative_song_path}\n")
logger.info(f"Created m3u playlist file at: {m3u_path}")
return m3u_path
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_name_sanitized = sanitize_name(playlist_name)
return os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")

View File

@@ -32,6 +32,17 @@ from deezspot.libutils.utils import (
save_cover_image, save_cover_image,
__get_dir as get_album_directory, __get_dir as get_album_directory,
) )
from deezspot.libutils.write_m3u import create_m3u_file, append_track_to_m3u
from deezspot.libutils.metadata_converter import track_object_to_dict, album_object_to_dict
from deezspot.libutils.progress_reporter import (
report_track_initializing, report_track_skipped, report_track_retrying,
report_track_realtime_progress, report_track_error, report_track_done,
report_album_initializing, report_album_done, report_playlist_initializing, report_playlist_done
)
from deezspot.libutils.taggers import (
enhance_metadata_with_image, process_and_tag_track, process_and_tag_episode,
save_cover_image_for_track
)
from deezspot.libutils.logging_utils import logger, report_progress from deezspot.libutils.logging_utils import logger, report_progress
from deezspot.libutils.cleanup_utils import ( from deezspot.libutils.cleanup_utils import (
register_active_download, register_active_download,
@@ -49,7 +60,6 @@ from deezspot.models.callback import (
IDs IDs
) )
from deezspot.spotloader.__spo_api__ import tracking, json_to_track_playlist_object from deezspot.spotloader.__spo_api__ import tracking, json_to_track_playlist_object
from datetime import datetime
# --- Global retry counter variables --- # --- Global retry counter variables ---
GLOBAL_RETRY_COUNT = 0 GLOBAL_RETRY_COUNT = 0
@@ -58,103 +68,15 @@ GLOBAL_MAX_RETRIES = 100 # Adjust this value as needed
# --- Global tracking of active downloads --- # --- Global tracking of active downloads ---
# Moved to deezspot.libutils.cleanup_utils # Moved to deezspot.libutils.cleanup_utils
# Use unified metadata converter
def _track_object_to_dict(track_obj: trackObject) -> dict: def _track_object_to_dict(track_obj: trackObject) -> dict:
"""Converts a trackObject into a dictionary for legacy functions like taggers.""" """Converts a trackObject into a dictionary for legacy functions like taggers."""
if not track_obj: return track_object_to_dict(track_obj, source_type='spotify')
return {}
tags = {}
# Track details
tags['music'] = track_obj.title
tags['tracknum'] = track_obj.track_number
tags['discnum'] = track_obj.disc_number
tags['duration'] = track_obj.duration_ms // 1000 if track_obj.duration_ms else 0
if track_obj.ids:
tags['ids'] = track_obj.ids.spotify
tags['isrc'] = track_obj.ids.isrc
tags['artist'] = "; ".join([artist.name for artist in track_obj.artists])
# Album details
if track_obj.album:
album = track_obj.album
tags['album'] = album.title
tags['ar_album'] = "; ".join([artist.name for artist in album.artists])
tags['nb_tracks'] = album.total_tracks
if album.release_date and 'year' in album.release_date:
try:
# Create a datetime object for the tagger
tags['year'] = datetime(
year=album.release_date['year'],
month=album.release_date.get('month', 1),
day=album.release_date.get('day', 1)
)
except (ValueError, TypeError):
tags['year'] = None # Handle invalid date parts
else:
tags['year'] = None
if album.ids:
tags['upc'] = album.ids.upc
tags['album_id'] = album.ids.spotify
if album.images:
tags['image'] = max(album.images, key=lambda i: i.get('height', 0) * i.get('width', 0)).get('url') if album.images else None
else:
tags['image'] = None
# These are not in the model, add them from album if they exist
tags['label'] = getattr(album, 'label', '')
tags['copyright'] = getattr(album, 'copyright', '')
# Default/Placeholder values
tags['bpm'] = tags.get('bpm', 'Unknown')
tags['gain'] = tags.get('gain', 'Unknown')
tags['lyric'] = tags.get('lyric', '')
tags['author'] = tags.get('author', '')
tags['composer'] = tags.get('composer', '')
tags['lyricist'] = tags.get('lyricist', '')
tags['version'] = tags.get('version', '')
return tags
# Use unified metadata converter
def _album_object_to_dict(album_obj: albumObject) -> dict: def _album_object_to_dict(album_obj: albumObject) -> dict:
"""Converts an albumObject into a dictionary for legacy functions.""" """Converts an albumObject into a dictionary for legacy functions."""
if not album_obj: return album_object_to_dict(album_obj, source_type='spotify')
return {}
tags = {}
# Album details
tags['album'] = album_obj.title
tags['ar_album'] = "; ".join([artist.name for artist in album_obj.artists])
tags['nb_tracks'] = album_obj.total_tracks
if album_obj.release_date and 'year' in album_obj.release_date:
try:
tags['year'] = datetime(
year=album_obj.release_date['year'],
month=album_obj.release_date.get('month', 1),
day=album_obj.release_date.get('day', 1)
)
except (ValueError, TypeError):
tags['year'] = None
else:
tags['year'] = None
if album_obj.ids:
tags['upc'] = album_obj.ids.upc
tags['album_id'] = album_obj.ids.spotify
if album_obj.images:
tags['image'] = max(album_obj.images, key=lambda i: i.get('height', 0) * i.get('width', 0)).get('url') if album_obj.images else None
else:
tags['image'] = None
tags['label'] = getattr(album_obj, 'label', '')
tags['copyright'] = getattr(album_obj, 'copyright', '')
return tags
class Download_JOB: class Download_JOB:
session = None session = None
@@ -377,11 +299,8 @@ class EASY_DW:
return self.__c_track return self.__c_track
def easy_dw(self) -> Track: def easy_dw(self) -> Track:
# Request the image data # Process image data using unified utility
pic = self.__song_metadata_dict.get('image') self.__song_metadata_dict = enhance_metadata_with_image(self.__song_metadata_dict)
image = request(pic).content if pic else None
if image:
self.__song_metadata_dict['image'] = image
try: try:
# Initialize success to False, it will be set to True if download_try is successful # Initialize success to False, it will be set to True if download_try is successful
@@ -426,8 +345,13 @@ class EASY_DW:
# If we reach here, the track should be successful and not skipped. # If we reach here, the track should be successful and not skipped.
if hasattr(self, '_EASY_DW__c_track') and self.__c_track and self.__c_track.success: if hasattr(self, '_EASY_DW__c_track') and self.__c_track and self.__c_track.success:
self.__c_track.tags = self.__song_metadata_dict # Apply tags using unified utility
write_tags(self.__c_track) process_and_tag_track(
track=self.__c_track,
metadata_dict=self.__song_metadata_dict,
source_type='spotify',
save_cover=getattr(self.__preferences, 'save_cover', False)
)
# Unregister the final successful file path after all operations are done. # Unregister the final successful file path after all operations are done.
# self.__c_track.song_path would have been updated by __convert_audio__ if conversion occurred. # self.__c_track.song_path would have been updated by __convert_audio__ if conversion occurred.
@@ -480,26 +404,13 @@ class EASY_DW:
owner=userObject(name=parent_info.get("owner")) owner=userObject(name=parent_info.get("owner"))
) )
# Build status object # Report track skipped status
status_obj = skippedObject( report_track_skipped(
ids=self.__song_metadata.ids, track_obj=track_obj,
reason=f"Track already exists at '{existing_file_path}'", reason=f"Track already exists at '{existing_file_path}'",
convert_to=self.__preferences.convert_to, preferences=self.__preferences,
bitrate=self.__preferences.bitrate parent_obj=parent_obj,
) total_tracks=total_tracks_val
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=getattr(self.__preferences, 'track_number', None),
total_tracks=total_tracks_val,
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
return self.__c_track return self.__c_track
@@ -519,25 +430,12 @@ class EASY_DW:
owner=userObject(name=parent_info.get("owner")) owner=userObject(name=parent_info.get("owner"))
) )
# Build status object # Report track initialization status
status_obj = initializingObject( report_track_initializing(
ids=self.__song_metadata.ids, track_obj=track_obj,
convert_to=self.__preferences.convert_to, preferences=self.__preferences,
bitrate=self.__preferences.bitrate parent_obj=parent_obj,
) total_tracks=total_tracks_val
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=getattr(self.__preferences, 'track_number', None),
total_tracks=total_tracks_val,
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
# If track does not exist in the desired final format, proceed with download/conversion # If track does not exist in the desired final format, proceed with download/conversion
@@ -593,27 +491,14 @@ class EASY_DW:
if current_percentage > self._last_reported_percentage: if current_percentage > self._last_reported_percentage:
self._last_reported_percentage = current_percentage self._last_reported_percentage = current_percentage
# Build status object # Report real-time progress
status_obj = realTimeObject( report_track_realtime_progress(
ids=self.__song_metadata.ids, track_obj=track_obj,
time_elapsed=int((current_time - start_time) * 1000), time_elapsed=int((current_time - start_time) * 1000),
progress=current_percentage, progress=current_percentage,
convert_to=self.__convert_to, preferences=self.__preferences,
bitrate=self.__bitrate parent_obj=parent_obj,
) total_tracks=total_tracks_val
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=getattr(self.__preferences, 'track_number', None),
total_tracks=total_tracks_val,
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
# Rate limiting (if needed) # Rate limiting (if needed)
@@ -669,28 +554,15 @@ class EASY_DW:
os.remove(self.__song_path) os.remove(self.__song_path)
unregister_active_download(self.__song_path) unregister_active_download(self.__song_path)
# Build status object # Report retry status
status_obj = retryingObject( report_track_retrying(
ids=self.__song_metadata.ids, track_obj=track_obj,
retry_count=retries, retry_count=retries,
seconds_left=retry_delay, seconds_left=retry_delay,
error=str(e), error=str(e),
convert_to=self.__convert_to, preferences=self.__preferences,
bitrate=self.__bitrate parent_obj=parent_obj,
) total_tracks=total_tracks_val
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=getattr(self.__preferences, 'track_number', None),
total_tracks=total_tracks_val,
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
if retries >= max_retries or GLOBAL_RETRY_COUNT >= GLOBAL_MAX_RETRIES: if retries >= max_retries or GLOBAL_RETRY_COUNT >= GLOBAL_MAX_RETRIES:
@@ -710,15 +582,8 @@ class EASY_DW:
retry_delay += retry_delay_increase # Use the custom retry delay increase retry_delay += retry_delay_increase # Use the custom retry delay increase
# Save cover image if requested, after successful download and before conversion # Save cover image if requested, after successful download and before conversion
if self.__preferences.save_cover and hasattr(self, '_EASY_DW__song_path') and self.__song_path and self.__song_metadata_dict.get('image'): if self.__preferences.save_cover and hasattr(self, '_EASY_DW__song_path') and self.__song_path:
try: save_cover_image_for_track(self.__song_metadata_dict, self.__song_path, self.__preferences.save_cover)
track_directory = dirname(self.__song_path)
# Ensure the directory exists (it should, from os.makedirs earlier)
image_bytes = request(self.__song_metadata_dict['image']).content
save_cover_image(image_bytes, track_directory, "cover.jpg")
logger.info(f"Saved cover image for track in {track_directory}")
except Exception as e_img_save:
logger.warning(f"Failed to save cover image for track: {e_img_save}")
try: try:
self.__convert_audio() self.__convert_audio()
@@ -732,26 +597,13 @@ class EASY_DW:
else: else:
error_msg = f"Audio conversion failed: {original_error_str}" error_msg = f"Audio conversion failed: {original_error_str}"
# Build status object # Report error status
status_obj = errorObject( report_track_error(
ids=self.__song_metadata.ids, track_obj=track_obj,
error=error_msg, error=error_msg,
convert_to=self.__convert_to, preferences=self.__preferences,
bitrate=self.__bitrate parent_obj=parent_obj,
) total_tracks=total_tracks_val
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=getattr(self.__preferences, 'track_number', None),
total_tracks=total_tracks_val,
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
logger.error(f"Audio conversion error: {error_msg}") logger.error(f"Audio conversion error: {error_msg}")
@@ -768,26 +620,13 @@ class EASY_DW:
# If conversion fails twice, create a final error report # If conversion fails twice, create a final error report
error_msg_2 = f"Audio conversion failed after retry for '{self.__song_metadata.title}'. Original error: {str(conv_e)}" error_msg_2 = f"Audio conversion failed after retry for '{self.__song_metadata.title}'. Original error: {str(conv_e)}"
# Build status object # Report error status
status_obj = errorObject( report_track_error(
ids=self.__song_metadata.ids, track_obj=track_obj,
error=error_msg_2, error=error_msg_2,
convert_to=self.__convert_to, preferences=self.__preferences,
bitrate=self.__bitrate parent_obj=parent_obj,
) total_tracks=total_tracks_val
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=getattr(self.__preferences, 'track_number', None),
total_tracks=total_tracks_val,
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
logger.error(error_msg) logger.error(error_msg)
@@ -801,8 +640,12 @@ class EASY_DW:
if hasattr(self, '_EASY_DW__c_track') and self.__c_track: if hasattr(self, '_EASY_DW__c_track') and self.__c_track:
self.__c_track.success = True self.__c_track.success = True
self.__c_track.tags = self.__song_metadata_dict # Apply tags using unified utility
write_tags(self.__c_track) process_and_tag_track(
track=self.__c_track,
metadata_dict=self.__song_metadata_dict,
source_type='spotify'
)
# Create done status report # Create done status report
parent_info, total_tracks_val = self._get_parent_info() parent_info, total_tracks_val = self._get_parent_info()
@@ -823,26 +666,14 @@ class EASY_DW:
total_failed=0 total_failed=0
) )
# Build status object # Report track done status
status_obj = doneObject( report_track_done(
ids=self.__song_metadata.ids, track_obj=track_obj,
preferences=self.__preferences,
summary=summary_obj, summary=summary_obj,
convert_to=self.__convert_to, parent_obj=parent_obj,
bitrate=self.__bitrate
)
# Build callback object
callback_obj = trackCallbackObject(
track=track_obj,
status_info=status_obj,
current_track=current_track_val, current_track=current_track_val,
total_tracks=total_tracks_val, total_tracks=total_tracks_val
parent=parent_obj
)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
) )
if hasattr(self, '_EASY_DW__c_track') and self.__c_track and self.__c_track.success: if hasattr(self, '_EASY_DW__c_track') and self.__c_track and self.__c_track.success:
@@ -1053,8 +884,12 @@ class EASY_DW:
# If we reach here, download and any conversion were successful. # If we reach here, download and any conversion were successful.
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 = True self.__c_episode.success = True
self.__c_episode.tags = self.__song_metadata_dict # Apply tags using unified utility
write_tags(self.__c_episode) process_and_tag_episode(
episode=self.__c_episode,
metadata_dict=self.__song_metadata_dict,
source_type='spotify'
)
# Unregister the final successful file path for episodes, as it's now complete. # Unregister the final successful file path for episodes, as it's now complete.
# self.__c_episode.episode_path would have been updated by __convert_audio__ if conversion occurred. # self.__c_episode.episode_path would have been updated by __convert_audio__ if conversion occurred.
unregister_active_download(self.__c_episode.episode_path) unregister_active_download(self.__c_episode.episode_path)
@@ -1106,13 +941,7 @@ class DW_ALBUM:
def dw(self) -> Album: def dw(self) -> Album:
# Report album initializing status # Report album initializing status
album_obj = self.__album_metadata album_obj = self.__album_metadata
status_obj = initializingObject(ids=album_obj.ids) report_album_initializing(album_obj)
callback_obj = albumCallbackObject(album=album_obj, status_info=status_obj)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
)
pic_url = max(album_obj.images, key=lambda i: i.get('height', 0) * i.get('width', 0)).get('url') if album_obj.images else None pic_url = max(album_obj.images, key=lambda i: i.get('height', 0) * i.get('width', 0)).get('url') if album_obj.images else None
image_bytes = request(pic_url).content if pic_url else None image_bytes = request(pic_url).content if pic_url else None
@@ -1133,11 +962,14 @@ class DW_ALBUM:
pad_tracks=self.__preferences.pad_tracks pad_tracks=self.__preferences.pad_tracks
) )
# Calculate total number of discs for proper metadata tagging
total_discs = max((track.disc_number for track in album_obj.tracks), default=1)
for a, track_in_album in enumerate(album_obj.tracks): for a, track_in_album in enumerate(album_obj.tracks):
current_track = a + 1
c_preferences = deepcopy(self.__preferences) c_preferences = deepcopy(self.__preferences)
c_preferences.track_number = current_track # Use actual track position for progress tracking, not for metadata
c_preferences.track_number = a + 1 # For progress reporting only
try: try:
# Fetch full track object as album endpoint only provides simplified track objects # Fetch full track object as album endpoint only provides simplified track objects
@@ -1210,13 +1042,7 @@ class DW_ALBUM:
total_failed=len(failed_tracks) total_failed=len(failed_tracks)
) )
status_obj = doneObject(ids=album_obj.ids, summary=summary_obj) report_album_done(album_obj, summary_obj)
callback_obj = albumCallbackObject(album=album_obj, status_info=status_obj)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
)
return album return album
@@ -1259,21 +1085,10 @@ class DW_PLAYLIST:
# --- End build playlistObject --- # --- End build playlistObject ---
# Report playlist initializing status # Report playlist initializing status
status_obj = initializingObject(ids=IDs(spotify=playlist_id)) report_playlist_initializing(playlist_obj_for_cb)
callback_obj = playlistCallbackObject(playlist=playlist_obj_for_cb, status_info=status_obj)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj
)
# --- Prepare the m3u playlist file --- # --- Prepare the m3u playlist file ---
playlist_m3u_dir = os.path.join(self.__output_dir, "playlists") m3u_path = create_m3u_file(self.__output_dir, playlist_name)
os.makedirs(playlist_m3u_dir, exist_ok=True)
playlist_name_sanitized = sanitize_name(playlist_name)
m3u_path = os.path.join(playlist_m3u_dir, f"{playlist_name_sanitized}.m3u")
if not os.path.exists(m3u_path):
with open(m3u_path, "w", encoding="utf-8") as m3u_file:
m3u_file.write("#EXTM3U\n")
# ------------------------------------- # -------------------------------------
playlist = Playlist() playlist = Playlist()
@@ -1319,9 +1134,7 @@ class DW_PLAYLIST:
# --- Append the final track path to the m3u file using a relative path --- # --- Append the final track path to the m3u file using a relative path ---
if track and track.success and hasattr(track, 'song_path') and track.song_path: if track and track.success and hasattr(track, 'song_path') and track.song_path:
relative_path = os.path.relpath(track.song_path, start=playlist_m3u_dir) append_track_to_m3u(m3u_path, track.song_path)
with open(m3u_path, "a", encoding="utf-8") as m3u_file:
m3u_file.write(f"{relative_path}\n")
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
if self.__make_zip: if self.__make_zip:
@@ -1361,13 +1174,7 @@ class DW_PLAYLIST:
total_failed=len(failed_tracks_cb) total_failed=len(failed_tracks_cb)
) )
status_obj = doneObject(ids=playlist_obj_for_cb.ids, summary=summary_obj) report_playlist_done(playlist_obj_for_cb, summary_obj)
callback_obj = playlistCallbackObject(playlist=playlist_obj_for_cb, status_info=status_obj)
report_progress(
reporter=Download_JOB.progress_reporter,
callback_obj=callback_obj,
)
return playlist return playlist