179 lines
9.8 KiB
Python
179 lines
9.8 KiB
Python
#!/usr/bin/python3
|
|
|
|
import os
|
|
from mutagen import File
|
|
from mutagen.easyid3 import EasyID3
|
|
from mutagen.oggvorbis import OggVorbis
|
|
from mutagen.oggopus import OggOpus
|
|
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, 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
|
|
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 |