bethaus-app/static/audioplayer.js
2025-05-11 21:47:14 +02:00

208 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// read the CSS variable from :root (or any selector)
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--dark-background').trim();
// player DOM elements
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
const audioPlayer = document.getElementById('globalAudio');
const audioPlayerContainer = document.getElementById('audioPlayerContainer');
const playerButton = document.querySelector('.player-button'),
audio = document.querySelector('audio'),
timeline = document.querySelector('.timeline'),
timeInfo = document.getElementById('timeInfo'),
playIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
`,
pauseIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
`,
soundIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
</svg>
`
function toggleAudio () {
if (audio.paused) {
audio.play();
playerButton.innerHTML = pauseIcon;
} else {
audio.pause();
playerButton.innerHTML = playIcon;
}
}
playerButton.addEventListener('click', toggleAudio);
let isSeeking = false;
// --- Slider (Timeline) events ---
// Mouse events
timeline.addEventListener('mousedown', () => { isSeeking = true; });
timeline.addEventListener('mouseup', () => { isSeeking = false;
changeSeek(); // Update the audio currentTime based on the slider position
});
// Touch events
timeline.addEventListener('touchstart', () => { isSeeking = true; });
timeline.addEventListener('touchend', () => { isSeeking = false;
changeSeek();
});
timeline.addEventListener('touchcancel', () => { isSeeking = false; });
// --- When metadata is loaded, set slider range in seconds ---
audio.addEventListener('loadedmetadata', () => {
timeline.min = 0;
timeline.max = audio.duration;
});
// --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
function changeSeek() {
audio.currentTime = timeline.value;
}
// --- Utility: Format seconds as mm:ss ---
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`;
}
// --- Update timeline, time info, and media session on each time update ---
audio.ontimeupdate = function() {
// --- Update the slider thumb position (in seconds) while playing ---
if (!isSeeking) {
timeline.value = audio.currentTime;
const percentagePosition = (audio.currentTime / audio.duration) * 100;
timeline.style.backgroundSize = `${percentagePosition}% 100%`;
}
// --- Update the time display ---
const currentTimeFormatted = formatTime(audio.currentTime);
const durationFormatted = isNaN(audio.duration) ? "00:00" : formatTime(audio.duration);
timeInfo.innerHTML = `${currentTimeFormatted} / ${durationFormatted}`;
if ('mediaSession' in navigator &&
'setPositionState' in navigator.mediaSession &&
!isNaN(audio.duration)) {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
}
};
// --- When audio ends ---
audio.onended = function() {
playerButton.innerHTML = playIcon;
};
async function downloadAudio() {
const audio = document.getElementById('globalAudio');
const src = audio.currentSrc || audio.src;
if (!src) return;
// Build the URL with your download flag + cachebuster
const downloadUrl = new URL(src, window.location.href);
downloadUrl.searchParams.set('download', 'true');
downloadUrl.searchParams.set('_', Date.now());
// Create a “real” link to that URL and click it
const a = document.createElement('a');
a.href = downloadUrl.toString();
// NOTE: do NOT set a.download here we want the server's Content-Disposition to drive it
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Global variable to store the current fetch's AbortController.
let currentFetchController = null;
async function startPlaying(relUrl) {
// Pause the audio and clear its source.
audioPlayer.pause();
audioPlayer.src = '';
// Display the audio player container.
audioPlayerContainer.style.display = "block";
// Set a timeout to display a loader message if needed.
const loaderTimeout = setTimeout(() => {
playerButton.innerHTML = playIcon;
nowPlayingInfo.textContent = "Wird geladen...";
}, 250);
const spinnerTimer = setTimeout(showSpinner, 500);
footer.style.display = 'flex';
// Abort any previous fetch if still running.
if (currentFetchController) {
currentFetchController.abort();
}
currentFetchController = new AbortController();
const mediaUrl = `/media/${relUrl}`;
try {
// Perform a HEAD request to verify media availability.
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
clearTimeout(loaderTimeout);
if (response.status === 403) {
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
window.location.href = '/';
return;
} else if (!response.ok) {
nowPlayingInfo.textContent = `Fehler: Unerwarteter Status (${response.status}).`;
console.error('Unexpected response status:', response.status);
return;
}
// Set the media URL, load, and play the audio.
audioPlayer.src = mediaUrl;
audioPlayer.load();
await audioPlayer.play();
clearTimeout(spinnerTimer);
hideSpinner();
currentTrackPath = relUrl;
playerButton.innerHTML = pauseIcon;
// Process file path for display.
const pathParts = relUrl.split('/');
const folderName = pathParts[pathParts.length - 2];
const fileName = pathParts.pop();
const pathStr = pathParts.join('/');
// Update Media Session metadata if available.
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: fileName.replace(/\.[^/.]+$/, ''), // remove extension
artist: folderName,
artwork: [
{ src: '/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' }
]
});
}
nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
'<br><span style="font-size: larger; font-weight: bold;">' +
fileName.replace('.mp3', '') + '</span>';
} catch (error) {
if (error.name === 'AbortError') {
console.log('Previous fetch aborted.');
} else {
console.error('Error fetching media:', error);
nowPlayingInfo.textContent = "Fehler: Netzwerkproblem oder ungültige URL.";
}
};
}