(Proper) Initial commit

This commit is contained in:
Tibo De Peuter 2025-01-05 23:56:55 +01:00
parent 48c0059860
commit 32796e4026
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
19 changed files with 6094 additions and 97 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

45
frontend/index.html Normal file
View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Queue Manager</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="connectivity-status" class="hidden">
<p>
<strong>Connection Status:</strong>
<span id="status">Offline</span>
</p>
</div>
<main>
<h1>Queue Manager</h1>
<div id="queue-form">
<label for="song-url">Song URL</label>
<input type="url" id="song-url" placeholder="https://open.spotify.com/track/4PTG3Z6ehGkBFwjybzWkR8?si=7c5064fbbdde4bc2">
<button id="add-song">Add to queue</button>
</div>
<h2>Currently Processing</h2>
<div id="currently-processing"></div>
<div id="queue-list">
<h2>Queue</h2>
<button id="refresh-queue">Refresh Queue</button>
<button id="clear-queue">Clear Queue</button>
<ol id="queue"></ol>
</div>
<div id="queue-history">
<h2>History</h2>
<ul id="history"></ul>
</div>
</main>
<footer>
<div id="api-url-form">
<label for="api-url">API URL</label>
<input type="url" id="api-url" placeholder="http://localhost:3000">
<button type="submit" id="set-api">Connect</button>
</div>
</footer>
<script type="module" src="script.js"></script>
</body>
</html>

331
frontend/script.js Normal file
View file

@ -0,0 +1,331 @@
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://127.0.0.1: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();

96
frontend/style.css Normal file
View file

@ -0,0 +1,96 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background: #f0f0f0;
}
footer {
background-color: white;
padding: 5px;
border-bottom: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
flex-shrink: 0;
border-top: 1px solid #ccc;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
main {
flex: 1;
overflow-y: auto;
background-color: #ffffff;
padding: 20px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 80%;
max-width: 1200px;
}
h1, h2 {
margin-top: 0;
}
#queue-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#queue-form input {
flex: 1;
}
#queue-form button {
padding: 5px 10px;
}
#queue-list, #queue-history {
margin-top: 20px;
}
#queue-list ol, #queue-history ul {
padding-left: 20px;
}
#queue-list li, #queue-history li {
margin-bottom: 10px;
align-items: center;
}
#connectivity-status {
position: fixed;
top: 0;
width: 100%;
background: red;
color: white;
text-align: center;
padding: 5px;
z-index: 1000;
}
.hidden {
display: none;
}
.cover-art {
width: 50px;
height: 50px;
object-fit: cover;
margin-right: 10px;
}
.delete, .source, .state, .retry {
float: right;
margin-left: 10px;
margin-right: 0;
}