- 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)
This commit is contained in:
parent
b194e1e15b
commit
68b4aace68
3 changed files with 98 additions and 5 deletions
|
|
@ -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…
Reference in a new issue