This repository has been archived on 2025-08-08. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
lucida-queue/frontend/script.js

331 lines
9.5 KiB
JavaScript

let apiBaseUrl = 'http://localhost:3000/api/v1';
const processingList = [];
async function checkConnectivity() {
try {
const response = await fetch(`${apiBaseUrl}/ping`);
if (!response.ok) {
document.getElementById('connectivity-status').classList.remove('hidden');
return;
}
document.getElementById('connectivity-status').classList.add('hidden');
} catch {
document.getElementById('connectivity-status').classList.remove('hidden');
}
}
function setApiUrl() {
const url = document.getElementById('api-url');
if (url === null || url.value === '') {
console.log('Using default API URL');
apiBaseUrl = `http://${window.location.hostname}:3000/api/v1`;
} else {
apiBaseUrl = url.value;
}
}
async function sendAdd(url) {
return fetch(`${apiBaseUrl}/queue`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({song: url})
});
}
async function addToQueue() {
const song = document.getElementById('song-url');
if (!song || !(song instanceof HTMLInputElement)) {
alert('Failed to find song URL input');
return;
}
const url = song.value;
if (!url) {
alert('Please enter a song URL');
return;
}
const response = await sendAdd(url);
if (response.ok) {
loadQueue();
song.value = '';
} else {
const errorMessage = await response.text();
alert(`Failed to add song to queue: ${errorMessage}`);
}
}
async function deleteFromQueue(item) {
const song = encodeURIComponent(item.song.url);
const response = await fetch(`${apiBaseUrl}/queue/${song}`, {
method: 'DELETE'
});
if (response.ok) {
loadQueue();
} else {
const errorMessage = await response.text();
alert(`Failed to delete song from queue: ${errorMessage}`);
}
}
async function retry(item) {
const song = encodeURIComponent(item.song.url);
const response = await fetch(`${apiBaseUrl}/queue/retry/${song}`, {
method: 'POST'
});
if (response.ok) {
loadQueue();
loadHistory();
} else {
const errorMessage = await response.text();
alert(`Failed to retry song: ${errorMessage}`);
}
}
async function clearQueue() {
// Show a confirmation dialog
if (!confirm('Are you sure you want to clear the queue?')) {
return;
}
const response = await fetch(`${apiBaseUrl}/queue`, {
method: 'DELETE'
});
if (response.ok) {
loadQueue();
} else {
const errorMessage = await response.text();
alert(`Failed to clear queue: ${errorMessage}`);
}
}
async function loadQueue() {
const response = await fetch(`${apiBaseUrl}/queue`);
const queue = await response.json();
const queueList = document.getElementById('queue');
if (!queueList) {
alert('Failed to find queue list');
return;
}
queueList.innerHTML = '';
queue.forEach(item => {
const element = constructQueueItem(item);
const deleteButton = document.createElement('button');
deleteButton.classList.add('delete');
deleteButton.textContent = 'x';
deleteButton.addEventListener('click', () => deleteFromQueue(item));
element.prepend(deleteButton);
queueList.appendChild(element);
});
}
async function loadProcessing() {
const response = await fetch(`${apiBaseUrl}/queue/processing`);
const status = await response.json();
const processingDiv = document.getElementById('currently-processing');
if (!processingDiv) {
alert('Failed to find processing status element');
return;
}
if (status.length === 0) {
processingDiv.textContent = 'Nothing processing';
}
status.forEach(item => {
// Add new items if they are not already in the list
if (!processingList.some(element => element.id === item.item.id)) {
const listItem = constructQueueItem(item.item);
const details = document.createElement('details');
details.id = item.item.id;
const summary = document.createElement('summary');
summary.innerHTML = listItem.innerHTML
details.appendChild(summary);
const trackCount = document.createElement('p');
trackCount.innerText = item.item.song.trackCount + ' tracks';
details.appendChild(trackCount);
const progress = document.createElement('p');
progress.classList.add('progress');
progress.textContent = item.status;
details.appendChild(progress);
processingDiv.appendChild(details);
processingList.push(details);
} else {
// Update the progress of existing items
const element = processingList.find(element => element.id === item.item.id);
const progress = element.querySelector('.progress');
progress.innerText = item.status;
}
});
// Remove items that are no longer processing
const toRemove = processingList.filter(element => !status.some(item => item.item.id === element.id));
toRemove.forEach(element => {
console.log('Removing', element);
processingList.splice(processingList.indexOf(element), 1);
element.remove();
// Add the item to the history
const historyList = document.getElementById('history');
if (!historyList) {
alert('Failed to find history list');
return;
}
historyList.prepend(element.querySelector('summary'));
});
}
async function loadHistory() {
const response = await fetch(`${apiBaseUrl}/queue/history`);
const history = await response.json();
const historyList = document.getElementById('history');
if (!historyList) {
alert('Failed to find history list');
return;
}
historyList.innerHTML = '';
history.reverse().forEach(item => {
const element = constructQueueItem(item);
if (!item.result.success) {
const retryButton = document.createElement('button');
retryButton.classList.add('retry');
retryButton.textContent = 'Retry';
retryButton.addEventListener('click', () => retry(item));
element.appendChild(retryButton);
}
historyList.appendChild(element);
})
}
function getSourceIcon(item) {
const sourceToIcon = new Map([
['spotify', 's'],
['qobuz', 'q'],
['unknown', '?']
]);
return sourceToIcon.get(item.song.source) ?? '?';
}
function getState(item) {
const state = document.createElement('span');
state.classList.add('state');
if (!item.result) {
state.textContent = '⌛';
state.title = 'Processing';
} else {
state.textContent = item.result.success ? '✓' : '✗';
state.title = item.result.success ? 'Processed' : 'Failed: ' + item.result.error;
}
return state;
}
function getCoverImage(item) {
const img = document.createElement('img');
img.src = item.song.cover || "./assets/cover-default.png";
img.alt = `${item.song.title} - ${item.song.artist}`;
img.classList.add('cover-art');
return img;
}
function constructQueueItem(item) {
const element = document.createElement('li');
const title = document.createElement('span');
title.classList.add('title');
title.textContent = `${item.song.title} - ${item.song.artist}`;
element.appendChild(title);
const coverImage = getCoverImage(item);
element.prepend(coverImage);
const source = document.createElement('span');
source.classList.add('source');
source.textContent = getSourceIcon(item);
const link = document.createElement('a');
link.href = item.song.url;
link.appendChild(source);
element.appendChild(link);
element.appendChild(getState(item));
return element;
}
function setup() {
const addSongButton = document.getElementById('add-song');
if (!addSongButton) {
alert('Failed to find add song button');
return;
}
addSongButton.addEventListener('click', addToQueue);
const addSongInput = document.getElementById('song-url');
if (!addSongInput) {
alert('Failed to find song URL input');
return;
}
addSongInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
addToQueue();
}
});
addSongInput.addEventListener('drop', (event) => {
event.preventDefault();
addSongInput.value = event.dataTransfer.getData('text');
addToQueue();
});
const refreshQueueButton = document.getElementById('refresh-queue');
if (!refreshQueueButton) {
alert('Failed to find refresh queue button');
return;
}
refreshQueueButton.addEventListener('click', loadQueue);
const clearQueueButton = document.getElementById('clear-queue');
if (!clearQueueButton) {
alert('Failed to find clear queue button');
return;
}
clearQueueButton.addEventListener('click', clearQueue);
const apiUrlButton = document.getElementById('set-api');
if (!apiUrlButton) {
alert('Failed to find set API URL button');
return;
}
apiUrlButton.addEventListener('click', setApiUrl);
document.addEventListener('DOMContentLoaded', () => {
setApiUrl();
loadQueue();
loadHistory();
checkConnectivity();
setInterval(checkConnectivity, 5000);
setInterval(loadProcessing, 5000);
});
}
setup();