205 lines
6.2 KiB
JavaScript
205 lines
6.2 KiB
JavaScript
import { GreyscaleAPI } from './api.js';
|
|
import { SimplePlayer } from './player.js';
|
|
|
|
function formatDuration(seconds) {
|
|
if (!Number.isFinite(seconds) || seconds < 0) return '0:00';
|
|
const m = Math.floor(seconds / 60);
|
|
const s = Math.floor(seconds % 60);
|
|
return `${m}:${String(s).padStart(2, '0')}`;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, '\u0026amp;')
|
|
.replace(/</g, '\u0026lt;')
|
|
.replace(/>/g, '\u0026gt;')
|
|
.replace(/"/g, '\u0026quot;')
|
|
.replace(/\u0027/g, '\u0026#39;');
|
|
}
|
|
|
|
const els = {
|
|
form: document.getElementById('search-form'),
|
|
input: document.getElementById('search-input'),
|
|
button: document.getElementById('search-button'),
|
|
status: document.getElementById('status-text'),
|
|
results: document.getElementById('results-list'),
|
|
audio: document.getElementById('audio-player'),
|
|
playPauseBtn: document.getElementById('play-pause-btn'),
|
|
prevBtn: document.getElementById('prev-btn'),
|
|
nextBtn: document.getElementById('next-btn'),
|
|
playerCover: document.getElementById('player-cover'),
|
|
playerTitle: document.getElementById('player-title'),
|
|
playerArtist: document.getElementById('player-artist'),
|
|
currentTime: document.getElementById('current-time'),
|
|
duration: document.getElementById('duration'),
|
|
progress: document.getElementById('progress'),
|
|
};
|
|
|
|
const api = new GreyscaleAPI();
|
|
const player = new SimplePlayer({
|
|
audio: els.audio,
|
|
progressEl: els.progress,
|
|
currentTimeEl: els.currentTime,
|
|
durationEl: els.duration,
|
|
});
|
|
|
|
let tracks = [];
|
|
|
|
function setStatus(text, type = 'neutral') {
|
|
els.status.textContent = text;
|
|
els.status.dataset.type = type;
|
|
}
|
|
|
|
function setSearchLoading(loading) {
|
|
els.button.disabled = loading;
|
|
els.input.disabled = loading;
|
|
els.button.textContent = loading ? 'Searching...' : 'Search';
|
|
}
|
|
|
|
function setPlayButtonState() {
|
|
els.playPauseBtn.textContent = els.audio.paused ? '▶' : '❚❚';
|
|
}
|
|
|
|
function updatePlayerMeta(track) {
|
|
if (!track) {
|
|
els.playerTitle.textContent = 'No track selected';
|
|
els.playerArtist.textContent = '-';
|
|
els.playerCover.src = '';
|
|
return;
|
|
}
|
|
|
|
els.playerTitle.textContent = track.title;
|
|
els.playerArtist.textContent = track.artist;
|
|
|
|
const cover = api.getCoverUrl(track.cover, 320);
|
|
els.playerCover.src = cover || '';
|
|
}
|
|
|
|
function renderResults(items) {
|
|
if (!items.length) {
|
|
els.results.innerHTML = '<li class="result-empty">No tracks found.</li>';
|
|
return;
|
|
}
|
|
|
|
els.results.innerHTML = items
|
|
.map(
|
|
(track, index) => `
|
|
<li class="result-item" data-index="${index}">
|
|
<img class="result-cover" src="${escapeHtml(api.getCoverUrl(track.cover, 160))}" alt="" loading="lazy" />
|
|
<div class="result-meta">
|
|
<div class="result-title">${escapeHtml(track.title)}</div>
|
|
<div class="result-subtitle">${escapeHtml(track.artist)}${
|
|
track.albumTitle ? ` — ${escapeHtml(track.albumTitle)}` : ''
|
|
}</div>
|
|
</div>
|
|
<div class="result-duration">${formatDuration(track.duration)}</div>
|
|
</li>
|
|
`
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
async function resolveTrack(track) {
|
|
return api.getTrackStream(track.id, 'HI_RES_LOSSLESS');
|
|
}
|
|
|
|
async function playByIndex(index) {
|
|
if (!Number.isInteger(index) || index < 0 || index >= tracks.length) return;
|
|
|
|
player.setQueue(tracks, index);
|
|
|
|
try {
|
|
setStatus(`Loading: ${tracks[index].title}`, 'neutral');
|
|
await player.playCurrent(resolveTrack);
|
|
setStatus(`Playing: ${tracks[index].title}`, 'ok');
|
|
} catch (error) {
|
|
console.error(error);
|
|
setStatus(`Playback failed: ${error.message || 'Unknown error'}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function runSearch(query) {
|
|
const trimmed = query.trim();
|
|
if (!trimmed) return;
|
|
|
|
setSearchLoading(true);
|
|
setStatus(`Searching for "${trimmed}"...`, 'neutral');
|
|
|
|
try {
|
|
tracks = await api.searchTracks(trimmed);
|
|
renderResults(tracks);
|
|
setStatus(`Found ${tracks.length} track${tracks.length === 1 ? '' : 's'}.`, 'ok');
|
|
} catch (error) {
|
|
console.error(error);
|
|
tracks = [];
|
|
renderResults([]);
|
|
setStatus(`Search failed: ${error.message || 'Unknown error'}`, 'error');
|
|
} finally {
|
|
setSearchLoading(false);
|
|
}
|
|
}
|
|
|
|
async function bootstrap() {
|
|
setStatus('Loading API instances...', 'neutral');
|
|
|
|
try {
|
|
await api.initInstances();
|
|
setStatus('Ready.', 'ok');
|
|
} catch (error) {
|
|
console.error(error);
|
|
setStatus('Instance loading failed, using fallback list.', 'error');
|
|
}
|
|
|
|
els.form.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
await runSearch(els.input.value);
|
|
});
|
|
|
|
els.results.addEventListener('click', async (event) => {
|
|
const item = event.target.closest('.result-item');
|
|
if (!item) return;
|
|
|
|
const index = Number(item.dataset.index);
|
|
await playByIndex(index);
|
|
});
|
|
|
|
player.onTrackChanged = (track) => {
|
|
updatePlayerMeta(track);
|
|
};
|
|
|
|
player.setResolver(resolveTrack);
|
|
|
|
els.playPauseBtn.addEventListener('click', async () => {
|
|
try {
|
|
await player.togglePlayPause();
|
|
} catch (error) {
|
|
console.error(error);
|
|
setStatus(`Playback error: ${error.message || 'Unknown error'}`, 'error');
|
|
}
|
|
});
|
|
|
|
els.prevBtn.addEventListener('click', async () => {
|
|
try {
|
|
await player.playPrev();
|
|
} catch (error) {
|
|
console.error(error);
|
|
setStatus(`Previous track failed: ${error.message || 'Unknown error'}`, 'error');
|
|
}
|
|
});
|
|
|
|
els.nextBtn.addEventListener('click', async () => {
|
|
try {
|
|
await player.playNext();
|
|
} catch (error) {
|
|
console.error(error);
|
|
setStatus(`Next track failed: ${error.message || 'Unknown error'}`, 'error');
|
|
}
|
|
});
|
|
|
|
els.audio.addEventListener('play', setPlayButtonState);
|
|
els.audio.addEventListener('pause', setPlayButtonState);
|
|
setPlayButtonState();
|
|
}
|
|
|
|
bootstrap();
|