Files
tidal-dl-ng-webui/app/templates/index.html
2025-12-08 12:11:20 +00:00

315 lines
12 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>
<button onclick="checkIp()" style="background: #444; color: white; border: none; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; margin-left: 10px; cursor: pointer;">Check Again</button>
</small>
</div>
</div>
<script>
async function checkIp() {
const el = document.getElementById('system-ip');
el.textContent = 'Loading...';
try {
const response = await fetch('/system/ip');
const data = await response.json();
el.textContent = data.ip;
} catch (e) {
el.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>