From 68b4aace68a09d3bec2c73c5acde8b47941b4c8a Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:56:42 -0700 Subject: [PATCH] Fix track source-info/redownload 404 and add clear-match (#237, #236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change to on 5 library track endpoints — Jellyfin uses GUID strings, int converter rejected them with 404 (#237) - Add PUT /api/library/clear-match endpoint — sets service ID to NULL and match status to not_found, allowing users to undo wrong matches (#236) - Add "Clear Match" button in the manual match modal for all services - Add bottom padding to .page to prevent floating buttons (bell, help) from overlapping track action buttons at page bottom (#237) --- web_server.py | 70 +++++++++++++++++++++++++++++++++++++++--- webui/static/script.js | 32 +++++++++++++++++++ webui/static/style.css | 1 + 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/web_server.py b/web_server.py index 3a1125cf..e32d74d6 100644 --- a/web_server.py +++ b/web_server.py @@ -12857,12 +12857,72 @@ def library_manual_match(): except Exception as e: print(f"❌ Error manual matching: {e}") + +@app.route('/api/library/clear-match', methods=['PUT']) +def library_clear_match(): + """Clear a service ID match for an entity, reverting it to not_found. + Body: { entity_type: str, entity_id: str, service: str } + """ + try: + data = request.get_json() + entity_type = data.get('entity_type') + entity_id = data.get('entity_id') + service = data.get('service') + + if not all([entity_type, entity_id, service]): + return jsonify({"success": False, "error": "entity_type, entity_id, and service are required"}), 400 + + id_col = _SERVICE_ID_COLUMNS.get(service, {}).get(entity_type) + if not id_col: + return jsonify({"success": False, "error": f"Invalid service/entity_type combination"}), 400 + + status_col = f"{service}_match_status" + attempted_col = f"{service}_last_attempted" + table = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}.get(entity_type) + if not table: + return jsonify({"success": False, "error": "Invalid entity_type"}), 400 + + database = get_database() + with database._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(f""" + UPDATE {table} + SET {id_col} = NULL, {status_col} = 'not_found', {attempted_col} = NULL + WHERE id = ? + """, (entity_id,)) + conn.commit() + if cursor.rowcount == 0: + return jsonify({"success": False, "error": "Entity not found"}), 404 + + # Re-fetch fresh data + artist_id = data.get('artist_id', entity_id) + if entity_type != 'artist': + artist_id = _get_artist_id_for_entity(database, entity_type, entity_id) + + updated = database.get_artist_full_detail(artist_id) + if updated.get('success'): + if updated.get('artist', {}).get('thumb_url'): + updated['artist']['thumb_url'] = fix_artist_image_url(updated['artist']['thumb_url']) + for album in updated.get('albums', []): + if album.get('thumb_url'): + album['thumb_url'] = fix_artist_image_url(album['thumb_url']) + + return jsonify({ + "success": True, + "message": f"Cleared {service} match for {entity_type}", + "updated_data": updated if updated.get('success') else None + }) + + except Exception as e: + print(f"❌ Error clearing match: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 -@app.route('/api/library/track/', methods=['DELETE']) +@app.route('/api/library/track/', methods=['DELETE']) def library_delete_track(track_id): """Delete a track from the database, optionally deleting the file and blacklisting the source.""" try: @@ -12974,7 +13034,7 @@ def remove_from_blacklist(blacklist_id): # TRACK SOURCE INFO & PROVENANCE # ================================================================================== -@app.route('/api/library/track//source-info', methods=['GET']) +@app.route('/api/library/track//source-info', methods=['GET']) def get_track_source_info(track_id): """Get download provenance info for a library track.""" try: @@ -13003,7 +13063,7 @@ def get_track_source_info(track_id): # TRACK REDOWNLOAD — Search metadata, search download sources, start redownload # ================================================================================== -@app.route('/api/library/track//redownload/search-metadata', methods=['POST']) +@app.route('/api/library/track//redownload/search-metadata', methods=['POST']) def redownload_search_metadata(track_id): """Search all available metadata sources for a track to find the correct version.""" try: @@ -13159,7 +13219,7 @@ def redownload_search_metadata(track_id): return jsonify({"success": False, "error": str(e)}), 500 -@app.route('/api/library/track//redownload/search-sources', methods=['POST']) +@app.route('/api/library/track//redownload/search-sources', methods=['POST']) def redownload_search_sources(track_id): """Search all active download sources for a track using the selected metadata.""" try: @@ -13292,7 +13352,7 @@ def redownload_search_sources(track_id): return jsonify({"success": False, "error": str(e)}), 500 -@app.route('/api/library/track//redownload/start', methods=['POST']) +@app.route('/api/library/track//redownload/start', methods=['POST']) def redownload_start(track_id): """Start downloading a specific track from a selected source to replace the current file.""" try: diff --git a/webui/static/script.js b/webui/static/script.js index e0f53dc1..f856787b 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -45098,6 +45098,38 @@ function openManualMatchModal(entityType, entityId, service, defaultQuery, artis searchBtn.textContent = 'Search'; searchBtn.onclick = () => doManualMatchSearch(service, entityType, searchInput.value, resultsContainer, entityId, artistId); searchRow.appendChild(searchBtn); + + // Clear Match button — lets user revert a wrong match to not_found + const clearBtn = document.createElement('button'); + clearBtn.className = 'enhanced-enrich-btn'; + clearBtn.style.cssText = 'background:rgba(255,80,80,0.12);color:#ff6b6b;margin-left:6px'; + clearBtn.textContent = 'Clear Match'; + clearBtn.title = 'Remove the current match — reverts to Not Found'; + clearBtn.onclick = async () => { + if (!confirm(`Clear ${serviceLabels[service] || service} match for this ${entityType}? It will revert to "Not Found".`)) return; + try { + const res = await fetch('/api/library/clear-match', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entity_type: entityType, entity_id: entityId, service, artist_id: artistId }) + }); + const data = await res.json(); + if (data.success) { + showToast(`Cleared ${serviceLabels[service] || service} match`, 'success'); + overlay.remove(); + if (data.updated_data) { + artistDetailPageState.enhancedData = data.updated_data; + renderEnhancedArtistView(data.updated_data, true); + } + } else { + showToast(data.error || 'Failed to clear match', 'error'); + } + } catch (e) { + showToast('Error clearing match', 'error'); + } + }; + searchRow.appendChild(clearBtn); + modal.appendChild(searchRow); // Handle Enter key diff --git a/webui/static/style.css b/webui/static/style.css index e5345e49..fa0c1935 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -2741,6 +2741,7 @@ body.helper-mode-active #dashboard-activity-feed:hover { display: none; height: 100%; padding: 40px; + padding-bottom: 90px; } .page.active {