Fix track source-info/redownload 404 and add clear-match (#237, #236)

- Change <int:track_id> to <track_id> 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)
pull/253/head
Broque Thomas 1 month ago
parent b194e1e15b
commit 68b4aace68

@ -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/<int:track_id>', methods=['DELETE'])
@app.route('/api/library/track/<track_id>', 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/<int:track_id>/source-info', methods=['GET'])
@app.route('/api/library/track/<track_id>/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/<int:track_id>/redownload/search-metadata', methods=['POST'])
@app.route('/api/library/track/<track_id>/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/<int:track_id>/redownload/search-sources', methods=['POST'])
@app.route('/api/library/track/<track_id>/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/<int:track_id>/redownload/start', methods=['POST'])
@app.route('/api/library/track/<track_id>/redownload/start', methods=['POST'])
def redownload_start(track_id):
"""Start downloading a specific track from a selected source to replace the current file."""
try:

@ -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

@ -2741,6 +2741,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
display: none;
height: 100%;
padding: 40px;
padding-bottom: 90px;
}
.page.active {

Loading…
Cancel
Save