fixed audio convertion fr this time

This commit is contained in:
Xoconoch
2025-06-03 22:25:12 -06:00
parent d38c64c643
commit 93d626bbc4
10 changed files with 1263 additions and 982 deletions

View File

@@ -5,7 +5,7 @@ import requests
import time
from os.path import isfile
from copy import deepcopy
from deezspot.libutils.audio_converter import convert_audio, parse_format_string
from deezspot.libutils.audio_converter import convert_audio
from deezspot.deezloader.dee_api import API
from deezspot.deezloader.deegw_api import API_GW
from deezspot.deezloader.deezer_settings import qualities
@@ -43,6 +43,9 @@ from mutagen.id3 import ID3
from mutagen.mp4 import MP4
from mutagen import File
from deezspot.libutils.logging_utils import logger, ProgressReporter
from deezspot.libutils.skip_detection import check_track_exists
from deezspot.libutils.cleanup_utils import register_active_download, unregister_active_download
from deezspot.libutils.audio_converter import AUDIO_FORMATS # Added for parse_format_string
class Download_JOB:
progress_reporter = None
@@ -200,6 +203,7 @@ class EASY_DW:
self.__recursive_quality = preferences.recursive_quality
self.__recursive_download = preferences.recursive_download
self.__convert_to = getattr(preferences, 'convert_to', None)
self.__bitrate = getattr(preferences, 'bitrate', None) # Added for consistency
if self.__infos_dw.get('__TYPE__') == 'episode':
@@ -226,45 +230,6 @@ class EASY_DW:
self.__set_quality()
self.__write_track()
def __track_already_exists(self, title, album):
# Ensure the song path is set; if not, compute it.
if not hasattr(self, '_EASY_DW__song_path') or not self.__song_path:
self.__set_song_path()
# Get only the final directory where the track will be saved.
final_dir = os.path.dirname(self.__song_path)
if not os.path.exists(final_dir):
return False
# List files only in the final directory.
for file in os.listdir(final_dir):
file_path = os.path.join(final_dir, file)
lower_file = file.lower()
try:
existing_title = None
existing_album = None
if lower_file.endswith('.flac'):
audio = FLAC(file_path)
existing_title = audio.get('title', [None])[0]
existing_album = audio.get('album', [None])[0]
elif lower_file.endswith('.mp3'):
audio = MP3(file_path, ID3=ID3)
existing_title = audio.get('TIT2', [None])[0]
existing_album = audio.get('TALB', [None])[0]
elif lower_file.endswith('.m4a'):
audio = MP4(file_path)
existing_title = audio.get('\xa9nam', [None])[0]
existing_album = audio.get('\xa9alb', [None])[0]
elif lower_file.endswith(('.ogg', '.wav')):
audio = File(file_path)
existing_title = audio.get('title', [None])[0]
existing_album = audio.get('album', [None])[0]
if existing_title == title and existing_album == album:
return True
except Exception:
continue
return False
def __set_quality(self) -> None:
self.__file_format = self.__c_quality['f_format']
self.__song_quality = self.__c_quality['s_quality']
@@ -334,16 +299,40 @@ class EASY_DW:
# Check if track already exists based on metadata
current_title = self.__song_metadata['music']
current_album = self.__song_metadata['album']
if self.__track_already_exists(current_title, current_album):
# Create skipped progress report using the new required format
current_artist = self.__song_metadata.get('artist') # For logging
# Use check_track_exists from skip_detection module
# self.__song_path is the original path before any conversion logic in this download attempt.
# self.__convert_to is the user's desired final format.
exists, existing_file_path = check_track_exists(
original_song_path=self.__song_path,
title=current_title,
album=current_album,
convert_to=self.__convert_to, # User's target conversion format
logger=logger
)
if exists and existing_file_path:
logger.info(f"Track '{current_title}' by '{current_artist}' already exists at '{existing_file_path}'. Skipping download.")
self.__c_track.song_path = existing_file_path
_, new_ext = os.path.splitext(existing_file_path)
self.__c_track.file_format = new_ext.lower()
# self.__c_track.song_quality might need re-evaluation if we could determine
# quality of existing file. For now, assume it's acceptable.
self.__c_track.success = True
self.__c_track.was_skipped = True
progress_data = {
"type": "track",
"song": current_title,
"artist": self.__song_metadata['artist'],
"status": "skipped",
"url": self.__link,
"reason": "Track already exists",
"convert_to": self.__convert_to
"reason": f"Track already exists in desired format at {existing_file_path}",
"convert_to": self.__convert_to,
"bitrate": self.__bitrate
}
# Add parent info based on parent type
@@ -389,15 +378,15 @@ class EASY_DW:
# Create a minimal track object for skipped scenario
skipped_item = Track(
self.__song_metadata,
self.__song_path, # song_path would be set if __write_track was called
self.__file_format, self.__song_quality,
existing_file_path, # Use the path of the existing file
self.__c_track.file_format, # Use updated file format
self.__song_quality, # Original download quality target
self.__link, self.__ids
)
skipped_item.success = False
skipped_item.success = True # Considered successful as file is available
skipped_item.was_skipped = True
# It's important that this skipped_item is what's checked later, or self.__c_track is updated
self.__c_track = skipped_item # Ensure self.__c_track reflects this skipped state
return self.__c_track # Return the correctly flagged skipped track
self.__c_track = skipped_item
return self.__c_track
# Initialize success to False for the item being processed
if self.__infos_dw.get('__TYPE__') == 'episode':
@@ -626,10 +615,23 @@ class EASY_DW:
Download_JOB.report_progress(progress_data)
# Start of processing block (decryption, tagging, cover, conversion)
# Decrypt the file using the utility function
decryptfile(c_crypted_audio, self.__fallback_ids, self.__song_path)
logger.debug(f"Successfully decrypted track using {encryption_type} encryption")
register_active_download(self.__song_path)
try:
# Decrypt the file using the utility function
decryptfile(c_crypted_audio, self.__fallback_ids, self.__song_path)
logger.debug(f"Successfully decrypted track using {encryption_type} encryption")
# self.__song_path is still registered
except Exception as e_decrypt:
unregister_active_download(self.__song_path)
if isfile(self.__song_path):
try:
os.remove(self.__song_path)
except OSError: # Handle potential errors during removal
logger.warning(f"Could not remove partially downloaded file: {self.__song_path}")
self.__c_track.success = False
self.__c_track.error_message = f"Decryption failed: {str(e_decrypt)}"
raise TrackNotFound(f"Failed to process {self.__song_path}. Error: {str(e_decrypt)}") from e_decrypt
self.__add_more_tags() # self.__song_metadata is updated here
self.__c_track.tags = self.__song_metadata # IMPORTANT: Update track object's tags
@@ -644,31 +646,55 @@ class EASY_DW:
# Apply audio conversion if requested
if self.__convert_to:
format_name, bitrate = parse_format_string(self.__convert_to)
format_name, bitrate = self._parse_format_string(self.__convert_to)
if format_name:
from deezspot.deezloader.__download__ import register_active_download, unregister_active_download # Ensure these are available or handle differently
# Current self.__song_path (original decrypted file) is registered.
# convert_audio will handle unregistering it if it creates a new file,
# and will register the new file.
path_before_conversion = self.__song_path
try:
converted_path = convert_audio(
self.__song_path,
path_before_conversion,
format_name,
bitrate,
bitrate if bitrate else self.__bitrate, # Prefer specific bitrate from string, fallback to general
register_active_download,
unregister_active_download
)
if converted_path != self.__song_path:
if converted_path != path_before_conversion:
# convert_audio has unregistered path_before_conversion (if it existed and was different)
# and registered converted_path.
self.__song_path = converted_path
self.__c_track.song_path = converted_path
_, new_ext = os.path.splitext(converted_path)
self.__file_format = new_ext.lower() # Update internal state
self.__c_track.file_format = new_ext.lower()
# self.__song_path (the converted_path) is now the registered active download
# If converted_path == path_before_conversion, no actual file change, registration status managed by convert_audio
except Exception as conv_error:
logger.error(f"Audio conversion error: {str(conv_error)}")
# Decide if this is a fatal error for the track or if we proceed with original
logger.error(f"Audio conversion error: {str(conv_error)}. Proceeding with original format.")
# path_before_conversion should still be registered if convert_audio failed early
# or did not successfully unregister it.
# If conversion fails, the original file (path_before_conversion) remains the target.
# Its registration state should be preserved if convert_audio didn't affect it.
# For safety, ensure it is considered the active download if conversion fails:
register_active_download(path_before_conversion)
# Write tags to the final file (original or converted)
write_tags(self.__c_track)
self.__c_track.success = True # Mark as successful only after all steps including tags
unregister_active_download(self.__song_path) # Unregister the final successful file
except Exception as e: # Handles errors from __write_track, decrypt, add_tags, save_cover, convert, write_tags
# Ensure unregister is called for self.__song_path if it was registered and an error occurred
# The specific error might have already unregistered it (e.g. decrypt error)
# Call it defensively.
unregister_active_download(self.__song_path)
if isfile(self.__song_path):
os.remove(self.__song_path)
try:
os.remove(self.__song_path)
except OSError:
logger.warning(f"Could not remove file on error: {self.__song_path}")
error_msg = str(e)
if "Data must be padded" in error_msg: error_msg = "Decryption error (padding issue) - Try a different quality setting or download format"
@@ -707,6 +733,8 @@ class EASY_DW:
error_message = f"Download failed for '{song_title}' by '{artist_name}' (Link: {self.__link}). Error: {str(e)}"
logger.error(error_message)
# Store error on track object if possible
# Ensure self.__song_path is unregistered if an error occurs before successful completion.
unregister_active_download(self.__song_path)
if hasattr(self, '_EASY_DW__c_track') and self.__c_track:
self.__c_track.success = False
self.__c_track.error_message = str(e)
@@ -719,42 +747,62 @@ class EASY_DW:
raise TrackNotFound("No direct stream URL found")
os.makedirs(os.path.dirname(self.__song_path), exist_ok=True)
response = requests.get(direct_url, stream=True)
response.raise_for_status()
content_length = response.headers.get('content-length')
total_size = int(content_length) if content_length else None
downloaded = 0
with open(self.__song_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
size = f.write(chunk)
downloaded += size
# Download progress reporting could be added here
# Build episode progress report
progress_data = {
"type": "episode",
"song": self.__song_metadata.get('music', 'Unknown Episode'),
"artist": self.__song_metadata.get('artist', 'Unknown Show'),
"status": "done"
}
# Use Spotify URL if available (for downloadspo functions), otherwise use Deezer link
spotify_url = getattr(self.__preferences, 'spotify_url', None)
progress_data["url"] = spotify_url if spotify_url else self.__link
register_active_download(self.__song_path)
try:
response = requests.get(direct_url, stream=True)
response.raise_for_status()
content_length = response.headers.get('content-length')
total_size = int(content_length) if content_length else None
downloaded = 0
with open(self.__song_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
size = f.write(chunk)
downloaded += size
# Download progress reporting could be added here
# If download successful, unregister the initially downloaded file before potential conversion
unregister_active_download(self.__song_path)
# Build episode progress report
progress_data = {
"type": "episode",
"song": self.__song_metadata.get('music', 'Unknown Episode'),
"artist": self.__song_metadata.get('artist', 'Unknown Show'),
"status": "done"
}
# Use Spotify URL if available (for downloadspo functions), otherwise use Deezer link
spotify_url = getattr(self.__preferences, 'spotify_url', None)
progress_data["url"] = spotify_url if spotify_url else self.__link
Download_JOB.report_progress(progress_data)
self.__c_track.success = True
self.__write_episode()
write_tags(self.__c_track)
Download_JOB.report_progress(progress_data)
self.__c_track.success = True
self.__write_episode()
write_tags(self.__c_track)
return self.__c_track
except Exception as e_dw_ep: # Catches errors from requests.get, file writing
unregister_active_download(self.__song_path) # Unregister if download part failed
if isfile(self.__song_path):
try:
os.remove(self.__song_path)
except OSError:
logger.warning(f"Could not remove episode file on error: {self.__song_path}")
self.__c_track.success = False # Mark as failed
episode_title = self.__preferences.song_metadata.get('music', 'Unknown Episode')
err_msg = f"Episode download failed for '{episode_title}' (URL: {self.__link}). Error: {str(e_dw_ep)}"
logger.error(err_msg)
self.__c_track.error_message = str(e_dw_ep)
raise TrackNotFound(message=err_msg, url=self.__link) from e_dw_ep
return self.__c_track
except Exception as e:
if isfile(self.__song_path):
os.remove(self.__song_path)
@@ -766,6 +814,35 @@ class EASY_DW:
self.__c_track.error_message = str(e)
raise TrackNotFound(message=err_msg, url=self.__link) from e
def _parse_format_string(self, format_str: str) -> tuple[str | None, str | None]:
"""Helper to parse format string like 'MP3_320K' into format and bitrate."""
if not format_str:
return None, None
parts = format_str.upper().split('_', 1)
format_name = parts[0]
bitrate = parts[1] if len(parts) > 1 else None
if format_name not in AUDIO_FORMATS:
logger.warning(f"Unsupported format {format_name} in format string '{format_str}'. Will not convert.")
return None, None
if bitrate:
# Ensure bitrate ends with 'K' for consistency if it's a number followed by K
if bitrate[:-1].isdigit() and not bitrate.endswith('K'):
bitrate += 'K'
valid_bitrates = AUDIO_FORMATS[format_name].get("bitrates", [])
if valid_bitrates and bitrate not in valid_bitrates:
default_br = AUDIO_FORMATS[format_name].get("default_bitrate")
logger.warning(f"Unsupported bitrate {bitrate} for {format_name}. Using default {default_br if default_br else 'as available'}.")
bitrate = default_br # Fallback to default, or None if no specific default for lossless
elif not valid_bitrates and AUDIO_FORMATS[format_name].get("default_bitrate") is None: # Lossless format
logger.info(f"Bitrate {bitrate} specified for lossless format {format_name}. Bitrate will be ignored by converter.")
# Keep bitrate as is, convert_audio will handle ignoring it for lossless.
return format_name, bitrate
def __add_more_tags(self) -> None:
contributors = self.__infos_dw.get('SNG_CONTRIBUTORS', {})