progress
This commit is contained in:
parent
b9879c4375
commit
b84a3ae108
3 changed files with 263 additions and 46 deletions
109
web_server.py
109
web_server.py
|
|
@ -874,6 +874,15 @@ def start_download():
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_completed_file_robust(download_dir, api_filename):
|
def _find_completed_file_robust(download_dir, api_filename):
|
||||||
"""
|
"""
|
||||||
Robustly finds a completed file on disk, accounting for name variations and
|
Robustly finds a completed file on disk, accounting for name variations and
|
||||||
|
|
@ -929,6 +938,8 @@ def _find_completed_file_robust(download_dir, api_filename):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/downloads/status')
|
@app.route('/api/downloads/status')
|
||||||
def get_download_status():
|
def get_download_status():
|
||||||
"""
|
"""
|
||||||
|
|
@ -1021,6 +1032,7 @@ def get_download_status():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/downloads/cancel', methods=['POST'])
|
@app.route('/api/downloads/cancel', methods=['POST'])
|
||||||
def cancel_download():
|
def cancel_download():
|
||||||
"""
|
"""
|
||||||
|
|
@ -1780,6 +1792,8 @@ def _search_track_in_album_context(original_search: dict, artist: dict) -> dict:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_album_info_web(context: dict, artist: dict) -> dict:
|
def _detect_album_info_web(context: dict, artist: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
This is the final, corrected version that ensures the official Spotify track
|
This is the final, corrected version that ensures the official Spotify track
|
||||||
|
|
@ -2022,6 +2036,8 @@ def _download_cover_art(album_info: dict, target_dir: str):
|
||||||
print(f"❌ Error downloading cover.jpg: {e}")
|
print(f"❌ Error downloading cover.jpg: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_spotify_album_tracks(spotify_album: dict) -> list:
|
def _get_spotify_album_tracks(spotify_album: dict) -> list:
|
||||||
"""Fetches all tracks for a given Spotify album ID."""
|
"""Fetches all tracks for a given Spotify album ID."""
|
||||||
if not spotify_album or not spotify_album.get('id'):
|
if not spotify_album or not spotify_album.get('id'):
|
||||||
|
|
@ -2086,6 +2102,7 @@ def _match_track_to_spotify_title(slsk_track_meta: dict, spotify_tracks: list) -
|
||||||
return slsk_track_meta # Fallback to original
|
return slsk_track_meta # Fallback to original
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Post-Processing Logic ---
|
# --- Post-Processing Logic ---
|
||||||
def _post_process_matched_download(context_key, context, file_path):
|
def _post_process_matched_download(context_key, context, file_path):
|
||||||
"""
|
"""
|
||||||
|
|
@ -2201,9 +2218,6 @@ def _post_process_matched_download(context_key, context, file_path):
|
||||||
matched_downloads_context[context_key] = context
|
matched_downloads_context[context_key] = context
|
||||||
print(f"♻️ Re-added {context_key} to context for retry")
|
print(f"♻️ Re-added {context_key} to context for retry")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Keep track of processed downloads to avoid re-processing
|
# Keep track of processed downloads to avoid re-processing
|
||||||
_processed_download_ids = set()
|
_processed_download_ids = set()
|
||||||
|
|
||||||
|
|
@ -2771,31 +2785,90 @@ def start_playlist_missing_downloads(playlist_id):
|
||||||
@app.route('/api/playlists/<batch_id>/download_status', methods=['GET'])
|
@app.route('/api/playlists/<batch_id>/download_status', methods=['GET'])
|
||||||
def get_batch_download_status(batch_id):
|
def get_batch_download_status(batch_id):
|
||||||
"""
|
"""
|
||||||
This endpoint returns real-time status for all tasks in a batch,
|
Returns real-time status for all tasks in a batch. This version correctly
|
||||||
enabling live progress tracking in the modal.
|
parses the live 'state' from slskd to report 'completed' status, fixing
|
||||||
|
the UI issue where downloads would get stuck at 100%.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# --- Fetch live transfer data from slskd ---
|
||||||
|
live_transfers_lookup = {}
|
||||||
|
try:
|
||||||
|
transfers_data = asyncio.run(soulseek_client._make_request('GET', 'transfers/downloads'))
|
||||||
|
if transfers_data:
|
||||||
|
all_transfers = []
|
||||||
|
for user_data in transfers_data:
|
||||||
|
username = user_data.get('username', 'Unknown')
|
||||||
|
if 'directories' in user_data:
|
||||||
|
for directory in user_data['directories']:
|
||||||
|
if 'files' in directory:
|
||||||
|
for file_info in directory['files']:
|
||||||
|
file_info['username'] = username
|
||||||
|
all_transfers.append(file_info)
|
||||||
|
|
||||||
|
# Create a lookup dictionary using a reliable composite key
|
||||||
|
for transfer in all_transfers:
|
||||||
|
key = f"{transfer.get('username')}::{os.path.basename(transfer.get('filename', ''))}"
|
||||||
|
live_transfers_lookup[key] = transfer
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Could not fetch live transfers for modal status: {e}")
|
||||||
|
|
||||||
|
# --- Process tasks and enrich with live data ---
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
batch_tasks = []
|
batch_tasks = []
|
||||||
for task_id, task in download_tasks.items():
|
if batch_id not in download_batches:
|
||||||
if task.get('batch_id') == batch_id:
|
return jsonify({"tasks": []})
|
||||||
task_status = {
|
|
||||||
'task_id': task_id,
|
for task_id in download_batches[batch_id].get('queue', []):
|
||||||
'track_index': task['track_index'],
|
task = download_tasks.get(task_id)
|
||||||
'status': task['status'],
|
if not task:
|
||||||
'track_info': task['track_info'],
|
continue
|
||||||
'download_id': task.get('download_id'),
|
|
||||||
'username': task.get('username')
|
task_status = {
|
||||||
}
|
'task_id': task_id,
|
||||||
batch_tasks.append(task_status)
|
'track_index': task['track_index'],
|
||||||
print(f"🔧 [Status API] Task {task_id} track_index {task['track_index']} status: {task['status']}")
|
'status': task['status'],
|
||||||
|
'track_info': task['track_info'],
|
||||||
|
'progress': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- USE THE RELIABLE KEY FOR MATCHING ---
|
||||||
|
task_filename = task.get('filename') or task['track_info'].get('filename')
|
||||||
|
task_username = task.get('username') or task['track_info'].get('username')
|
||||||
|
|
||||||
|
if task_filename and task_username:
|
||||||
|
lookup_key = f"{task_username}::{os.path.basename(task_filename)}"
|
||||||
|
|
||||||
|
if lookup_key in live_transfers_lookup:
|
||||||
|
live_info = live_transfers_lookup[lookup_key]
|
||||||
|
|
||||||
|
# --- THIS IS THE KEY FIX ---
|
||||||
|
# Correctly parse the live state string from the API
|
||||||
|
state_str = live_info.get('state', 'Unknown')
|
||||||
|
|
||||||
|
if 'Completed' in state_str or 'Succeeded' in state_str:
|
||||||
|
task_status['status'] = 'completed'
|
||||||
|
elif 'Cancelled' in state_str or 'Canceled' in state_str:
|
||||||
|
task_status['status'] = 'cancelled'
|
||||||
|
elif 'Failed' in state_str or 'Errored' in state_str:
|
||||||
|
task_status['status'] = 'failed'
|
||||||
|
elif 'InProgress' in state_str:
|
||||||
|
task_status['status'] = 'downloading'
|
||||||
|
else:
|
||||||
|
task_status['status'] = 'queued'
|
||||||
|
# --- END OF FIX ---
|
||||||
|
|
||||||
|
task_status['progress'] = live_info.get('percentComplete', 0)
|
||||||
|
print(f"🔧 [Status API] Live Update for Task {task_id}: Status '{task_status['status']}', Progress {task_status['progress']}%")
|
||||||
|
|
||||||
|
batch_tasks.append(task_status)
|
||||||
|
|
||||||
# Sort by track_index to maintain order
|
|
||||||
batch_tasks.sort(key=lambda x: x['track_index'])
|
batch_tasks.sort(key=lambda x: x['track_index'])
|
||||||
|
|
||||||
return jsonify({"tasks": batch_tasks})
|
return jsonify({"tasks": batch_tasks})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
print(f"❌ Error getting batch status: {e}")
|
print(f"❌ Error getting batch status: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2018,23 +2018,20 @@ function startModalDownloadPolling() {
|
||||||
let failedCount = 0;
|
let failedCount = 0;
|
||||||
let totalTasks = tasks.length;
|
let totalTasks = tasks.length;
|
||||||
|
|
||||||
// Update each track's status - but only for missing tracks
|
// Update each track's status in the modal
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
const trackIndex = task.track_index;
|
const trackIndex = task.track_index;
|
||||||
const row = document.querySelector(`tr[data-track-index="${trackIndex}"]`);
|
const row = document.querySelector(`tr[data-track-index="${trackIndex}"]`);
|
||||||
|
|
||||||
// Only update if this is a missing track (has a matching entry in missingTracks array)
|
|
||||||
const isMissingTrack = missingTracks.some(mt => mt.track_index === trackIndex);
|
const isMissingTrack = missingTracks.some(mt => mt.track_index === trackIndex);
|
||||||
|
|
||||||
|
|
||||||
if (row && isMissingTrack) {
|
if (row && isMissingTrack) {
|
||||||
const statusElement = row.querySelector('.track-download-status');
|
const statusElement = row.querySelector('.track-download-status');
|
||||||
const actionsElement = row.querySelector('.track-actions');
|
const actionsElement = row.querySelector('.track-actions');
|
||||||
|
|
||||||
// Store task ID for cancellation
|
|
||||||
row.dataset.taskId = task.task_id;
|
row.dataset.taskId = task.task_id;
|
||||||
|
|
||||||
const status = task.status;
|
const status = task.status;
|
||||||
|
const progress = task.progress || 0; // Get live progress from backend
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
|
|
@ -2048,15 +2045,9 @@ function startModalDownloadPolling() {
|
||||||
actionsElement.innerHTML = `<button class="cancel-track-btn" onclick="cancelTrackDownload(${trackIndex})">Cancel</button>`;
|
actionsElement.innerHTML = `<button class="cancel-track-btn" onclick="cancelTrackDownload(${trackIndex})">Cancel</button>`;
|
||||||
break;
|
break;
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
statusElement.textContent = '⏬ Downloading...';
|
statusElement.textContent = `⏬ Downloading... ${Math.round(progress)}%`;
|
||||||
statusElement.className = 'track-download-status download-downloading';
|
statusElement.className = 'track-download-status download-downloading';
|
||||||
actionsElement.innerHTML = `<button class="cancel-track-btn" onclick="cancelTrackDownload(${trackIndex})">Cancel</button>`;
|
actionsElement.innerHTML = `<button class="cancel-track-btn" onclick="cancelTrackDownload(${trackIndex})">Cancel</button>`;
|
||||||
|
|
||||||
// Start live download polling when we detect actual downloads have begun
|
|
||||||
if (!isDownloadPollingActive) {
|
|
||||||
console.log('🔄 Download detected - starting live download polling integration');
|
|
||||||
startDownloadPolling();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
statusElement.textContent = '✅ Completed';
|
statusElement.textContent = '✅ Completed';
|
||||||
|
|
@ -2084,13 +2075,11 @@ function startModalDownloadPolling() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update progress
|
// Update progress bar and stats
|
||||||
const progressPercent = totalTasks > 0 ? ((completedCount + failedCount) / totalTasks) * 100 : 0;
|
const progressPercent = totalTasks > 0 ? ((completedCount + failedCount) / totalTasks) * 100 : 0;
|
||||||
document.getElementById('download-progress-fill').style.width = `${progressPercent}%`;
|
document.getElementById('download-progress-fill').style.width = `${progressPercent}%`;
|
||||||
document.getElementById('download-progress-text').textContent =
|
document.getElementById('download-progress-text').textContent =
|
||||||
`${completedCount}/${totalTasks} completed (${progressPercent.toFixed(0)}%)`;
|
`${completedCount}/${totalTasks} completed (${progressPercent.toFixed(0)}%)`;
|
||||||
|
|
||||||
// Update downloaded count
|
|
||||||
document.getElementById('stat-downloaded').textContent = completedCount;
|
document.getElementById('stat-downloaded').textContent = completedCount;
|
||||||
|
|
||||||
// Stop polling when all tasks are complete
|
// Stop polling when all tasks are complete
|
||||||
|
|
@ -2099,26 +2088,16 @@ function startModalDownloadPolling() {
|
||||||
modalDownloadPoller = null;
|
modalDownloadPoller = null;
|
||||||
document.getElementById('cancel-all-btn').style.display = 'none';
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
console.log('✅ All download tasks completed, stopping polling');
|
console.log('✅ All download tasks completed, stopping polling');
|
||||||
|
|
||||||
// Also stop live download polling if we started it
|
|
||||||
if (isDownloadPollingActive) {
|
|
||||||
stopDownloadPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completedCount > 0) {
|
if (completedCount > 0) {
|
||||||
showToast(`Download completed: ${completedCount} tracks downloaded successfully!`, 'success');
|
showToast(`Download completed: ${completedCount} tracks downloaded successfully!`, 'success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modal tracks with live download progress from the actual download queue
|
|
||||||
updateModalWithLiveDownloadProgress();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error polling download status:', error);
|
console.error('Error polling download status:', error);
|
||||||
}
|
}
|
||||||
}, 2000); // Poll every 2 seconds
|
}, 2000); // Poll every 2 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateModalWithLiveDownloadProgress() {
|
async function updateModalWithLiveDownloadProgress() {
|
||||||
try {
|
try {
|
||||||
if (!currentDownloadBatchId) return;
|
if (!currentDownloadBatchId) return;
|
||||||
|
|
@ -3767,6 +3746,165 @@ function applyFiltersAndSort() {
|
||||||
displayDownloadsResults(processedResults);
|
displayDownloadsResults(processedResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateRelevanceScore(result, query) {
|
||||||
|
let score = 0.0;
|
||||||
|
const queryTerms = query.split(' ').filter(t => t.length > 1);
|
||||||
|
|
||||||
|
// 1. Search Term Matching (40%)
|
||||||
|
let searchableText = `${result.title || ''} ${result.artist || ''} ${result.album || ''} ${result.album_title || ''}`.toLowerCase();
|
||||||
|
let termMatches = 0;
|
||||||
|
for (const term of queryTerms) {
|
||||||
|
if (searchableText.includes(term)) {
|
||||||
|
termMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += (termMatches / queryTerms.length) * 0.40;
|
||||||
|
|
||||||
|
// 2. Quality Score (25%)
|
||||||
|
score += (result.quality_score || 0) * 0.25;
|
||||||
|
|
||||||
|
// 3. User Reliability (Availability & Speed) (20%)
|
||||||
|
const reliability = ((result.free_upload_slots || 0) > 0 ? 0.5 : 0) + Math.min(1, (result.upload_speed || 0) / 500) * 0.5;
|
||||||
|
score += reliability * 0.20;
|
||||||
|
|
||||||
|
// 4. File Completeness (Bitrate & Duration) (15%)
|
||||||
|
const completeness = (Math.min(1, (result.bitrate || 0) / 320) * 0.5) + (result.duration > 0 ? 0.5 : 0);
|
||||||
|
score += completeness * 0.15;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
// APPEND THIS JAVASCRIPT SNIPPET (B)
|
||||||
|
|
||||||
|
function initializeFilters() {
|
||||||
|
const toggleBtn = document.getElementById('filter-toggle-btn');
|
||||||
|
const container = document.getElementById('filters-container');
|
||||||
|
const content = document.getElementById('filter-content');
|
||||||
|
|
||||||
|
if (toggleBtn && container && content) {
|
||||||
|
// Using .onclick ensures we only ever have one click handler
|
||||||
|
toggleBtn.onclick = () => {
|
||||||
|
const isExpanded = container.classList.contains('expanded');
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
// Collapse the container
|
||||||
|
container.classList.remove('expanded');
|
||||||
|
toggleBtn.textContent = '⏷ Filters';
|
||||||
|
} else {
|
||||||
|
// Expand the container
|
||||||
|
content.classList.remove('hidden'); // Make sure content is visible for animation
|
||||||
|
container.classList.add('expanded');
|
||||||
|
toggleBtn.textContent = '⏶ Filters';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This part is correct and doesn't need to change
|
||||||
|
document.querySelectorAll('.filter-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', handleFilterClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterClick(event) {
|
||||||
|
const button = event.target;
|
||||||
|
const filterType = button.dataset.filterType;
|
||||||
|
const value = button.dataset.value;
|
||||||
|
|
||||||
|
if (filterType === 'type') currentFilterType = value;
|
||||||
|
if (filterType === 'format') currentFilterFormat = value;
|
||||||
|
if (filterType === 'sort') currentSortBy = value;
|
||||||
|
|
||||||
|
if (button.id === 'sort-order-btn') {
|
||||||
|
isSortReversed = !isSortReversed;
|
||||||
|
button.textContent = isSortReversed ? '↑' : '↓';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(`.filter-btn[data-filter-type="${filterType}"]`).forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
if (filterType) { // Don't try to activate the sort order button
|
||||||
|
button.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFiltersAndSort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
currentFilterType = 'all';
|
||||||
|
currentFilterFormat = 'all';
|
||||||
|
currentSortBy = 'quality_score';
|
||||||
|
isSortReversed = false;
|
||||||
|
|
||||||
|
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.querySelector('.filter-btn[data-filter-type="type"][data-value="all"]').classList.add('active');
|
||||||
|
document.querySelector('.filter-btn[data-filter-type="format"][data-value="all"]').classList.add('active');
|
||||||
|
document.querySelector('.filter-btn[data-filter-type="sort"][data-value="quality_score"]').classList.add('active');
|
||||||
|
document.getElementById('sort-order-btn').textContent = '↓';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFiltersAndSort() {
|
||||||
|
let processedResults = [...allSearchResults];
|
||||||
|
const query = document.getElementById('downloads-search-input').value.trim().toLowerCase();
|
||||||
|
|
||||||
|
// 1. Filter by Type
|
||||||
|
if (currentFilterType !== 'all') {
|
||||||
|
processedResults = processedResults.filter(r => r.result_type === currentFilterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Filter by Format
|
||||||
|
if (currentFilterFormat !== 'all') {
|
||||||
|
processedResults = processedResults.filter(r => {
|
||||||
|
const quality = (r.dominant_quality || r.quality || '').toLowerCase();
|
||||||
|
return quality === currentFilterFormat;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sort Results
|
||||||
|
processedResults.sort((a, b) => {
|
||||||
|
let valA, valB;
|
||||||
|
|
||||||
|
// Special handling for relevance sort
|
||||||
|
if (currentSortBy === 'relevance') {
|
||||||
|
valA = calculateRelevanceScore(a, query);
|
||||||
|
valB = calculateRelevanceScore(b, query);
|
||||||
|
return valB - valA; // Higher score is better
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for availability
|
||||||
|
if (currentSortBy === 'availability') {
|
||||||
|
valA = (a.free_upload_slots || 0) - (a.queue_length || 0) * 0.1;
|
||||||
|
valB = (b.free_upload_slots || 0) - (b.queue_length || 0) * 0.1;
|
||||||
|
return valB - valA;
|
||||||
|
}
|
||||||
|
|
||||||
|
valA = a[currentSortBy] || 0;
|
||||||
|
valB = b[currentSortBy] || 0;
|
||||||
|
|
||||||
|
if (typeof valA === 'string') {
|
||||||
|
// For name/title sort, use the correct property
|
||||||
|
const titleA = (a.album_title || a.title || '').toLowerCase();
|
||||||
|
const titleB = (b.album_title || b.title || '').toLowerCase();
|
||||||
|
return titleA.localeCompare(titleB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default numeric sort (descending)
|
||||||
|
return valB - valA;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle sort direction toggle
|
||||||
|
const sortDefaults = {
|
||||||
|
relevance: 'desc', quality_score: 'desc', size: 'desc', bitrate: 'desc',
|
||||||
|
upload_speed: 'desc', duration: 'desc', availability: 'desc',
|
||||||
|
title: 'asc', username: 'asc'
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOrder = sortDefaults[currentSortBy] || 'desc';
|
||||||
|
if ((defaultOrder === 'asc' && isSortReversed) || (defaultOrder === 'desc' && !isSortReversed)) {
|
||||||
|
processedResults.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayDownloadsResults(processedResults);
|
||||||
|
}
|
||||||
|
|
||||||
function calculateRelevanceScore(result, query) {
|
function calculateRelevanceScore(result, query) {
|
||||||
let score = 0.0;
|
let score = 0.0;
|
||||||
const queryTerms = query.split(' ').filter(t => t.length > 1);
|
const queryTerms = query.split(' ').filter(t => t.length > 1);
|
||||||
|
|
@ -4331,6 +4469,8 @@ async function confirmMatch() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function matchedDownloadTrack(trackIndex) {
|
function matchedDownloadTrack(trackIndex) {
|
||||||
const results = window.currentSearchResults;
|
const results = window.currentSearchResults;
|
||||||
if (!results || !results[trackIndex]) {
|
if (!results || !results[trackIndex]) {
|
||||||
|
|
|
||||||
|
|
@ -4827,6 +4827,10 @@ body {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-result-card .track-actions{
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.cancel-track-btn {
|
.cancel-track-btn {
|
||||||
background: #f44336;
|
background: #f44336;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue