From d91e6a384d4cf802033d311b2342702bb665bea6 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Thu, 4 Jun 2026 09:33:03 -0700 Subject: [PATCH] Remove the old Retag Tool (superseded by Library Re-tag job + Write Tags) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old per-download Retag Tool was limited (only native-pipeline downloads, 100-group cap, manual per-group) and did the wrong thing — it moved/reorganized files instead of just tagging. It's superseded by the new Library Re-tag job (whole-library, in-place) + the enhanced-library 'Write Tags' button. Removed: the post-download record_retag_download ingestion hook (stops writing retag_groups on every download), core/library/retag.py, the web_server state + deps + /api/retag/* endpoints + the tool:retag WebSocket emit, the dashboard card + both modals (index.html), the core.js socket handler, and the tools-page wiring + help entry (wishlist-tools.js). Updated the import-pipeline test. Verified: web_server parses, app + core imports OK, 392 tests pass, no live references to removed symbols. Left as inert (harmless) for a careful follow-up sweep: the retag_groups/ retag_tracks tables + their DB CRUD methods (no longer written/read), and the now-orphaned retag JS helper functions (no entry point/wiring/socket calls them; interspersed with wishlist functions, so not blind-deleted). --- core/imports/pipeline.py | 8 - core/imports/side_effects.py | 85 ------- core/library/retag.py | 350 -------------------------- tests/imports/test_import_pipeline.py | 1 - web_server.py | 168 ------------- webui/index.html | 74 ------ webui/static/core.js | 1 - webui/static/wishlist-tools.js | 43 ---- 8 files changed, 730 deletions(-) delete mode 100644 core/library/retag.py diff --git a/core/imports/pipeline.py b/core/imports/pipeline.py index dc6c9bf4..067e4559 100644 --- a/core/imports/pipeline.py +++ b/core/imports/pipeline.py @@ -40,7 +40,6 @@ from core.imports.side_effects import ( emit_track_downloaded, record_download_provenance, record_library_history_download, - record_retag_download, record_soulsync_library_entry, ) from core.wishlist.resolution import check_and_remove_from_wishlist @@ -892,13 +891,6 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta record_download_provenance(context) record_soulsync_library_entry(context, artist_context, album_info) - try: - if not playlist_folder_mode: - completed_path = context.get('_final_processed_path', final_path) - record_retag_download(context, artist_context, album_info, completed_path) - except Exception as retag_err: - logger.error(f"[Post-Process] Retag data capture failed (non-fatal): {retag_err}") - try: completed_path = context.get('_final_processed_path', final_path) batch_id_for_repair = context.get('batch_id') diff --git a/core/imports/side_effects.py b/core/imports/side_effects.py index 6513aabf..7d9d92c9 100644 --- a/core/imports/side_effects.py +++ b/core/imports/side_effects.py @@ -676,88 +676,3 @@ def record_soulsync_library_entry(context: Dict[str, Any], artist_context: Dict[ logger.info("[SoulSync Library] Added: %s / %s / %s", artist_name, album_name, track_name) except Exception as exc: logger.error("[SoulSync Library] Could not record library entry: %s", exc) - - -def record_retag_download(context: Dict[str, Any], artist_context: Dict[str, Any], album_info: Dict[str, Any], final_path: str) -> None: - """Record a completed download for later re-tagging.""" - try: - db = get_database() - - context = normalize_import_context(context) - artist_context = get_import_context_artist(context) or (artist_context if isinstance(artist_context, dict) else {}) - album_context = get_import_context_album(context) - track_info = get_import_track_info(context) - original_search = get_import_original_search(context) - source = get_import_source(context) - source_ids = get_import_source_ids(context) - - artist_name = extract_artist_name(artist_context) or get_import_clean_artist(context, default="Unknown Artist") - is_album = album_info and album_info.get("is_album", False) - group_type = "album" if is_album else "single" - album_name = album_info.get("album_name", "") if album_info else get_import_clean_album(context, default=original_search.get("album", "Unknown")) - - image_url = album_info.get("album_image_url") if album_info else None - if not image_url: - image_url = album_context.get("image_url", "") - if not image_url and album_context.get("images"): - images = album_context.get("images", []) - if images and isinstance(images[0], dict): - image_url = images[0].get("url", "") - - total_tracks = album_context.get("total_tracks", 1) if album_context else 1 - release_date = album_context.get("release_date", "") if album_context else "" - - spotify_album_id = None - itunes_album_id = None - if source == "spotify": - spotify_album_id = source_ids.get("album_id", "") or None - elif source == "itunes": - itunes_album_id = source_ids.get("album_id", "") or None - - group_id = db.find_retag_group(artist_name, album_name) - if group_id is None: - group_id = db.add_retag_group( - group_type=group_type, - artist_name=artist_name, - album_name=album_name, - image_url=image_url, - spotify_album_id=spotify_album_id, - itunes_album_id=itunes_album_id, - total_tracks=total_tracks, - release_date=release_date, - ) - if group_id is None: - return - - track_number = album_info.get("track_number", 1) if album_info else (track_info.get("track_number", 1) or 1) - disc_number = original_search.get("disc_number") or (album_info.get("disc_number", 1) if album_info else track_info.get("disc_number", 1) or 1) - title = get_import_clean_title( - context, - album_info=album_info, - default=album_info.get("clean_track_name", "Unknown Track") if album_info else "Unknown Track", - ) - file_format = os.path.splitext(str(final_path))[1].lstrip(".").lower() - - source_track_id = None - itunes_track_id = None - if source == "spotify": - source_track_id = source_ids.get("track_id", "") or None - elif source == "itunes": - itunes_track_id = source_ids.get("track_id", "") or None - - if not db.retag_track_exists(group_id, str(final_path)): - db.add_retag_track( - group_id=group_id, - track_number=track_number, - disc_number=disc_number, - title=title, - file_path=str(final_path), - file_format=file_format, - spotify_track_id=source_track_id, - itunes_track_id=itunes_track_id, - ) - logger.info("[Retag] Recorded track for retag: '%s' in '%s'", title, album_name) - - db.trim_retag_groups(100) - except Exception as exc: - logger.error("[Retag] Could not record track for retag: %s", exc) diff --git a/core/library/retag.py b/core/library/retag.py deleted file mode 100644 index f5cf74e7..00000000 --- a/core/library/retag.py +++ /dev/null @@ -1,350 +0,0 @@ -"""Library retag worker. - -`execute_retag(group_id, album_id, deps)` rewrites tags + filenames for a -group of audio files when the user has matched them to a different -album. The worker: - -1. Fetches album + track metadata for the new `album_id` (Spotify or - iTunes — Spotify client transparently falls back). -2. Loads existing files in the retag group from the DB. -3. Matches each existing track to a new Spotify track: - - Priority 1: same disc + track number. - - Priority 2: title similarity >= 0.6 (SequenceMatcher). -4. For each matched pair: - - Re-write metadata tags via `_enhance_file_metadata`. - - Compute the new path via `_build_final_path_for_track` and move - the audio file (plus .lrc / .txt sidecars) if the path changes. - - Drop an orphaned cover.jpg if it's left in an empty directory. - - Clean up empty parent directories left behind. - - Download the new cover art into the new album dir. -5. Update the retag group record with the new artist / album / image / - total_tracks / release_date and the appropriate Spotify-or-iTunes - album ID. -6. Mark the retag state 'finished' (or 'error' on exception). - -The original mutated `retag_state` as a module global. Here it's exposed -through the `RetagDeps` proxy as a Python property so the lifted body -keeps the same `name[key] = value` syntax. The property setter rebinds -the web_server.py reference if needed (currently the function only -mutates in place via .update() and key assignment, so the setter never -fires). -""" - -from __future__ import annotations - -import logging -import os -import traceback -from dataclasses import dataclass -from difflib import SequenceMatcher -from typing import Any, Callable, Optional - -logger = logging.getLogger(__name__) - - -@dataclass -class RetagDeps: - """Bundle of cross-cutting deps the retag worker needs. - - `retag_state` is exposed as a property so the lifted body keeps - `name[key] = value` / `name.update(...)` syntax. - """ - config_manager: Any - retag_lock: Any # threading.Lock - spotify_client: Any - get_audio_quality_string: Callable[[str], str] - enhance_file_metadata: Callable - build_final_path_for_track: Callable - safe_move_file: Callable - cleanup_empty_directories: Callable - download_cover_art: Callable - docker_resolve_path: Callable[[str], str] - _get_retag_state: Callable[[], dict] - _set_retag_state: Callable[[dict], None] - get_database: Callable[[], Any] - # Discord report (Netti93) — retag was clearing the LYRICS / USLT - # tag without rewriting it, while the download pipeline calls - # `generate_lrc_file` after enrichment to refetch + embed lyrics. - # Injected here so retag mirrors the same post-enrichment step. - # Optional for backward compat with any test caller that builds - # RetagDeps without the new field — empty default no-ops the call. - generate_lrc_file: Optional[Callable] = None - - @property - def retag_state(self) -> dict: - return self._get_retag_state() - - @retag_state.setter - def retag_state(self, value: dict) -> None: - self._set_retag_state(value) - - -def execute_retag(group_id, album_id, deps: RetagDeps): - """Execute a retag operation: re-tag files in a group with metadata from a new album match.""" - try: - with deps.retag_lock: - deps.retag_state.update({ - "status": "running", - "phase": "Fetching album metadata...", - "progress": 0, - "current_track": "", - "total_tracks": 0, - "processed": 0, - "error_message": "" - }) - - # 1. Fetch new album metadata from Spotify/iTunes - album_data = deps.spotify_client.get_album(album_id) - if not album_data: - raise ValueError(f"Could not fetch album data for ID: {album_id}") - - album_tracks_response = deps.spotify_client.get_album_tracks(album_id) - if not album_tracks_response: - raise ValueError(f"Could not fetch album tracks for ID: {album_id}") - - album_tracks_items = album_tracks_response.get('items', []) - - # Extract artist info - album_artists = album_data.get('artists', []) - new_artist = album_artists[0] if album_artists else {'name': 'Unknown Artist', 'id': ''} - # Ensure artist is a dict with expected fields - if not isinstance(new_artist, dict): - new_artist = {'name': str(new_artist), 'id': ''} - new_album_name = album_data.get('name', 'Unknown Album') - new_images = album_data.get('images', []) - new_image_url = new_images[0]['url'] if new_images else None - new_release_date = album_data.get('release_date', '') - total_tracks = album_data.get('total_tracks', len(album_tracks_items)) - - # Build spotify track list - spotify_tracks = [] - for item in album_tracks_items: - track_artists = item.get('artists', []) - spotify_tracks.append({ - 'name': item.get('name', ''), - 'track_number': item.get('track_number', 1), - 'disc_number': item.get('disc_number', 1), - 'id': item.get('id', ''), - 'artists': track_artists, - 'duration_ms': item.get('duration_ms', 0) - }) - - total_discs = max((t['disc_number'] for t in spotify_tracks), default=1) - - # 2. Load existing tracks for this group - db = deps.get_database() - existing_tracks = db.get_retag_tracks(group_id) - if not existing_tracks: - raise ValueError(f"No tracks found for retag group {group_id}") - - with deps.retag_lock: - deps.retag_state['total_tracks'] = len(existing_tracks) - deps.retag_state['phase'] = "Matching tracks..." - - # 3. Match existing files to new tracklist - matched_pairs = [] - for existing_track in existing_tracks: - best_match = None - best_score = 0 - - # Priority 1: Match by track number - for st in spotify_tracks: - if (st['track_number'] == existing_track.get('track_number') and - st['disc_number'] == existing_track.get('disc_number', 1)): - best_match = st - best_score = 1.0 - break - - # Priority 2: Match by title similarity - if not best_match: - from difflib import SequenceMatcher - existing_title = (existing_track.get('title') or '').lower().strip() - for st in spotify_tracks: - st_title = (st.get('name') or '').lower().strip() - score = SequenceMatcher(None, existing_title, st_title).ratio() - if score > best_score and score > 0.6: - best_score = score - best_match = st - - if best_match: - matched_pairs.append((existing_track, best_match)) - else: - logger.warning(f"[Retag] No match found for track: '{existing_track.get('title')}'") - matched_pairs.append((existing_track, None)) - - with deps.retag_lock: - deps.retag_state['phase'] = "Retagging files..." - - # 4. Retag each matched track - for existing_track, matched_spotify in matched_pairs: - current_file_path = existing_track.get('file_path', '') - track_title = matched_spotify['name'] if matched_spotify else existing_track.get('title', 'Unknown') - - with deps.retag_lock: - deps.retag_state['current_track'] = track_title - - if not matched_spotify: - with deps.retag_lock: - deps.retag_state['processed'] += 1 - deps.retag_state['progress'] = int(deps.retag_state['processed'] / deps.retag_state['total_tracks'] * 100) - continue - - # Verify file exists - if not os.path.exists(current_file_path): - logger.warning(f"[Retag] File not found, skipping: {current_file_path}") - with deps.retag_lock: - deps.retag_state['processed'] += 1 - deps.retag_state['progress'] = int(deps.retag_state['processed'] / deps.retag_state['total_tracks'] * 100) - continue - - # Build synthetic context for _enhance_file_metadata - track_artists = matched_spotify.get('artists', []) - context = { - 'original_search_result': { - 'spotify_clean_title': matched_spotify['name'], - 'spotify_clean_album': new_album_name, - 'track_number': matched_spotify['track_number'], - 'disc_number': matched_spotify.get('disc_number', 1), - 'artists': track_artists, - 'title': matched_spotify['name'] - }, - 'spotify_album': { - 'id': album_id, - 'name': new_album_name, - 'release_date': new_release_date, - 'total_tracks': total_tracks, - 'image_url': new_image_url, - 'total_discs': total_discs - }, - 'track_info': {'id': matched_spotify['id']}, - 'spotify_artist': new_artist, - '_audio_quality': deps.get_audio_quality_string(current_file_path) or '' - } - - album_info = { - 'is_album': total_tracks > 1, - 'album_name': new_album_name, - 'track_number': matched_spotify['track_number'], - 'disc_number': matched_spotify.get('disc_number', 1), - 'clean_track_name': matched_spotify['name'], - 'album_image_url': new_image_url - } - - # Re-write metadata tags - try: - deps.enhance_file_metadata(current_file_path, context, new_artist, album_info) - logger.info(f"[Retag] Re-tagged: '{track_title}'") - except Exception as meta_err: - logger.error(f"[Retag] Metadata write failed for '{track_title}': {meta_err}") - - # Discord report (Netti93) — `enhance_file_metadata` clears - # ALL tags (incl. USLT lyrics) and rewrites only the source - # metadata. The download pipeline calls `generate_lrc_file` - # after enrichment to refetch + embed lyrics — retag was - # missing that step and dropped the LYRICS tag with no - # rewrite. Mirroring the download path's post-enrichment - # step. Same args, same `lrclib_enabled` config gate, same - # idempotency (skip when sidecar already present). - if deps.generate_lrc_file: - try: - deps.generate_lrc_file(current_file_path, context, new_artist, album_info) - except Exception as lrc_err: - logger.debug("[Retag] generate_lrc_file failed for '%s': %s", track_title, lrc_err) - - # Compute new path and move if different - file_ext = os.path.splitext(current_file_path)[1] - try: - new_path, _ = deps.build_final_path_for_track(context, new_artist, album_info, file_ext) - - if os.path.normpath(current_file_path) != os.path.normpath(new_path): - logger.info(f"[Retag] Moving '{os.path.basename(current_file_path)}' -> '{new_path}'") - old_dir = os.path.dirname(current_file_path) - os.makedirs(os.path.dirname(new_path), exist_ok=True) - deps.safe_move_file(current_file_path, new_path) - - # Move lyrics sidecar file alongside audio file if it exists - for lyrics_ext in ('.lrc', '.txt'): - old_lyrics = os.path.splitext(current_file_path)[0] + lyrics_ext - if os.path.exists(old_lyrics): - new_lyrics = os.path.splitext(new_path)[0] + lyrics_ext - try: - deps.safe_move_file(old_lyrics, new_lyrics) - logger.info(f"[Retag] Moved {lyrics_ext} file alongside audio") - except Exception as lrc_err: - logger.error(f"[Retag] Failed to move {lyrics_ext} file: {lrc_err}") - - # Remove old cover.jpg if directory changed and old dir is now empty of audio - new_dir = os.path.dirname(new_path) - if os.path.normpath(old_dir) != os.path.normpath(new_dir): - old_cover = os.path.join(old_dir, 'cover.jpg') - if os.path.exists(old_cover): - # Check if any audio files remain in old directory - audio_exts = {'.flac', '.mp3', '.m4a', '.ogg', '.opus', '.wav', '.aac'} - remaining_audio = [f for f in os.listdir(old_dir) - if os.path.splitext(f)[1].lower() in audio_exts] - if not remaining_audio: - try: - os.remove(old_cover) - logger.warning("[Retag] Removed orphaned cover.jpg from old directory") - except Exception as e: - logger.debug("remove orphaned cover failed: %s", e) - - # Cleanup old empty directories - transfer_dir = deps.docker_resolve_path(deps.config_manager.get('soulseek.transfer_path', './Transfer')) - deps.cleanup_empty_directories(transfer_dir, current_file_path) - - # Update DB record - db.update_retag_track_path(existing_track['id'], str(new_path)) - current_file_path = new_path - else: - logger.warning(f"[Retag] Path unchanged for '{track_title}', no move needed") - except Exception as move_err: - logger.error(f"[Retag] Path/move failed for '{track_title}': {move_err}") - - # Download cover art to album directory - try: - deps.download_cover_art(album_info, os.path.dirname(current_file_path), context) - except Exception as cover_err: - logger.error(f"[Retag] Cover art download failed: {cover_err}") - - with deps.retag_lock: - deps.retag_state['processed'] += 1 - deps.retag_state['progress'] = int(deps.retag_state['processed'] / deps.retag_state['total_tracks'] * 100) - - # 5. Update the retag group record with new metadata - update_kwargs = { - 'artist_name': new_artist.get('name', 'Unknown Artist'), - 'album_name': new_album_name, - 'image_url': new_image_url, - 'total_tracks': total_tracks, - 'release_date': new_release_date - } - # Set the correct ID field based on Spotify vs iTunes - if str(album_id).isdigit(): - update_kwargs['itunes_album_id'] = album_id - update_kwargs['spotify_album_id'] = None - else: - update_kwargs['spotify_album_id'] = album_id - update_kwargs['itunes_album_id'] = None - - db.update_retag_group(group_id, **update_kwargs) - - with deps.retag_lock: - deps.retag_state.update({ - "status": "finished", - "phase": "Retag complete!", - "progress": 100, - "current_track": "" - }) - logger.info(f"[Retag] Retag operation complete for group {group_id}") - - except Exception as e: - import traceback - logger.error(f"[Retag] Error during retag: {e}") - logger.error(traceback.format_exc()) - with deps.retag_lock: - deps.retag_state.update({ - "status": "error", - "phase": "Error", - "error_message": str(e) - }) diff --git a/tests/imports/test_import_pipeline.py b/tests/imports/test_import_pipeline.py index 13684241..b283cb7e 100644 --- a/tests/imports/test_import_pipeline.py +++ b/tests/imports/test_import_pipeline.py @@ -209,7 +209,6 @@ def test_post_process_matched_download_forwards_separate_metadata_runtime(tmp_pa monkeypatch.setattr(import_pipeline, "record_soulsync_library_entry", _record_library) monkeypatch.setattr(import_pipeline, "check_and_remove_from_wishlist", lambda *args, **kwargs: None) - monkeypatch.setattr(import_pipeline, "record_retag_download", lambda *args, **kwargs: None) context = { "track_info": {"_playlist_folder_mode": True, "_playlist_name": "Playlist"}, diff --git a/web_server.py b/web_server.py index 2dd699b9..04390e3a 100644 --- a/web_server.py +++ b/web_server.py @@ -862,19 +862,6 @@ duplicate_cleaner_state = { duplicate_cleaner_lock = threading.Lock() duplicate_cleaner_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="DuplicateCleaner") -# Retag Tool Globals -retag_state = { - "status": "idle", - "phase": "Ready", - "progress": 0, - "current_track": "", - "total_tracks": 0, - "processed": 0, - "error_message": "", -} -retag_lock = threading.Lock() -retag_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="RetagWorker") - # Download Missing Tracks Modal State Management # Thread-safe state tracking for modal download functionality. # Shared task/batch state now lives in core.runtime_state. @@ -1709,7 +1696,6 @@ def _shutdown_runtime_components(): (db_update_executor, "db update executor"), (quality_scanner_executor, "quality scanner executor"), (duplicate_cleaner_executor, "duplicate cleaner executor"), - (retag_executor, "retag executor"), (sync_executor, "sync executor"), (missing_download_executor, "missing download executor"), (album_bundle_executor, "album bundle executor"), @@ -14274,45 +14260,6 @@ _download_retry_attempts = {} # {context_key: {'count': N, 'first_attempt': tim _download_retry_max = 10 # Max retries before giving up (10 seconds with 1s poll interval) _download_retry_lock = threading.Lock() -# Retag worker logic lives in core/library/retag.py. -from core.library import retag as _library_retag - - -def _build_retag_deps(): - """Build the RetagDeps bundle from web_server.py globals on each call.""" - from database.music_database import get_database as _get_db - - def _get_state(): - return retag_state - - def _set_state(value): - global retag_state - retag_state = value - - from core.metadata.lyrics import generate_lrc_file as _generate_lrc_file - - return _library_retag.RetagDeps( - config_manager=config_manager, - retag_lock=retag_lock, - spotify_client=spotify_client, - get_audio_quality_string=_get_audio_quality_string, - enhance_file_metadata=_enhance_file_metadata, - build_final_path_for_track=_build_final_path_for_track, - safe_move_file=_safe_move_file, - cleanup_empty_directories=_cleanup_empty_directories, - download_cover_art=_download_cover_art, - docker_resolve_path=docker_resolve_path, - _get_retag_state=_get_state, - _set_retag_state=_set_state, - get_database=_get_db, - generate_lrc_file=_generate_lrc_file, - ) - - -def _execute_retag(group_id, album_id): - return _library_retag.execute_retag(group_id, album_id, _build_retag_deps()) - - def _automatic_wishlist_cleanup_after_db_update(): """Automatic wishlist cleanup that runs after database updates.""" return _cleanup_wishlist_after_db_update(logger=logger) @@ -16461,115 +16408,6 @@ def stop_duplicate_cleaner(): # == RETAG TOOL ENDPOINTS == # =============================== -@app.route('/api/retag/stats', methods=['GET']) -def get_retag_stats(): - """Get retag tool statistics for the dashboard card.""" - from database.music_database import get_database - db = get_database() - stats = db.get_retag_stats() - return jsonify({"success": True, **stats}) - -@app.route('/api/retag/groups', methods=['GET']) -def get_retag_groups(): - """Get all retag groups sorted by artist name.""" - from database.music_database import get_database - db = get_database() - groups = db.get_retag_groups() - return jsonify({"success": True, "groups": groups}) - -@app.route('/api/retag/groups//tracks', methods=['GET']) -def get_retag_group_tracks(group_id): - """Get tracks for a specific retag group.""" - from database.music_database import get_database - db = get_database() - tracks = db.get_retag_tracks(group_id) - return jsonify({"success": True, "tracks": tracks}) - -@app.route('/api/retag/search', methods=['GET']) -def search_retag_albums(): - """Search for albums to use for retagging (uses Spotify/iTunes fallback).""" - query = request.args.get('q', '').strip() - if not query: - return jsonify({"success": False, "error": "Query parameter 'q' is required"}), 400 - - limit = min(int(request.args.get('limit', 12)), 50) - try: - results = spotify_client.search_albums(query, limit=limit) - albums = [] - for a in results: - albums.append({ - 'id': str(a.id), - 'name': a.name, - 'artist': ', '.join(a.artists) if a.artists else 'Unknown Artist', - 'release_date': a.release_date or '', - 'total_tracks': a.total_tracks, - 'image_url': a.image_url, - 'album_type': a.album_type or 'album' - }) - return jsonify({"success": True, "albums": albums}) - except Exception as e: - logger.error(f"[Retag] Album search error: {e}") - return jsonify({"success": False, "error": str(e)}), 500 - -@app.route('/api/retag/execute', methods=['POST']) -def execute_retag(): - """Start a retag operation for a group with a new album match.""" - data = request.get_json() - if not data: - return jsonify({"success": False, "error": "JSON body required"}), 400 - - group_id = data.get('group_id') - album_id = data.get('album_id') - if not group_id or not album_id: - return jsonify({"success": False, "error": "group_id and album_id are required"}), 400 - - with retag_lock: - if retag_state["status"] == "running": - return jsonify({"success": False, "error": "A retag operation is already running"}), 409 - - retag_executor.submit(_execute_retag, group_id, str(album_id)) - return jsonify({"success": True, "message": "Retag operation started"}) - -@app.route('/api/retag/status', methods=['GET']) -def get_retag_status(): - """Get the current retag operation status.""" - with retag_lock: - return jsonify(dict(retag_state)) - -@app.route('/api/retag/groups/', methods=['DELETE']) -def delete_retag_group(group_id): - """Delete a retag group (files are NOT deleted).""" - from database.music_database import get_database - db = get_database() - success = db.delete_retag_group(group_id) - if success: - return jsonify({"success": True}) - else: - return jsonify({"success": False, "error": "Group not found"}), 404 - -@app.route('/api/retag/groups/delete-batch', methods=['POST']) -def delete_retag_groups_batch(): - """Delete multiple retag groups at once.""" - from database.music_database import get_database - data = request.get_json() or {} - group_ids = data.get('group_ids', []) - if not group_ids: - return jsonify({"success": False, "error": "No group IDs provided"}), 400 - db = get_database() - removed = 0 - for gid in group_ids: - if db.delete_retag_group(int(gid)): - removed += 1 - return jsonify({"success": True, "removed": removed}) - -@app.route('/api/retag/groups/clear-all', methods=['POST']) -def clear_all_retag_groups(): - """Delete all retag groups.""" - from database.music_database import get_database - db = get_database() - count = db.delete_all_retag_groups() - return jsonify({"success": True, "removed": count}) - # =============================== # == DOWNLOAD MISSING TRACKS == # =============================== @@ -34968,12 +34806,6 @@ def _emit_tool_progress_loop(): socketio.emit('tool:duplicate-cleaner', state_copy) except Exception as e: logger.debug(f"Error emitting duplicate cleaner status: {e}") - # Retag - try: - with retag_lock: - socketio.emit('tool:retag', dict(retag_state)) - except Exception as e: - logger.debug(f"Error emitting retag status: {e}") # DB Update try: with db_update_lock: diff --git a/webui/index.html b/webui/index.html index f9d3204c..b30f9212 100644 --- a/webui/index.html +++ b/webui/index.html @@ -6708,46 +6708,6 @@ -
-
-

Retag Tool

- -
-

Fix metadata on previously downloaded albums & singles

-
-
- Groups: - 0 -
-
- Tracks: - 0 -
-
- Artists: - 0 -
-
- Status: - Idle -
-
-
- -
-
-

Ready

-
-
-
-
-

0 / 0 tracks (0.0%)

-
-
- - -

Management

@@ -7788,40 +7748,6 @@ -
-
-
-
-

Retag Tool

-
-
- - -
-
- -
-
Loading downloads...
-
-
-
- - -
-
-
-

Search for Correct Album

- -
-
- -
-
-
-
diff --git a/webui/static/core.js b/webui/static/core.js index 48843c3e..44681920 100644 --- a/webui/static/core.js +++ b/webui/static/core.js @@ -487,7 +487,6 @@ function initializeWebSocket() { socket.on('tool:stream', (data) => updateStreamStatusFromData(data)); socket.on('tool:quality-scanner', (data) => updateQualityScanProgressFromData(data)); socket.on('tool:duplicate-cleaner', (data) => updateDuplicateCleanProgressFromData(data)); - socket.on('tool:retag', (data) => updateRetagStatusFromData(data)); socket.on('tool:db-update', (data) => updateDbProgressFromData(data)); socket.on('tool:metadata', (data) => updateMetadataStatusFromData(data)); socket.on('tool:logs', (data) => updateLogsFromData(data)); diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index d2f2db1a..774ab25f 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -5581,42 +5581,6 @@ const TOOL_HELP_CONTENT = {

This tool replicates the same scan process that runs automatically after completing a download modal - ensuring your new tracks are immediately available in your library!

` }, - 'retag-tool': { - title: 'Retag Tool', - content: ` -

What does this tool do?

-

The Retag Tool lets you fix metadata on files that have already been downloaded and processed. If an album was tagged with wrong metadata, you can search for the correct match and re-apply tags.

- -

How it works

-
    -
  • Browse your past downloads organized by artist
  • -
  • Expand an album or single to see individual tracks
  • -
  • Click Retag to search for the correct album match
  • -
  • Select the right album and confirm — metadata and file paths are updated automatically
  • -
- -

What gets updated?

-
    -
  • File tags: Title, artist, album, track number, genre, cover art
  • -
  • File paths: Files are moved/renamed to match new metadata (based on your path template)
  • -
  • Cover art: cover.jpg is updated in the album folder
  • -
- -

Stats Explained

-
    -
  • Groups: Number of album/single download groups tracked
  • -
  • Tracks: Total individual track files tracked
  • -
  • Artists: Number of unique artists across all groups
  • -
- -

Notes

-
    -
  • Only album and single downloads are tracked (not playlists)
  • -
  • Deleting a group from the list does not delete the files
  • -
  • Only one retag operation can run at a time
  • -
- ` - }, 'discover-page': { title: 'Discover Page Guide', content: ` @@ -7448,11 +7412,6 @@ async function initializeToolsPage() { duplicateCleanButton._toolsWired = true; } - const retagOpenButton = document.getElementById('retag-open-button'); - if (retagOpenButton && !retagOpenButton._toolsWired) { - retagOpenButton.addEventListener('click', openRetagModal); - retagOpenButton._toolsWired = true; - } const mediaScanButton = document.getElementById('media-scan-button'); if (mediaScanButton && !mediaScanButton._toolsWired) { @@ -7472,8 +7431,6 @@ async function initializeToolsPage() { await checkAndShowMediaScanForPlex(); loadBackupList(); initializeToolHelpButtons(); - loadRetagStats(); - checkRetagStatus(); await fetchAndUpdateDbStats(); loadDiscoveryPoolStats(); loadMetadataCacheStats();