From bd393805a8bcb46a5bdfba3a0489cc66d308e13a Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Sat, 23 Aug 2025 12:01:18 -0600 Subject: [PATCH] fix: ensure distroless compatibility --- deezspot/libutils/audio_converter.py | 37 ++++++++++++----------- deezspot/spotloader/__download__.py | 44 +++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/deezspot/libutils/audio_converter.py b/deezspot/libutils/audio_converter.py index 50f041c..7928483 100644 --- a/deezspot/libutils/audio_converter.py +++ b/deezspot/libutils/audio_converter.py @@ -69,12 +69,6 @@ AUDIO_FORMATS = { } } -def check_ffmpeg_available(): - """Check if FFmpeg is installed and available.""" - if which("ffmpeg") is None: - logger.error("FFmpeg is not installed or not in PATH. Audio conversion is unavailable.") - return False - return True def get_output_path(input_path, format_name): """Get the output path with the new extension based on the format.""" @@ -93,6 +87,7 @@ def get_output_path(input_path, format_name): return os.path.join(dir_name, new_file_name) + def register_active_download(path): """ Register a file as being actively downloaded. @@ -134,8 +129,14 @@ def convert_audio(input_path, format_name=None, bitrate=None, register_func=None global unregister_active_download unregister_active_download = unregister_func - # If no format specified or FFmpeg not available, return the original path - if not format_name or not check_ffmpeg_available(): + # If no format specified, return the original path + if not format_name: + return input_path + + # Resolve ffmpeg path explicitly (distroless-safe) + ffmpeg_path = which("ffmpeg") or "/usr/local/bin/ffmpeg" + if not os.path.exists(ffmpeg_path): + logger.error(f"FFmpeg is not available (looked for '{ffmpeg_path}'). Audio conversion is unavailable.") return input_path # Validate format and get format details @@ -162,16 +163,9 @@ def convert_audio(input_path, format_name=None, bitrate=None, register_func=None # Skip conversion if the file is already in the target format and bitrate matches (or not applicable) if input_path.lower().endswith(format_details["extension"].lower()): - # For lossless, or if effective_bitrate matches (or no specific bitrate needed for format) if format_details["default_bitrate"] is None: # Lossless logger.info(f"File {input_path} is already in {format_name_upper} (lossless) format. Skipping conversion.") return input_path - # For lossy, if no specific bitrate was relevant (already handled by effective_bitrate logic) - # This condition might be redundant if we always convert to ensure bitrate. - # Let's assume for now, if it's already the right extension, we don't re-encode unless bitrate implies so. - # However, the original logic converted if bitrate was specified even for same extension. - # To maintain similar behavior: if a bitrate is effectively set for a lossy format, we proceed. - # If effective_bitrate is None (e.g. for FLAC, WAV), and extension matches, skip. if not effective_bitrate and format_details["default_bitrate"] is not None: logger.info(f"File {input_path} is already in {format_name_upper} format with a suitable bitrate. Skipping conversion.") return input_path @@ -186,10 +180,10 @@ def convert_audio(input_path, format_name=None, bitrate=None, register_func=None register_active_download(temp_output) try: - cmd = ["ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-i", input_path] + cmd = [ffmpeg_path, "-y", "-hide_banner", "-loglevel", "error", "-i", input_path] # Add bitrate parameter for lossy formats if an effective_bitrate is set - if effective_bitrate and format_details["bitrates"]: # format_details["bitrates"] implies lossy + if effective_bitrate and format_details["bitrates"]: # lossy cmd.extend(["-b:a", effective_bitrate]) # Add codec parameter @@ -234,6 +228,15 @@ def convert_audio(input_path, format_name=None, bitrate=None, register_func=None logger.info(f"Successfully converted to {format_name_upper}" + (f" at {effective_bitrate}" if effective_bitrate else "")) return output_path + except FileNotFoundError as fnf: + logger.error(f"FFmpeg executable not found at '{ffmpeg_path}'. Conversion aborted.") + if exists(temp_output): + try: + os.remove(temp_output) + except Exception: + pass + unregister_active_download(temp_output) + return input_path except Exception as e: logger.error(f"Error during audio conversion: {str(e)}") # Clean up temp files diff --git a/deezspot/spotloader/__download__.py b/deezspot/spotloader/__download__.py index 394ca1a..3167fe6 100644 --- a/deezspot/spotloader/__download__.py +++ b/deezspot/spotloader/__download__.py @@ -17,6 +17,8 @@ from os import ( system, replace as os_replace, ) +import subprocess +import shutil from deezspot.models.download import ( Track, Album, @@ -263,8 +265,23 @@ class EASY_DW: try: # Step 1: First convert the OGG file to standard format (copy operation) # Output is og_song_path_for_ogg_output - ffmpeg_cmd = f'ffmpeg -y -hide_banner -loglevel error -i "{temp_filename}" -c:a copy "{og_song_path_for_ogg_output}"' - system(ffmpeg_cmd) # Creates/overwrites og_song_path_for_ogg_output + # Resolve ffmpeg path explicitly to avoid PATH issues in distroless + ffmpeg_path = shutil.which("ffmpeg") or "/usr/local/bin/ffmpeg" + try: + result = subprocess.run( + [ + ffmpeg_path, "-y", "-hide_banner", "-loglevel", "error", + "-i", temp_filename, "-c:a", "copy", og_song_path_for_ogg_output + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + if result.returncode != 0 or not os.path.exists(og_song_path_for_ogg_output): + raise RuntimeError(f"ffmpeg remux failed (rc={result.returncode}). stderr: {result.stderr.strip()}") + except FileNotFoundError as fnf: + raise RuntimeError(f"ffmpeg not found: attempted '{ffmpeg_path}'. Ensure it is present in PATH.") from fnf # temp_filename has been processed. Unregister and remove it. # CURRENT_DOWNLOAD was temp_filename. @@ -1024,16 +1041,27 @@ def download_cli(preferences: Preferences) -> None: __not_interface = preferences.not_interface __quality_download = preferences.quality_download __recursive_download = preferences.recursive_download - cmd = f"deez-dw.py -so spo -l \"{__link}\" " + # Build argv list instead of shell string (distroless-safe) + argv = ["deez-dw.py", "-so", "spo", "-l", __link] if __output_dir: - cmd += f"-o {__output_dir} " + argv += ["-o", str(__output_dir)] if __not_interface: - cmd += f"-g " + argv += ["-g"] if __quality_download: - cmd += f"-q {__quality_download} " + argv += ["-q", str(__quality_download)] if __recursive_download: - cmd += f"-rd " - system(cmd) + argv += ["-rd"] + prog = shutil.which(argv[0]) + if not prog: + logger.error("deez-dw.py CLI not found in PATH; cannot run download_cli in this environment.") + return + argv[0] = prog + try: + result = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode != 0: + logger.error(f"deez-dw.py exited with {result.returncode}: {result.stderr.strip()}") + except Exception as e: + logger.error(f"Failed to execute deez-dw.py: {e}") class DW_TRACK: def __init__(