diff --git a/static/app.js b/static/app.js
index c6a9fef..cf4e913 100644
--- a/static/app.js
+++ b/static/app.js
@@ -45,9 +45,6 @@ function paintFile() {
if (currentMusicFileElement) {
const fileItem = currentMusicFileElement.closest('.file-item');
fileItem.classList.add('currently-playing');
- // setTimeout(() => {
- // fileItem.scrollIntoView({ block: "center", inline: "nearest" });
- // }, 300);
}
}
}
@@ -310,7 +307,7 @@ document.querySelectorAll('.play-file').forEach(link => {
// Mark the clicked item as currently playing.
this.closest('.file-item').classList.add('currently-playing');
- startPlaying(relUrl);
+ player.loadTrack(relUrl);
// Delay preloading to avoid blocking playback.
setTimeout(preload_audio, 1000);
diff --git a/static/audioplayer.js b/static/audioplayer.js
index 37b98cf..dc3f2d4 100644
--- a/static/audioplayer.js
+++ b/static/audioplayer.js
@@ -1,300 +1,239 @@
-// read the CSS variable from :root (or any selector)
-const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--dark-background').trim();
+class SimpleAudioPlayer {
+ constructor({
+ audioSelector = '#globalAudio',
+ playBtnSelector = '.player-button',
+ timelineSelector = '.timeline',
+ infoSelector = '#timeInfo',
+ nowPlayingSelector = '#nowPlayingInfo',
+ containerSelector = '#audioPlayerContainer',
+ footerSelector = 'footer'
+ } = {}) {
+ // Elements
+ this.audio = document.querySelector(audioSelector);
+ this.playBtn = document.querySelector(playBtnSelector);
+ this.timeline = document.querySelector(timelineSelector);
+ this.timeInfo = document.querySelector(infoSelector);
+ this.nowInfo = document.querySelector(nowPlayingSelector);
+ this.container = document.querySelector(containerSelector);
+ this.footer = document.querySelector(footerSelector);
-// player DOM elements
-const nowPlayingInfo = document.getElementById('nowPlayingInfo');
-const audio = document.getElementById('globalAudio');
-const audioPlayerContainer = document.getElementById('audioPlayerContainer');
+ // State
+ this.isSeeking = false;
+ this.rafId = null;
+ this.abortCtrl = null;
+ this._posInterval = null;
-const playerButton = document.querySelector('.player-button'),
- timeline = document.querySelector('.timeline'),
- timeInfo = document.getElementById('timeInfo'),
- playIcon = `
-
- `,
- pauseIcon = `
-
- `,
- soundIcon = `
-
- `
+ // Pre-compute icons once
+ const fill = getComputedStyle(document.documentElement)
+ .getPropertyValue('--dark-background').trim();
+ this.icons = {
+ play : this.svg(
+ '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',
+ fill
+ ),
+ pause: this.svg(
+ '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',
+ fill
+ ),
+ sound: this.svg(
+ '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',
+ fill
+ )
+ };
-function toggleAudio () {
- if (audio.paused) {
- audio.play();
- playerButton.innerHTML = pauseIcon;
- } else {
- audio.pause();
- playerButton.innerHTML = playIcon;
+ // Seeked => refresh timeline if external seek (e.g. Android controls)
+ this.audio.addEventListener('seeked', () => this.updateTimeline());
+
+ // Bind UI events
+ this._bindEvents();
+
+ // Setup MediaSession handlers + heartbeat
+ this._initMediaSession();
}
-}
-playerButton.addEventListener('click', toggleAudio);
-let isSeeking = false;
+ // Minimal SVG helper
+ svg(pathD, fill) {
+ return ``;
+ }
-// --- 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
-});
+ _bindEvents() {
+ this.playBtn.addEventListener('click', () => this.togglePlay());
-// Touch events
-timeline.addEventListener('touchstart', () => { isSeeking = true; });
-timeline.addEventListener('touchend', () => { isSeeking = false;
- changeSeek();
-});
-timeline.addEventListener('touchcancel', () => { isSeeking = false; });
+ this.audio.addEventListener('loadedmetadata', () => {
+ this.timeline.min = 0;
+ this.timeline.max = this.audio.duration;
+ this.timeline.value = 0;
+ this.timeline.style.backgroundSize = '0% 100%';
+ });
+ this.audio.addEventListener('play', () => this._updatePlayState());
+ this.audio.addEventListener('pause', () => this._updatePlayState());
+ this.audio.addEventListener('ended', () => this.playBtn.innerHTML = this.icons.play);
-// --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
-function changeSeek() {
- audio.currentTime = timeline.value;
+ // Native timeupdate => update timeline
+ this.audio.addEventListener('timeupdate', () => this.updateTimeline());
- if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
- navigator.mediaSession.setPositionState({
- duration: audio.duration,
- playbackRate: audio.playbackRate,
- position: audio.currentTime
+ // Fallback interval for throttled mobile
+ this._interval = setInterval(() => {
+ if (!this.audio.paused && !this.isSeeking) {
+ this.updateTimeline();
+ }
+ }, 500);
+
+ // Unified seek input
+ this.timeline.addEventListener('input', () => {
+ this.isSeeking = true;
+ this.audio.currentTime = this.timeline.value;
+
+ // immediate Android sync
+ if (navigator.mediaSession?.setPositionState) {
+ navigator.mediaSession.setPositionState({
+ duration: this.audio.duration,
+ playbackRate: this.audio.playbackRate,
+ position: this.audio.currentTime
+ });
+ }
+
+ this.updateTimeline();
+ this.isSeeking = false;
});
}
-}
-// --- 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}`;
-}
+ togglePlay() {
+ this.audio.paused ? this.audio.play() : this.audio.pause();
+ }
+ _updatePlayState() {
+ this.playBtn.innerHTML = this.audio.paused ? this.icons.play : this.icons.pause;
+ if ('mediaSession' in navigator) {
+ navigator.mediaSession.playbackState = this.audio.paused ? 'paused' : 'playing';
+ }
+ }
-// --- Update timeline, time info, and media session on each time update ---
-function updateTimeline() {
- if (!isSeeking && audio.duration) {
- timeline.value = audio.currentTime;
- timeline.style.backgroundSize = `${(audio.currentTime / audio.duration) * 100}% 100%`;
- timeInfo.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`;
+ _formatTime(sec) {
+ const m = Math.floor(sec / 60), s = Math.floor(sec % 60);
+ return `${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`;
+ }
+
+ updateTimeline() {
+ if (!this.audio.duration || this.isSeeking) return;
+
+ // 1) Move the thumb
+ this.timeline.value = this.audio.currentTime;
+ // 2) Resize fill
+ const pct = (this.audio.currentTime / this.audio.duration) * 100;
+ this.timeline.style.backgroundSize = `${pct}% 100%`;
+ // 3) Update text
+ this.timeInfo.textContent =
+ `${this._formatTime(this.audio.currentTime)} / ${this._formatTime(this.audio.duration)}`;
+ // 4) Push to Android widget
+ if (navigator.mediaSession?.setPositionState) {
+ navigator.mediaSession.setPositionState({
+ duration: this.audio.duration,
+ playbackRate: this.audio.playbackRate,
+ position: this.audio.currentTime
+ });
+ }
+ }
+
+ async loadTrack(relUrl) {
+ this.audio.pause();
+ this.container.style.display = 'block';
+ this.nowInfo.textContent = 'Loading…';
+
+ this.abortCtrl?.abort();
+ this.abortCtrl = new AbortController();
+
+ try {
+ const head = await fetch(`/media/${relUrl}`, {
+ method: 'HEAD',
+ signal: this.abortCtrl.signal
+ });
+ if (!head.ok) throw new Error(`Status ${head.status}`);
+
+ this.audio.src = `/media/${relUrl}`;
+ await this.audio.play();
+
+ // Full breadcrumb
+ const parts = relUrl.split('/');
+ const file = parts.pop();
+ const folderPath = parts.join(' › ');
+ this.nowInfo.innerHTML =
+ `${folderPath} › ${file.replace(/\.[^/.]+$/, '')}`;
+
+ if ('mediaSession' in navigator) {
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title : file.replace(/\.[^/.]+$/, ''),
+ artist: parts.pop(),
+ artwork: [{ src:'/icons/logo-192x192.png', sizes:'192x192', type:'image/png' }]
+ });
+ }
+ } catch (err) {
+ if (err.name !== 'AbortError') {
+ this.nowInfo.textContent = 'Error loading track';
+ console.error(err);
+ }
+ }
+ }
+
+ _initMediaSession() {
+ if (!('mediaSession' in navigator)) return;
+
+ const wrap = fn => details => {
+ fn(details);
+ this.updateTimeline();
+ };
+
+ // Core controls
+ navigator.mediaSession.setActionHandler('play', wrap(() => this.audio.play()));
+ navigator.mediaSession.setActionHandler('pause', wrap(() => this.audio.pause()));
+ navigator.mediaSession.setActionHandler('seekbackward', wrap(({seekOffset=10}) => {
+ this.audio.currentTime = Math.max(0, this.audio.currentTime - seekOffset);
+ }));
+ navigator.mediaSession.setActionHandler('seekforward', wrap(({seekOffset=10}) => {
+ this.audio.currentTime = Math.min(this.audio.duration, this.audio.currentTime + seekOffset);
+ }));
+ navigator.mediaSession.setActionHandler('seekto', wrap(({seekTime, fastSeek}) => {
+ if (fastSeek && this.audio.fastSeek) this.audio.fastSeek(seekTime);
+ else this.audio.currentTime = seekTime;
+ }));
+
+ // Prev & Next
+ navigator.mediaSession.setActionHandler('previoustrack', wrap(() => {
+ if (currentMusicIndex > 0) {
+ const prevFile = currentMusicFiles[currentMusicIndex - 1];
+ const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
+ if (prevLink) {
+ prevLink.click();
+ }
+ }
+ }));
+ navigator.mediaSession.setActionHandler('nexttrack', wrap(() => {
+ if (currentMusicIndex < currentMusicFiles.length - 1) {
+ const nextFile = currentMusicFiles[currentMusicIndex + 1];
+ const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
+ if (nextLink) {
+ nextLink.click();
+ }
+ }
+ }));
+
+ // Heartbeat for widget
+ this._posInterval = setInterval(() => {
+ if (!this.audio.paused && navigator.mediaSession.setPositionState) {
+ navigator.mediaSession.setPositionState({
+ duration: this.audio.duration,
+ playbackRate: this.audio.playbackRate,
+ position: this.audio.currentTime
+ });
+ }
+ }, 1000);
}
}
-// --- When metadata is loaded, set slider range in seconds ---
-audio.addEventListener('loadedmetadata', () => {
- timeline.min = 0;
- timeline.max = audio.duration;
- timeline.value = 0;
- timeline.style.backgroundSize = '0% 100%';
-});
-
-// --- Update timeline and time info on time update ---
-audio.addEventListener('timeupdate', () => {
- updateTimeline();
-
- if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
- navigator.mediaSession.setPositionState({
- duration: audio.duration,
- playbackRate: audio.playbackRate,
- position: audio.currentTime
- });
- }
-});
-
-// --- Update media session metadata when audio is loaded ---
-audio.addEventListener('play', () => {
- if ('mediaSession' in navigator)
- navigator.mediaSession.playbackState = 'playing';
-});
-audio.addEventListener('pause', () => {
- if ('mediaSession' in navigator)
- navigator.mediaSession.playbackState = 'paused';
-});
-
-audio.ontimeupdate = updateTimeline;
-
-// Fallback for mobile throttling
-setInterval(() => {
- if (!audio.paused && !isSeeking) updateTimeline();
-}, 500);
+// Initialize instance
+const player = new SimpleAudioPlayer();
-// --- When audio ends ---
-audio.onended = function() {
- playerButton.innerHTML = playIcon;
-};
-
-async function downloadAudio() {
- const src = audio.currentSrc || audio.src;
- if (!src) return;
-
- // Build the URL with your download flag + cache‑buster
- 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();
- a.download = ''; // tell Safari “this is a download”
- a.target = '_blank'; // force a real navigation on iOS
- // 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.
- audio.pause();
- audio.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.
- audio.src = mediaUrl;
- audio.load();
- await audio.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' }
- ]
- });
- navigator.mediaSession.playbackState = 'playing';
- navigator.mediaSession.setPositionState({
- duration: audio.duration,
- playbackRate: audio.playbackRate,
- position: 0
- });
- }
-
- nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
- '
' +
- fileName.replace('.mp3', '') + '';
- } 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.";
- }
- };
-}
-
-
-if ('mediaSession' in navigator) {
-
- // Handler for the play action
- navigator.mediaSession.setActionHandler('play', () => {
- navigator.mediaSession.playbackState = 'playing'
- document.getElementById('globalAudio').play();
- });
-
- // Handler for the pause action
- navigator.mediaSession.setActionHandler('pause', () => {
- navigator.mediaSession.playbackState = 'paused'
- document.getElementById('globalAudio').pause();
- });
-
- // Handler for the previous track action
- navigator.mediaSession.setActionHandler('previoustrack', () => {
- if (currentMusicIndex > 0) {
- const prevFile = currentMusicFiles[currentMusicIndex - 1];
- const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
- if (prevLink) {
- prevLink.click();
- }
- }
- });
-
- // Handler for the next track action
- navigator.mediaSession.setActionHandler('nexttrack', () => {
- if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
- const nextFile = currentMusicFiles[currentMusicIndex + 1];
- const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
- if (nextLink) {
- nextLink.click();
- }
- }
- });
-
- // Handler for the seek backward and seek forward actions
- navigator.mediaSession.setActionHandler('seekbackward', details => {
- const offset = details.seekOffset || 10;
- audio.currentTime = Math.max(0, audio.currentTime - offset);
- });
- navigator.mediaSession.setActionHandler('seekforward', details => {
- const offset = details.seekOffset || 10;
- audio.currentTime = Math.min(audio.duration, audio.currentTime + offset);
- });
-
- // Handler for the seek backward action
- navigator.mediaSession.setActionHandler('seekto', ({seekTime, fastSeek}) => {
- if (fastSeek && 'fastSeek' in audio) {
- audio.fastSeek(seekTime);
- } else {
- audio.currentTime = seekTime;
- }
- // immediately update the remote clock
- navigator.mediaSession.setPositionState({
- duration: audio.duration,
- playbackRate: audio.playbackRate,
- position: audio.currentTime
- });
- });
-
-}
\ No newline at end of file
diff --git a/static/audioplayer_bak.js b/static/audioplayer_bak.js
new file mode 100644
index 0000000..37b98cf
--- /dev/null
+++ b/static/audioplayer_bak.js
@@ -0,0 +1,300 @@
+// 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 audio = document.getElementById('globalAudio');
+const audioPlayerContainer = document.getElementById('audioPlayerContainer');
+
+const playerButton = document.querySelector('.player-button'),
+ timeline = document.querySelector('.timeline'),
+ timeInfo = document.getElementById('timeInfo'),
+ playIcon = `
+
+ `,
+ pauseIcon = `
+
+ `,
+ soundIcon = `
+
+ `
+
+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; });
+
+
+// --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
+function changeSeek() {
+ audio.currentTime = timeline.value;
+
+ if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
+ navigator.mediaSession.setPositionState({
+ duration: audio.duration,
+ playbackRate: audio.playbackRate,
+ position: audio.currentTime
+ });
+ }
+}
+
+// --- 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 ---
+function updateTimeline() {
+ if (!isSeeking && audio.duration) {
+ timeline.value = audio.currentTime;
+ timeline.style.backgroundSize = `${(audio.currentTime / audio.duration) * 100}% 100%`;
+ timeInfo.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`;
+ }
+}
+
+// --- When metadata is loaded, set slider range in seconds ---
+audio.addEventListener('loadedmetadata', () => {
+ timeline.min = 0;
+ timeline.max = audio.duration;
+ timeline.value = 0;
+ timeline.style.backgroundSize = '0% 100%';
+});
+
+// --- Update timeline and time info on time update ---
+audio.addEventListener('timeupdate', () => {
+ updateTimeline();
+
+ if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
+ navigator.mediaSession.setPositionState({
+ duration: audio.duration,
+ playbackRate: audio.playbackRate,
+ position: audio.currentTime
+ });
+ }
+});
+
+// --- Update media session metadata when audio is loaded ---
+audio.addEventListener('play', () => {
+ if ('mediaSession' in navigator)
+ navigator.mediaSession.playbackState = 'playing';
+});
+audio.addEventListener('pause', () => {
+ if ('mediaSession' in navigator)
+ navigator.mediaSession.playbackState = 'paused';
+});
+
+audio.ontimeupdate = updateTimeline;
+
+// Fallback for mobile throttling
+setInterval(() => {
+ if (!audio.paused && !isSeeking) updateTimeline();
+}, 500);
+
+
+// --- When audio ends ---
+audio.onended = function() {
+ playerButton.innerHTML = playIcon;
+};
+
+async function downloadAudio() {
+ const src = audio.currentSrc || audio.src;
+ if (!src) return;
+
+ // Build the URL with your download flag + cache‑buster
+ 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();
+ a.download = ''; // tell Safari “this is a download”
+ a.target = '_blank'; // force a real navigation on iOS
+ // 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.
+ audio.pause();
+ audio.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.
+ audio.src = mediaUrl;
+ audio.load();
+ await audio.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' }
+ ]
+ });
+ navigator.mediaSession.playbackState = 'playing';
+ navigator.mediaSession.setPositionState({
+ duration: audio.duration,
+ playbackRate: audio.playbackRate,
+ position: 0
+ });
+ }
+
+ nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
+ '
' +
+ fileName.replace('.mp3', '') + '';
+ } 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.";
+ }
+ };
+}
+
+
+if ('mediaSession' in navigator) {
+
+ // Handler for the play action
+ navigator.mediaSession.setActionHandler('play', () => {
+ navigator.mediaSession.playbackState = 'playing'
+ document.getElementById('globalAudio').play();
+ });
+
+ // Handler for the pause action
+ navigator.mediaSession.setActionHandler('pause', () => {
+ navigator.mediaSession.playbackState = 'paused'
+ document.getElementById('globalAudio').pause();
+ });
+
+ // Handler for the previous track action
+ navigator.mediaSession.setActionHandler('previoustrack', () => {
+ if (currentMusicIndex > 0) {
+ const prevFile = currentMusicFiles[currentMusicIndex - 1];
+ const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
+ if (prevLink) {
+ prevLink.click();
+ }
+ }
+ });
+
+ // Handler for the next track action
+ navigator.mediaSession.setActionHandler('nexttrack', () => {
+ if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
+ const nextFile = currentMusicFiles[currentMusicIndex + 1];
+ const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
+ if (nextLink) {
+ nextLink.click();
+ }
+ }
+ });
+
+ // Handler for the seek backward and seek forward actions
+ navigator.mediaSession.setActionHandler('seekbackward', details => {
+ const offset = details.seekOffset || 10;
+ audio.currentTime = Math.max(0, audio.currentTime - offset);
+ });
+ navigator.mediaSession.setActionHandler('seekforward', details => {
+ const offset = details.seekOffset || 10;
+ audio.currentTime = Math.min(audio.duration, audio.currentTime + offset);
+ });
+
+ // Handler for the seek backward action
+ navigator.mediaSession.setActionHandler('seekto', ({seekTime, fastSeek}) => {
+ if (fastSeek && 'fastSeek' in audio) {
+ audio.fastSeek(seekTime);
+ } else {
+ audio.currentTime = seekTime;
+ }
+ // immediately update the remote clock
+ navigator.mediaSession.setPositionState({
+ duration: audio.duration,
+ playbackRate: audio.playbackRate,
+ position: audio.currentTime
+ });
+ });
+
+}
\ No newline at end of file
diff --git a/static/search.js b/static/search.js
index 0959140..f70839d 100644
--- a/static/search.js
+++ b/static/search.js
@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', function() {
card.className = 'card';
card.innerHTML = `
Anzahl Downloads: ${file.hitcount}
${ file.performance_date !== undefined ? `Datum: ${file.performance_date}
` : ``} diff --git a/templates/app.html b/templates/app.html index 820facd..cc8fb77 100644 --- a/templates/app.html +++ b/templates/app.html @@ -245,10 +245,10 @@ + -