From dbebdf79fd1ca7dfa11f3490eb4c94a19df37a32 Mon Sep 17 00:00:00 2001 From: lelo Date: Thu, 29 May 2025 21:51:44 +0000 Subject: [PATCH] rework audioplayer --- static/app.js | 5 +- static/audioplayer.js | 499 +++++++++++++++++--------------------- static/audioplayer_bak.js | 300 +++++++++++++++++++++++ static/search.js | 2 +- templates/app.html | 2 +- 5 files changed, 522 insertions(+), 286 deletions(-) create mode 100644 static/audioplayer_bak.js 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 @@ + -