311 lines
11 KiB
HTML
311 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tidal DL Web</title>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
|
|
<body>
|
|
<div class="container">
|
|
<h1>Tidal DL Web</h1>
|
|
|
|
<div class="card">
|
|
<h2>Search</h2>
|
|
<input type="text" id="query" placeholder="Search for artists, albums..." onkeypress="handleEnter(event)">
|
|
<select id="type"
|
|
style="padding: 10px; background: #2d2d2d; color: white; border: 1px solid #333; border-radius: 4px;">
|
|
<option value="artist">Artist</option>
|
|
<option value="album">Album</option>
|
|
</select>
|
|
<button onclick="search()">Search</button>
|
|
</div>
|
|
|
|
<div id="results" class="card" style="display:none;">
|
|
<h2>Results</h2>
|
|
<div id="results-list"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Download Queue</h2>
|
|
<div id="queue-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Album Selection Modal -->
|
|
<div id="album-modal" class="modal">
|
|
<div class="modal-content">
|
|
<h3 id="modal-artist-name">Albums</h3>
|
|
<div id="album-grid" class="album-grid"></div>
|
|
<div class="modal-buttons">
|
|
<button onclick="downloadSelectedAlbums()" style="background-color: #1db954;">Download Selected</button>
|
|
<button onclick="downloadAllAlbums()" style="background-color: #0d7a3a;">Download All</button>
|
|
<button onclick="closeAlbumModal()" style="background-color: #666;">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="card" style="margin-top: 20px; text-align: center; color: #888;">
|
|
<small>System IP: <span id="system-ip">Loading...</span></small>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function checkIp() {
|
|
try {
|
|
const response = await fetch('/system/ip');
|
|
const data = await response.json();
|
|
document.getElementById('system-ip').textContent = data.ip;
|
|
} catch (e) {
|
|
document.getElementById('system-ip').textContent = 'Error';
|
|
}
|
|
}
|
|
checkIp();
|
|
|
|
function handleEnter(e) {
|
|
if (e.key === 'Enter') search();
|
|
}
|
|
|
|
async function search() {
|
|
const query = document.getElementById('query').value;
|
|
const type = document.getElementById('type').value;
|
|
if (!query) return;
|
|
|
|
const response = await fetch(`/search/?query=${encodeURIComponent(query)}&type=${type}`);
|
|
const results = await response.json();
|
|
|
|
const list = document.getElementById('results-list');
|
|
list.innerHTML = '';
|
|
document.getElementById('results').style.display = 'block';
|
|
|
|
if (results.length === 0) {
|
|
list.innerHTML = '<p>No results found.</p>';
|
|
return;
|
|
}
|
|
|
|
// Grouping Logic
|
|
let grouped = {};
|
|
let groupKey = '';
|
|
|
|
if (type === 'track') {
|
|
groupKey = 'album'; // Group tracks by Album
|
|
} else if (type === 'album') {
|
|
groupKey = 'artist'; // Group albums by Artist
|
|
} else {
|
|
// No grouping for artist/playlist
|
|
renderFlatList(results, list);
|
|
return;
|
|
}
|
|
|
|
results.forEach(item => {
|
|
const key = item[groupKey] || 'Unknown';
|
|
if (!grouped[key]) grouped[key] = [];
|
|
grouped[key].push(item);
|
|
});
|
|
|
|
for (const [groupName, items] of Object.entries(grouped)) {
|
|
const details = document.createElement('details');
|
|
details.open = true; // Open by default
|
|
|
|
const summary = document.createElement('summary');
|
|
summary.textContent = `${groupName} (${items.length})`;
|
|
details.appendChild(summary);
|
|
|
|
const groupList = document.createElement('div');
|
|
groupList.className = 'group-list';
|
|
|
|
items.forEach(item => {
|
|
const div = createResultItem(item);
|
|
groupList.appendChild(div);
|
|
});
|
|
|
|
details.appendChild(groupList);
|
|
list.appendChild(details);
|
|
}
|
|
}
|
|
|
|
function renderFlatList(items, container) {
|
|
items.forEach(item => {
|
|
const div = createResultItem(item);
|
|
container.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function createResultItem(item) {
|
|
const div = document.createElement('div');
|
|
div.className = 'result-item';
|
|
|
|
// For artists, show album selection modal instead of direct download
|
|
const buttonHtml = item.type === 'artist'
|
|
? `<button onclick="showArtistAlbums('${item.id}', '${item.title.replace(/'/g, "\\'")}')">View Albums</button>`
|
|
: `<button onclick="download('${item.type}', '${item.id}')">Download</button>`;
|
|
|
|
div.innerHTML = `
|
|
<img src="${item.cover || '/static/placeholder.png'}" alt="Cover">
|
|
<div class="result-info">
|
|
<strong>${item.title}</strong><br>
|
|
${item.artist ? item.artist + ' - ' : ''}${item.album || ''}
|
|
</div>
|
|
${buttonHtml}
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
async function download(type, id) {
|
|
await fetch('/download/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type, id })
|
|
});
|
|
updateQueue();
|
|
}
|
|
|
|
async function updateQueue() {
|
|
const response = await fetch('/download/queue');
|
|
const queue = await response.json();
|
|
|
|
const list = document.getElementById('queue-list');
|
|
list.innerHTML = '';
|
|
|
|
queue.forEach(item => {
|
|
const div = document.createElement('div');
|
|
div.className = 'queue-item';
|
|
let controls = '';
|
|
if (item.status === 'downloading' || item.status === 'queued') {
|
|
if (item.control === 'pause') {
|
|
controls += `<button onclick="controlTask('${item.id}', 'resume')">Resume</button>`;
|
|
} else {
|
|
controls += `<button onclick="controlTask('${item.id}', 'pause')">Pause</button>`;
|
|
}
|
|
controls += `<button onclick="controlTask('${item.id}', 'cancel')" style="background-color: #ff4444;">Cancel</button>`;
|
|
}
|
|
|
|
div.innerHTML = `
|
|
<div>
|
|
<strong>${item.name}</strong><br>
|
|
${item.type}
|
|
${item.current_item ? '<br><small>' + item.current_item + '</small>' : ''}
|
|
${item.total_items ? '<br><small>Track ' + item.current_index + ' of ' + item.total_items + '</small>' : ''}
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<div class="status-${item.status}">
|
|
${item.status} ${item.progress ? Math.round(item.progress) + '%' : ''}
|
|
</div>
|
|
<div class="controls">
|
|
${controls}
|
|
</div>
|
|
</div>
|
|
`;
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
|
|
async function controlTask(id, action) {
|
|
await fetch(`/download/${action}/${id}`, { method: 'POST' });
|
|
updateQueue();
|
|
}
|
|
|
|
// Album Selection Modal Functions
|
|
let currentAlbums = [];
|
|
let selectedAlbumIds = new Set();
|
|
|
|
async function showArtistAlbums(artistId, artistName) {
|
|
try {
|
|
const response = await fetch(`/search/artist/${artistId}/albums`);
|
|
currentAlbums = await response.json();
|
|
|
|
document.getElementById('modal-artist-name').textContent = `Albums by ${artistName}`;
|
|
|
|
const grid = document.getElementById('album-grid');
|
|
grid.innerHTML = '';
|
|
selectedAlbumIds.clear();
|
|
|
|
if (currentAlbums.length === 0) {
|
|
grid.innerHTML = '<p>No albums found.</p>';
|
|
} else {
|
|
currentAlbums.forEach(album => {
|
|
const albumDiv = document.createElement('div');
|
|
albumDiv.className = 'album-item';
|
|
albumDiv.dataset.albumId = album.id;
|
|
albumDiv.onclick = () => toggleAlbumSelection(album.id);
|
|
|
|
albumDiv.innerHTML = `
|
|
<img src="${album.cover || '/static/placeholder.png'}" alt="${album.title}">
|
|
<div class="album-item-info">
|
|
<strong>${album.title}</strong>
|
|
<small>${album.tracks} tracks • ${album.release_date}</small>
|
|
</div>
|
|
`;
|
|
|
|
grid.appendChild(albumDiv);
|
|
});
|
|
}
|
|
|
|
document.getElementById('album-modal').style.display = 'block';
|
|
} catch (e) {
|
|
alert('Failed to load albums: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function toggleAlbumSelection(albumId) {
|
|
const albumDiv = document.querySelector(`[data-album-id="${albumId}"]`);
|
|
|
|
if (selectedAlbumIds.has(albumId)) {
|
|
selectedAlbumIds.delete(albumId);
|
|
albumDiv.classList.remove('selected');
|
|
} else {
|
|
selectedAlbumIds.add(albumId);
|
|
albumDiv.classList.add('selected');
|
|
}
|
|
}
|
|
|
|
async function downloadSelectedAlbums() {
|
|
if (selectedAlbumIds.size === 0) {
|
|
alert('Please select at least one album to download.');
|
|
return;
|
|
}
|
|
|
|
for (const albumId of selectedAlbumIds) {
|
|
await download('album', albumId);
|
|
}
|
|
|
|
closeAlbumModal();
|
|
}
|
|
|
|
async function downloadAllAlbums() {
|
|
if (currentAlbums.length === 0) {
|
|
alert('No albums to download.');
|
|
return;
|
|
}
|
|
|
|
for (const album of currentAlbums) {
|
|
await download('album', album.id);
|
|
}
|
|
|
|
closeAlbumModal();
|
|
}
|
|
|
|
function closeAlbumModal() {
|
|
document.getElementById('album-modal').style.display = 'none';
|
|
selectedAlbumIds.clear();
|
|
currentAlbums = [];
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
window.onclick = function (event) {
|
|
const modal = document.getElementById('album-modal');
|
|
if (event.target === modal) {
|
|
closeAlbumModal();
|
|
}
|
|
}
|
|
|
|
// Poll queue
|
|
setInterval(updateQueue, 2000);
|
|
updateQueue();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |