From 313b5677a5a72e038016895ed7df8ea2a085cb6d Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:39:16 -0700 Subject: [PATCH] Drop stale post-PR378 redefs and fix B009 Lifted-then-not-deleted leftovers from the PR378 merge: - web_server.py `_resolve_album_group` and `_build_final_path_for_track` were already imported at module top from `core/imports/`. Removed the shadowing local copies. - Mutagen reimports (FLAC/MP4/OggVorbis) at L17736-17738 shadowed the top-of-file imports. Picture/MP4Cover/MP4FreeForm were unused. Dropped the whole block. - core/imports/context.py: `getattr(artist, "name")` -> `artist.name` (B009). Ruff clean, 667 tests pass. --- core/imports/context.py | 2 +- web_server.py | 369 ---------------------------------------- 2 files changed, 1 insertion(+), 370 deletions(-) diff --git a/core/imports/context.py b/core/imports/context.py index f0987f8a..752528f0 100644 --- a/core/imports/context.py +++ b/core/imports/context.py @@ -36,7 +36,7 @@ def extract_artist_name(artist: Any) -> str: if isinstance(artist, dict): return str(artist.get("name", "") or "") if hasattr(artist, "name"): - return str(getattr(artist, "name") or "") + return str(artist.name or "") return str(artist) if artist else "" diff --git a/web_server.py b/web_server.py index 5d76cb76..c8e1bbfe 100644 --- a/web_server.py +++ b/web_server.py @@ -16870,76 +16870,6 @@ def _normalize_base_album_name(base_album: str, artist_name: str) -> str: logger.info(f"Album variant normalization: '{base_album}' -> '{normalized}'") return normalized -def _resolve_album_group(spotify_artist: dict, album_info: dict, original_album: str = None) -> str: - """ - Smart album grouping: Start with standard, upgrade to deluxe if ANY track is deluxe. - This ensures all tracks from the same album get the same folder name. - (Adapted from GUI downloads.py) - """ - try: - with album_cache_lock: - artist_name = spotify_artist["name"] - detected_album = album_info.get('album_name', '') - - # Extract base album name (without edition indicators) - if detected_album: - base_album = _get_base_album_name(detected_album) - elif original_album: - # Clean the original Soulseek album name - cleaned_original = _clean_album_title_web(original_album, artist_name) - base_album = _get_base_album_name(cleaned_original) - else: - base_album = _get_base_album_name(detected_album) - - # Normalize the base name (handle case variations, etc.) - base_album = _normalize_base_album_name(base_album, artist_name) - - # Create a key for this album group (artist + base album) - album_key = f"{artist_name}::{base_album}" - - # Check if we already have a cached result for this album - if album_key in album_name_cache: - cached_name = album_name_cache[album_key] - logger.info(f"Using cached album name for '{album_key}': '{cached_name}'") - return cached_name - - logger.info(f"Album grouping - Key: '{album_key}', Detected: '{detected_album}'") - - # Check if this track indicates a deluxe edition - is_deluxe_track = False - if detected_album: - is_deluxe_track = _detect_deluxe_edition(detected_album) - elif original_album: - is_deluxe_track = _detect_deluxe_edition(original_album) - - # Get current edition level for this album group (default to standard) - current_edition = album_editions.get(album_key, "standard") - - # SMART ALGORITHM: Upgrade to deluxe if this track is deluxe - if is_deluxe_track and current_edition == "standard": - logger.info(f"UPGRADE: Album '{base_album}' upgraded from standard to deluxe!") - album_editions[album_key] = "deluxe" - current_edition = "deluxe" - - # Build final album name based on edition level - if current_edition == "deluxe": - final_album_name = f"{base_album} (Deluxe Edition)" - else: - final_album_name = base_album - - # Store the resolution in both caches - album_groups[album_key] = final_album_name - album_name_cache[album_key] = final_album_name - album_artists[album_key] = artist_name - - logger.info(f"Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})") - - return final_album_name - - except Exception as e: - logger.error(f"Error resolving album group: {e}") - return album_info.get('album_name', 'Unknown Album') - def _clean_album_title_web(album_title: str, artist_name: str) -> str: """Clean up album title by removing common prefixes, suffixes, and artist redundancy""" import re @@ -17451,302 +17381,6 @@ def _compute_m3u_folder(transfer_dir, context_type, playlist_name, artist_name=' return os.path.join(transfer_dir, playlist_sanitized) -def _build_final_path_for_track(context, spotify_artist, album_info, file_ext): - """ - SHARED PATH BUILDER - Used by both post-processing AND verification. - This ensures they always produce the same path. - - Returns: (final_path, folder_created_successfully) - """ - transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer')) - track_info = context.get("track_info", {}) - original_search = context.get("original_search_result", {}) - playlist_folder_mode = track_info.get("_playlist_folder_mode", False) - - # ENHANCE BYPASS: Place file in same location as original (different extension OK) - _source_info = track_info.get('source_info') or {} - if isinstance(_source_info, str): - try: - _source_info = json.loads(_source_info) - except (json.JSONDecodeError, TypeError): - _source_info = {} - if _source_info.get('enhance') and _source_info.get('original_file_path'): - original_path = _source_info['original_file_path'] - original_dir = os.path.dirname(original_path) - original_stem = os.path.splitext(os.path.basename(original_path))[0] - final_path = os.path.join(original_dir, original_stem + file_ext) - os.makedirs(original_dir, exist_ok=True) - logger.info(f"[Enhance] Using original file location: {final_path}") - return final_path, True - - # Extract year and album_type from spotify_album for template use (safe for all modes) - year = '' # Empty string instead of 'Unknown' to avoid "Unknown albumName" - spotify_album = context.get("spotify_album", {}) - if spotify_album and spotify_album.get('release_date'): - release_date = spotify_album['release_date'] - if release_date and len(release_date) >= 4: - year = release_date[:4] - - # Album type for $albumtype template variable (Album, EP, Single, Compilation) - raw_album_type = '' - if spotify_album: - raw_album_type = spotify_album.get('album_type', '') or '' - total_tracks = (spotify_album.get('total_tracks', 0) or 0) if spotify_album else 0 - - if raw_album_type.lower() == 'compilation': - album_type_display = 'Compilation' - elif raw_album_type.lower() == 'album': - album_type_display = 'Album' - elif raw_album_type.lower() in ('single', 'ep'): - # Spotify labels both singles and EPs as 'single' — use track count to distinguish - if total_tracks <= 3: - album_type_display = 'Single' - elif total_tracks <= 6: - album_type_display = 'EP' - else: - album_type_display = 'Album' - elif not raw_album_type: - # No album_type from API (e.g. iTunes source) — infer from track count - if total_tracks <= 0: - album_type_display = 'Album' # Unknown, safe default - elif total_tracks <= 3: - album_type_display = 'Single' - elif total_tracks <= 6: - album_type_display = 'EP' - else: - album_type_display = 'Album' - else: - album_type_display = raw_album_type.capitalize() or 'Album' - - # Determine which template type to use - if playlist_folder_mode: - # PLAYLIST MODE - playlist_name = track_info.get("_playlist_name", "Unknown Playlist") - track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track') - - _artists = original_search.get('artists') or track_info.get('artists') or [] - - template_context = { - 'artist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name, - 'albumartist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name, - 'album': track_name, - 'title': track_name, - 'playlist_name': playlist_name, - 'track_number': 1, - 'disc_number': 1, - 'year': year, - 'quality': context.get('_audio_quality', ''), - 'albumtype': album_type_display, - '_artists_list': _artists, - '_itunes_artist_id': str(spotify_artist.get('id', '')) if isinstance(spotify_artist, dict) and str(spotify_artist.get('id', '')).isdigit() and (original_search.get('_source') == 'itunes' or track_info.get('_source') == 'itunes') else None, - } - - folder_path, filename_base = _get_file_path_from_template(template_context, 'playlist_path') - if folder_path and filename_base: - final_path = os.path.join(transfer_dir, folder_path, filename_base + file_ext) - os.makedirs(os.path.join(transfer_dir, folder_path), exist_ok=True) - return final_path, True - else: - # Fallback - playlist_name_sanitized = _sanitize_filename(playlist_name) - playlist_dir = os.path.join(transfer_dir, playlist_name_sanitized) - os.makedirs(playlist_dir, exist_ok=True) - artist_name_sanitized = _sanitize_filename(template_context['artist']) - track_name_sanitized = _sanitize_filename(track_name) - new_filename = f"{artist_name_sanitized} - {track_name_sanitized}{file_ext}" - return os.path.join(playlist_dir, new_filename), True - - elif album_info and album_info.get('is_album'): - # ALBUM MODE - clean_track_name = album_info.get('clean_track_name', 'Unknown Track') - if original_search.get('spotify_clean_title'): - clean_track_name = original_search['spotify_clean_title'] - elif album_info.get('clean_track_name'): - clean_track_name = album_info['clean_track_name'] - else: - clean_track_name = original_search.get('title', 'Unknown Track') - - track_number = album_info.get('track_number', 1) - if track_number is None or not isinstance(track_number, int) or track_number < 1: - track_number = 1 - - # Multi-disc album subfolder support - disc_number = album_info.get('disc_number', 1) - - # Get structured artists list for collab artist handling - _artists = original_search.get('artists') or track_info.get('artists') or [] - # Extract iTunes artist ID for primary artist resolution (only for iTunes source) - _spotify_album = context.get('spotify_album', {}) - _itunes_aid = None - _track_source = original_search.get('_source') or track_info.get('_source', '') - _is_itunes = _track_source == 'itunes' or (isinstance(spotify_artist, dict) and str(spotify_artist.get('id', '')).isdigit() and _track_source != 'deezer') - if _is_itunes and isinstance(spotify_artist, dict): - _aid = spotify_artist.get('id', '') - if str(_aid).isdigit(): - _itunes_aid = str(_aid) - if not _itunes_aid and _spotify_album: - _ext = _spotify_album.get('external_urls', {}) - if isinstance(_ext, dict) and _ext.get('itunes_artist_id'): - _itunes_aid = _ext['itunes_artist_id'] - - # Resolve album-level artist name for $albumartist. - # Per-track spotify_artist may vary on collab albums or after artist name changes. - # Prefer stable album-level sources so all tracks land in the same folder. - _artist_name = spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name - _album_artist_name = _artist_name # default: same as track artist - - # Build album-level artists list for collab mode resolution. - # Using album-level artists (instead of per-track _artists) ensures collab mode - # produces the SAME result for every track, preventing folder/tag splits. - _album_artists_for_collab = None # None = fall back to per-track _artists - _explicit_artist_ctx = track_info.get('_explicit_artist_context') if isinstance(track_info, dict) else None - if isinstance(_explicit_artist_ctx, dict) and _explicit_artist_ctx.get('name'): - _album_artist_name = _explicit_artist_ctx['name'] - _album_artists_for_collab = [_explicit_artist_ctx] - elif isinstance(_explicit_artist_ctx, str) and _explicit_artist_ctx: - _album_artist_name = _explicit_artist_ctx - _album_artists_for_collab = [{'name': _explicit_artist_ctx}] - else: - _sa_artists = _spotify_album.get('artists', []) if _spotify_album else [] - if _sa_artists: - _first_sa = _sa_artists[0] - if isinstance(_first_sa, dict) and _first_sa.get('name'): - _album_artist_name = _first_sa['name'] - elif isinstance(_first_sa, str) and _first_sa: - _album_artist_name = _first_sa - _album_artists_for_collab = _sa_artists - - template_context = { - 'artist': _artist_name, - 'albumartist': _album_artist_name, - 'album': album_info['album_name'], - 'title': clean_track_name, - 'track_number': track_number, - 'disc_number': disc_number, - 'year': year, - 'quality': context.get('_audio_quality', ''), - 'albumtype': album_type_display, - '_artists_list': _album_artists_for_collab if _album_artists_for_collab else _artists, - '_itunes_artist_id': _itunes_aid, - } - spotify_album = context.get('spotify_album', {}) - total_discs = spotify_album.get('total_discs', 1) if spotify_album else 1 - - # Single-track downloads from search don't know total_discs — resolve it - if total_discs <= 1 and spotify_album and spotify_album.get('id'): - # Quick check: if this track is on disc 2+, album is definitely multi-disc - if disc_number > 1: - total_discs = disc_number - else: - # Fetch album tracks to compute total_discs (cached by metadata client) - try: - _album_id = spotify_album['id'] - _fb_client = _get_metadata_fallback_client() - if _fb_client: - _atd = _fb_client.get_album_tracks(str(_album_id)) - if _atd and _atd.get('items'): - total_discs = max((t.get('disc_number', 1) for t in _atd['items']), default=1) - if total_discs > 1: - spotify_album['total_discs'] = total_discs - logger.info(f"[Multi-Disc] Resolved {total_discs} discs for single-track download of '{spotify_album.get('name')}'") - except Exception as _disc_err: - logger.warning(f"[Multi-Disc] Could not resolve total_discs: {_disc_err}") - - # Check if user controls disc structure via $disc in their template - album_template = config_manager.get('file_organization.templates.album_path', '') - user_controls_disc = '$disc' in album_template - - disc_label = config_manager.get('file_organization.disc_label', 'Disc') - - # total_discs was resolved above — pass it into the template context so - # $cdnum can emit "CDxx" only for real multi-disc albums. - template_context['total_discs'] = total_discs - - folder_path, filename_base = _get_file_path_from_template(template_context, 'album_path') - if folder_path and filename_base: - if total_discs > 1 and not user_controls_disc: - disc_folder = f"{disc_label} {disc_number}" - final_path = os.path.join(transfer_dir, folder_path, disc_folder, filename_base + file_ext) - os.makedirs(os.path.join(transfer_dir, folder_path, disc_folder), exist_ok=True) - else: - final_path = os.path.join(transfer_dir, folder_path, filename_base + file_ext) - os.makedirs(os.path.join(transfer_dir, folder_path), exist_ok=True) - return final_path, True - else: - # Fallback — use albumartist for folder consistency (same as template) - artist_name_sanitized = _sanitize_filename(template_context['albumartist']) - album_name_sanitized = _sanitize_filename(album_info['album_name']) - artist_dir = os.path.join(transfer_dir, artist_name_sanitized) - album_folder_name = f"{artist_name_sanitized} - {album_name_sanitized}" - album_dir = os.path.join(artist_dir, album_folder_name) - if total_discs > 1: - album_dir = os.path.join(album_dir, f"{disc_label} {disc_number}") - os.makedirs(album_dir, exist_ok=True) - final_track_name_sanitized = _sanitize_filename(clean_track_name) - new_filename = f"{track_number:02d} - {final_track_name_sanitized}{file_ext}" - return os.path.join(album_dir, new_filename), True - - else: - # SINGLE MODE - clean_track_name = album_info.get('clean_track_name', 'Unknown Track') if album_info else 'Unknown Track' - if original_search.get('spotify_clean_title'): - clean_track_name = original_search['spotify_clean_title'] - elif album_info and album_info.get('clean_track_name'): - clean_track_name = album_info['clean_track_name'] - else: - clean_track_name = original_search.get('title', 'Unknown Track') - - # Get structured artists list for collab artist handling (same as album/playlist modes) - _artists = original_search.get('artists') or track_info.get('artists') or [] - # Extract iTunes artist ID for primary artist resolution (only for iTunes source) - _spotify_album = context.get('spotify_album', {}) - _itunes_aid = None - _track_source = original_search.get('_source') or track_info.get('_source', '') - _is_itunes = _track_source == 'itunes' or (isinstance(spotify_artist, dict) and str(spotify_artist.get('id', '')).isdigit() and _track_source != 'deezer') - if _is_itunes and isinstance(spotify_artist, dict): - _aid = spotify_artist.get('id', '') - if str(_aid).isdigit(): - _itunes_aid = str(_aid) - if not _itunes_aid and _spotify_album: - _ext = _spotify_album.get('external_urls', {}) - if isinstance(_ext, dict) and _ext.get('itunes_artist_id'): - _itunes_aid = _ext['itunes_artist_id'] - - template_context = { - 'artist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name, - 'albumartist': spotify_artist["name"] if isinstance(spotify_artist, dict) else spotify_artist.name, - 'album': album_info.get('album_name', clean_track_name) if album_info else clean_track_name, - 'title': clean_track_name, - 'track_number': 1, - 'disc_number': 1, - 'year': year, - 'quality': context.get('_audio_quality', ''), - 'albumtype': album_type_display, - '_artists_list': _artists, - '_itunes_artist_id': _itunes_aid, - } - - folder_path, filename_base = _get_file_path_from_template(template_context, 'single_path') - if filename_base: - # folder_path may be '' for flat templates like "$artist - $title" (no subfolders) - if folder_path: - final_path = os.path.join(transfer_dir, folder_path, filename_base + file_ext) - os.makedirs(os.path.join(transfer_dir, folder_path), exist_ok=True) - else: - final_path = os.path.join(transfer_dir, filename_base + file_ext) - os.makedirs(transfer_dir, exist_ok=True) - return final_path, True - else: - # Fallback - artist_name_sanitized = _sanitize_filename(template_context['artist']) - final_track_name_sanitized = _sanitize_filename(clean_track_name) - artist_dir = os.path.join(transfer_dir, artist_name_sanitized) - single_folder_name = f"{artist_name_sanitized} - {final_track_name_sanitized}" - single_dir = os.path.join(artist_dir, single_folder_name) - os.makedirs(single_dir, exist_ok=True) - new_filename = f"{final_track_name_sanitized}{file_ext}" - return os.path.join(single_dir, new_filename), True - def _get_file_path_from_template_raw(template: str, context: dict) -> tuple: """ Build file path using a user-provided template string directly. @@ -18099,9 +17733,6 @@ def _get_file_path_from_template(context: dict, template_type: str = 'album_path # =================================================================== from mutagen import File as MutagenFile from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TRCK, TCON, TPE2, TPOS, TXXX, APIC, UFID, TSRC, TBPM, TCOP, TPUB, TMED, TDOR -from mutagen.flac import FLAC, Picture -from mutagen.mp4 import MP4, MP4Cover, MP4FreeForm -from mutagen.oggvorbis import OggVorbis from mutagen.apev2 import APEv2, APENoHeaderError import urllib.request