Add web-based admin panel for library management, playlists, and track organization
This commit is contained in:
298
public/app.js
Normal file
298
public/app.js
Normal file
@@ -0,0 +1,298 @@
|
||||
// API Base URL
|
||||
const API_BASE = window.location.origin;
|
||||
|
||||
// Load dashboard stats on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDashboard();
|
||||
updateSubsonicUrl();
|
||||
});
|
||||
|
||||
// Switch between tabs
|
||||
function switchTab(tabName) {
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Load data for the tab
|
||||
if (tabName === 'tracks') loadTracks();
|
||||
if (tabName === 'albums') loadAlbums();
|
||||
if (tabName === 'playlists') loadPlaylists();
|
||||
}
|
||||
|
||||
// Load dashboard statistics
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const [tracksRes, albumsRes, playlistsRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/api/tracks`),
|
||||
fetch(`${API_BASE}/api/albums`),
|
||||
fetch(`${API_BASE}/api/playlists`)
|
||||
]);
|
||||
|
||||
const tracks = await tracksRes.json();
|
||||
const albums = await albumsRes.json();
|
||||
const playlists = await playlistsRes.json();
|
||||
|
||||
const singles = tracks.tracks.filter(t => t.isSingle).length;
|
||||
|
||||
document.getElementById('totalTracks').textContent = tracks.tracks.length;
|
||||
document.getElementById('totalAlbums').textContent = albums.albums.length;
|
||||
document.getElementById('totalSingles').textContent = singles;
|
||||
document.getElementById('totalPlaylists').textContent = playlists.playlists.length;
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load all tracks
|
||||
async function loadTracks() {
|
||||
const trackList = document.getElementById('trackList');
|
||||
trackList.innerHTML = '<li class="loading">Loading tracks...</li>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/tracks`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.tracks.length === 0) {
|
||||
trackList.innerHTML = '<li class="loading">No tracks found. Scan your library first.</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
trackList.innerHTML = data.tracks.map(track => `
|
||||
<li class="track-item">
|
||||
<div class="track-info">
|
||||
<div class="track-title">
|
||||
${track.title}
|
||||
<span class="badge ${track.isSingle ? 'badge-single' : 'badge-album'}">
|
||||
${track.isSingle ? 'Single' : 'Album'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="track-meta">${track.artist} • ${track.album}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-secondary" onclick="toggleSingle('${track.id}', ${!track.isSingle})">
|
||||
Mark as ${track.isSingle ? 'Album' : 'Single'}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
trackList.innerHTML = '<li class="loading">Error loading tracks</li>';
|
||||
console.error('Error loading tracks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Search tracks
|
||||
function searchTracks() {
|
||||
const searchTerm = document.getElementById('searchTracks').value.toLowerCase();
|
||||
const items = document.querySelectorAll('.track-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(searchTerm) ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle single/album status
|
||||
async function toggleSingle(trackId, isSingle) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/tracks/${trackId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isSingle })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadTracks();
|
||||
loadDashboard();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating track:', error);
|
||||
alert('Failed to update track');
|
||||
}
|
||||
}
|
||||
|
||||
// Load albums
|
||||
async function loadAlbums() {
|
||||
const albumList = document.getElementById('albumList');
|
||||
albumList.innerHTML = '<li class="loading">Loading albums...</li>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/albums`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.albums.length === 0) {
|
||||
albumList.innerHTML = '<li class="loading">No albums found</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
albumList.innerHTML = data.albums.map(album => `
|
||||
<li class="track-item">
|
||||
<div class="track-info">
|
||||
<div class="track-title">${album.name}</div>
|
||||
<div class="track-meta">${album.artist} • ${album.trackCount} tracks</div>
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
albumList.innerHTML = '<li class="loading">Error loading albums</li>';
|
||||
console.error('Error loading albums:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load playlists
|
||||
async function loadPlaylists() {
|
||||
const playlistList = document.getElementById('playlistList');
|
||||
playlistList.innerHTML = '<li class="loading">Loading playlists...</li>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/playlists`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.playlists.length === 0) {
|
||||
playlistList.innerHTML = '<li class="loading">No playlists yet. Create one!</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
playlistList.innerHTML = data.playlists.map(playlist => `
|
||||
<li class="playlist-item">
|
||||
<div class="track-info">
|
||||
<div class="track-title">${playlist.name}</div>
|
||||
<div class="track-meta">${playlist.trackCount} tracks</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger" onclick="deletePlaylist('${playlist.id}')">Delete</button>
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
playlistList.innerHTML = '<li class="loading">Error loading playlists</li>';
|
||||
console.error('Error loading playlists:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Show create playlist modal
|
||||
function showCreatePlaylist() {
|
||||
document.getElementById('createPlaylistModal').classList.add('active');
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
document.getElementById('createPlaylistModal').classList.remove('active');
|
||||
document.getElementById('playlistName').value = '';
|
||||
document.getElementById('playlistDescription').value = '';
|
||||
}
|
||||
|
||||
// Create playlist
|
||||
async function createPlaylist() {
|
||||
const name = document.getElementById('playlistName').value;
|
||||
const description = document.getElementById('playlistDescription').value;
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter a playlist name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/playlists`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
closeModal();
|
||||
loadPlaylists();
|
||||
loadDashboard();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating playlist:', error);
|
||||
alert('Failed to create playlist');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete playlist
|
||||
async function deletePlaylist(playlistId) {
|
||||
if (!confirm('Are you sure you want to delete this playlist?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/playlists/${playlistId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadPlaylists();
|
||||
loadDashboard();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting playlist:', error);
|
||||
alert('Failed to delete playlist');
|
||||
}
|
||||
}
|
||||
|
||||
// Scan library
|
||||
async function scanLibrary() {
|
||||
const scanStatus = document.getElementById('scanStatus');
|
||||
scanStatus.style.display = 'block';
|
||||
|
||||
try {
|
||||
// Note: This would need a scan endpoint on the server
|
||||
alert('Library scanning is currently done via CLI: npm run scan\n\nWe can add a web-based scan button if needed!');
|
||||
scanStatus.style.display = 'none';
|
||||
} catch (error) {
|
||||
console.error('Error scanning library:', error);
|
||||
alert('Failed to scan library');
|
||||
scanStatus.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Upload audio file
|
||||
async function uploadAudio() {
|
||||
const fileInput = document.getElementById('uploadFile');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
const isSingle = confirm('Is this a single track? (Click Cancel if it\'s part of an album)');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('audio', file);
|
||||
formData.append('isSingle', isSingle);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('File uploaded successfully!');
|
||||
loadDashboard();
|
||||
if (document.getElementById('tracks').classList.contains('active')) {
|
||||
loadTracks();
|
||||
}
|
||||
} else {
|
||||
alert('Failed to upload file');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
alert('Failed to upload file');
|
||||
}
|
||||
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
// Update Subsonic URL
|
||||
function updateSubsonicUrl() {
|
||||
const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rest`;
|
||||
document.getElementById('subsonicUrl').value = url;
|
||||
}
|
||||
Reference in New Issue
Block a user