Files
deezspot-spotizerr-dev/deezspot/libutils/skip_detection.py
2025-06-04 14:44:14 -06:00

175 lines
9.6 KiB
Python

#!/usr/bin/python3
import os
from mutagen import File
from mutagen.easyid3 import EasyID3
from mutagen.oggvorbis import OggVorbis
from mutagen.flac import FLAC
from mutagen.mp3 import MP3 # Added for explicit MP3 type checking
# from mutagen.mp4 import MP4 # MP4 is usually handled by File for .m4a
# AUDIO_FORMATS and get_output_path will be imported from audio_converter
# We need to ensure this doesn't create circular dependencies.
# If audio_converter also imports something from libutils that might import this,
# it could be an issue. For now, proceeding with direct import.
from deezspot.libutils.audio_converter import AUDIO_FORMATS, get_output_path
# Logger instance will be passed as an argument to functions that need it.
def read_metadata_from_file(file_path, logger):
"""Reads title and album metadata from an audio file."""
try:
if not os.path.isfile(file_path):
logger.debug(f"File not found for metadata reading: {file_path}")
return None, None
audio = File(file_path, easy=False) # easy=False to access format-specific tags better
if audio is None:
logger.warning(f"Could not load audio file with mutagen: {file_path}")
return None, None
title = None
album = None
if isinstance(audio, EasyID3): # This might occur if easy=True was used, but we use easy=False
# This branch is less likely to be hit with current File(..., easy=False) usage for MP3s
title = audio.get('title', [None])[0]
album = audio.get('album', [None])[0]
elif isinstance(audio, MP3): # Correctly handle MP3 objects when easy=False
# For mutagen.mp3.MP3, tags are typically accessed via audio.tags (an ID3 object)
# Common ID3 frames for title and album are TIT2 and TALB respectively.
# The .text attribute of a frame object usually holds a list of strings.
if audio.tags:
title_frame = audio.tags.get('TIT2')
if title_frame:
title = title_frame.text[0] if title_frame.text else None
album_frame = audio.tags.get('TALB')
if album_frame:
album = album_frame.text[0] if album_frame.text else None
else:
logger.debug(f"No tags found in MP3 file: {file_path}")
elif isinstance(audio, OggVorbis): # OGG
title = audio.get('TITLE', [None])[0] # Vorbis tags are case-insensitive but typically uppercase
album = audio.get('ALBUM', [None])[0]
elif isinstance(audio, FLAC): # FLAC
title = audio.get('TITLE', [None])[0]
album = audio.get('ALBUM', [None])[0]
elif file_path.lower().endswith('.m4a'): # M4A (AAC/ALAC)
# Mutagen's File(filepath) for .m4a returns an MP4 object
title = audio.get('\xa9nam', [None])[0] # iTunes title tag
album = audio.get('\xa9alb', [None])[0] # iTunes album tag
else:
logger.warning(f"Unsupported file type for metadata extraction by read_metadata_from_file: {file_path} (type: {type(audio)})")
return None, None
return title, album
except Exception as e:
logger.error(f"Error reading metadata from {file_path}: {str(e)}")
return None, None
def check_track_exists(original_song_path, title, album, convert_to, logger):
"""Checks if a track exists, considering original and target converted formats.
Args:
original_song_path (str): The expected path for the song in its original download format.
title (str): The title of the track to check.
album (str): The album of the track to check.
convert_to (str | None): The target format for conversion (e.g., 'MP3', 'FLAC'), or None.
logger (logging.Logger): Logger instance.
Returns:
tuple[bool, str | None]: (True, path_to_existing_file) if exists, else (False, None).
"""
scan_dir = os.path.dirname(original_song_path)
if not os.path.exists(scan_dir):
logger.debug(f"Scan directory {scan_dir} does not exist. Track cannot exist.")
return False, None
# Priority 1: Check if the file exists in the target converted format
if convert_to:
target_format_upper = convert_to.upper()
if target_format_upper in AUDIO_FORMATS:
final_expected_converted_path = get_output_path(original_song_path, target_format_upper)
final_target_ext = AUDIO_FORMATS[target_format_upper]["extension"].lower()
# Check exact predicted path for converted file
if os.path.exists(final_expected_converted_path):
existing_title, existing_album = read_metadata_from_file(final_expected_converted_path, logger)
if existing_title == title and existing_album == album:
logger.info(f"Found existing track (exact converted path match): {title} - {album} at {final_expected_converted_path}")
return True, final_expected_converted_path
# Scan directory for other files with the target extension
for file_in_dir in os.listdir(scan_dir):
if file_in_dir.lower().endswith(final_target_ext):
file_path_to_check = os.path.join(scan_dir, file_in_dir)
# Skip if it's the same as the one we just checked (and it matched or didn't exist)
if file_path_to_check == final_expected_converted_path and os.path.exists(final_expected_converted_path):
continue
existing_title, existing_album = read_metadata_from_file(file_path_to_check, logger)
if existing_title == title and existing_album == album:
logger.info(f"Found existing track (converted extension scan): {title} - {album} at {file_path_to_check}")
return True, file_path_to_check
# If conversion is specified, and we didn't find the converted file, we should not report other formats as existing.
# The intention is to get the file in the `convert_to` format.
return False, None
else:
logger.warning(f"Invalid convert_to format: '{convert_to}'. Checking for original/general format.")
# Fall through to check original/general if convert_to was invalid
# Priority 2: Check if the file exists in its original download format
original_ext_lower = os.path.splitext(original_song_path)[1].lower()
if os.path.exists(original_song_path):
existing_title, existing_album = read_metadata_from_file(original_song_path, logger)
if existing_title == title and existing_album == album:
logger.info(f"Found existing track (exact original path match): {title} - {album} at {original_song_path}")
return True, original_song_path
# Scan directory for other files with the original extension (if no conversion target)
for file_in_dir in os.listdir(scan_dir):
if file_in_dir.lower().endswith(original_ext_lower):
file_path_to_check = os.path.join(scan_dir, file_in_dir)
if file_path_to_check == original_song_path: # Already checked this one
continue
existing_title, existing_album = read_metadata_from_file(file_path_to_check, logger)
if existing_title == title and existing_album == album:
logger.info(f"Found existing track (original extension scan): {title} - {album} at {file_path_to_check}")
return True, file_path_to_check
# Priority 3: General scan for any known audio format if no conversion was specified OR if convert_to was invalid
# This part only runs if convert_to is None or was an invalid format string.
if not convert_to or (convert_to and convert_to.upper() not in AUDIO_FORMATS):
for file_in_dir in os.listdir(scan_dir):
file_lower = file_in_dir.lower()
# Check against all known audio format extensions
is_known_audio_format = False
for fmt_details in AUDIO_FORMATS.values():
if file_lower.endswith(fmt_details["extension"].lower()):
is_known_audio_format = True
break
if is_known_audio_format:
# Skip if it's the original extension and we've already scanned for those
if file_lower.endswith(original_ext_lower):
# We've already checked exact original_song_path and scanned for original_ext_lower
# so this specific file would have been caught unless it's the original_song_path itself,
# or another file with original_ext_lower that didn't match metadata.
# This avoids re-checking files already covered by Priority 2 logic more explicitly.
pass # Let it proceed to metadata check if it wasn't an exact match path-wise
file_path_to_check = os.path.join(scan_dir, file_in_dir)
# Avoid re-checking original_song_path if it exists, it was covered by Priority 2's exact match.
if os.path.exists(original_song_path) and file_path_to_check == original_song_path:
continue
existing_title, existing_album = read_metadata_from_file(file_path_to_check, logger)
if existing_title == title and existing_album == album:
logger.info(f"Found existing track (general audio format scan): {title} - {album} at {file_path_to_check}")
return True, file_path_to_check
return False, None