From b7c43605604148d3452e79e4d26b35d7a1a53a3a Mon Sep 17 00:00:00 2001 From: lelo Date: Wed, 19 Mar 2025 17:47:01 +0100 Subject: [PATCH] nice audio player --- allowed_secrets.json.example.json | 6 ++ app.py | 21 ++-- requirements_transcription.txt | 5 + static/app.js | 13 ++- static/audioplayer.css | 121 +++++++++++++++++++++++ static/audioplayer.js | 157 ++++++++++++++++++++++++++++++ static/styles.css | 28 ------ templates/browse.html | 37 +++++-- transcribe_all.py | 2 +- 9 files changed, 346 insertions(+), 44 deletions(-) create mode 100644 allowed_secrets.json.example.json create mode 100644 requirements_transcription.txt create mode 100644 static/audioplayer.css create mode 100644 static/audioplayer.js diff --git a/allowed_secrets.json.example.json b/allowed_secrets.json.example.json new file mode 100644 index 0000000..ba8022d --- /dev/null +++ b/allowed_secrets.json.example.json @@ -0,0 +1,6 @@ +{ + "dev_key_f83745ft0g5rg3": { + "expiry" : "31.12.2000", + "file_root": "\\\\path\\if\\using\\windows" + } +} \ No newline at end of file diff --git a/app.py b/app.py index 3859229..06adf67 100755 --- a/app.py +++ b/app.py @@ -18,8 +18,9 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) app.config['SECRET_KEY'] = '85c1117eb3a5f2c79f0ff395bada8ff8d9a257b99ef5e143' app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90) -app.config['SESSION_COOKIE_SAMESITE'] = 'None' -app.config['SESSION_COOKIE_SECURE'] = True +if os.environ.get('FLASK_ENV') == 'production': + app.config['SESSION_COOKIE_SAMESITE'] = 'None' + app.config['SESSION_COOKIE_SECURE'] = True def load_allowed_secrets(filename='allowed_secrets.json'): with open(filename) as f: @@ -29,20 +30,23 @@ def load_allowed_secrets(filename='allowed_secrets.json'): value['expiry'] = datetime.strptime(value['expiry'], '%d.%m.%Y').date() return secrets -app.config['ALLOWED_SECRETS'] = load_allowed_secrets() - def require_secret(f): @wraps(f) def decorated_function(*args, **kwargs): - allowed_secrets = app.config['ALLOWED_SECRETS'] + allowed_secrets = load_allowed_secrets() today = date.today() def is_valid(secret_data): + print("is_valid:", secret_data) expiry_date = secret_data.get('expiry') - return expiry_date and today <= expiry_date + is_valid = expiry_date and today <= expiry_date + print("is_valid", is_valid) + return is_valid # Check if a secret was provided via GET parameter get_secret = request.args.get('secret') + print("request.args:", request.args) + print("get_secret:", get_secret) if get_secret is not None: secret_data = allowed_secrets.get(get_secret) if secret_data: @@ -51,13 +55,17 @@ def require_secret(f): session['secret'] = get_secret session.permanent = True app.config['FILE_ROOT'] = secret_data.get('file_root') + print("session:", session['secret']) return f(*args, **kwargs) else: # Secret provided via URL is expired or invalid return render_template('error.html', message="Invalid or expired secret."), 403 + + # If no secret provided via GET, check the session session_secret = session.get('secret') + print("session_secret", session_secret) if session_secret is not None: secret_data = allowed_secrets.get(session_secret) if secret_data: @@ -186,6 +194,7 @@ def generate_breadcrumbs(subpath): @app.route('/api/path/') @require_secret def api_browse(subpath): + print("subpath:", subpath) file_root = app.config['FILE_ROOT'] directory = os.path.join(file_root, subpath.replace('/', os.sep)) diff --git a/requirements_transcription.txt b/requirements_transcription.txt new file mode 100644 index 0000000..bb2ef44 --- /dev/null +++ b/requirements_transcription.txt @@ -0,0 +1,5 @@ +openai-whisper +#https://pytorch.org/get-started/locally/ +torch==2.5.1 +torchvision==0.20.1 +torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu124 \ No newline at end of file diff --git a/static/app.js b/static/app.js index 4649836..64dc21e 100644 --- a/static/app.js +++ b/static/app.js @@ -91,7 +91,7 @@ function loadDirectory(subpath) { }) .catch(error => { console.error('Error loading directory:', error); - document.getElementById('content').innerHTML = '

Error loading directory.

'; + document.getElementById('content').innerHTML = '

Error loading directory!

'+error+'

'; }); } @@ -143,6 +143,7 @@ document.querySelectorAll('.play-file').forEach(link => { const fileType = this.getAttribute('data-file-type'); const relUrl = this.getAttribute('data-url'); const nowPlayingInfo = document.getElementById('nowPlayingInfo'); + document.getElementById('audioPlayerContainer').style.display = "block" // Remove the class from all file items. document.querySelectorAll('.file-item').forEach(item => { @@ -154,6 +155,7 @@ document.querySelectorAll('.play-file').forEach(link => { const idx = this.getAttribute('data-index'); const audioPlayer = document.getElementById('globalAudio'); currentMusicIndex = idx !== null ? parseInt(idx) : -1; + const playerButton = document.querySelector('.player-button'); // Add the class to the clicked file item's parent. this.closest('.file-item').classList.add('currently-playing'); @@ -167,7 +169,11 @@ document.querySelectorAll('.play-file').forEach(link => { audioPlayer.pause(); audioPlayer.src = ''; // Clear existing source. - nowPlayingInfo.textContent = "Wird geladen..."; + // only show this if it takes longer. avoid flicker + let loaderTimeout = setTimeout(() => { + playerButton.innerHTML = playIcon; + nowPlayingInfo.textContent = "Wird geladen..."; + }, 500); document.querySelector('footer').style.display = 'flex'; const mediaUrl = '/media/' + relUrl; @@ -175,7 +181,7 @@ document.querySelectorAll('.play-file').forEach(link => { try { // Pass the signal to the fetch so it can be aborted. const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal }); - + clearTimeout(loaderTimeout); // done loading. stop timeout if (response.status === 403) { nowPlayingInfo.textContent = "Fehler: Zugriff verweigert."; window.location.href = '/'; // Redirect if forbidden. @@ -186,6 +192,7 @@ document.querySelectorAll('.play-file').forEach(link => { audioPlayer.src = mediaUrl; audioPlayer.load(); audioPlayer.play(); + playerButton.innerHTML = pauseIcon; const pathParts = relUrl.split('/'); const fileName = pathParts.pop(); const pathStr = pathParts.join('/'); diff --git a/static/audioplayer.css b/static/audioplayer.css new file mode 100644 index 0000000..7aef1ad --- /dev/null +++ b/static/audioplayer.css @@ -0,0 +1,121 @@ +.audio-player-container { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + padding: 7px; + width: 100%; + display: none; +} + +/* Now Playing Info */ +.now-playing-info { + margin-top: 5px; + font-size: 16px; + color: #23313f; + text-align: center; +} + +.audio-player { + --player-button-width: 4em; + --sound-button-width: 3em; + --space: 0.5em; +} + +.controls { + display: flex; + align-items: center; + width: 100%; + gap: var(--space); /* ensures spacing between controls */ +} + +/* Make the slider container fill the available space and stack its children vertically */ +.slider { + flex: 1; + display: flex; + flex-direction: column; +} + +/* The range input takes the full width of its container */ +.timeline { + -webkit-appearance: none; + width: 100%; + height: 0.5em; + background-color: #ccc; /* light gray background */ + border-radius: 5px; + background-size: 0% 100%; + background-image: linear-gradient(#34495e, #34495e); /* progress bar */ + background-repeat: no-repeat; + appearance: none; + outline: none; +} + +/* Slider Thumb Styling */ +/* WebKit browsers (Chrome, Safari) */ +.timeline::-webkit-slider-thumb { + -webkit-appearance: none; + width: 1em; + height: 1em; + border-radius: 50%; + background-color: #34495e; + cursor: pointer; + border: none; + outline: none; +} + +/* Firefox */ +.timeline::-moz-range-thumb { + width: 1em; + height: 1em; + border-radius: 50%; + background-color: #34495e; + cursor: pointer; + border: none; + outline: none; +} + +/* Internet Explorer and Edge */ +.timeline::-ms-thumb { + width: 1em; + height: 1em; + border-radius: 50%; + background-color: #34495e; + cursor: pointer; + border: none; + outline: none; +} + + +/* Remove default track styling */ +.timeline::-webkit-slider-runnable-track, +.timeline::-moz-range-track, +.timeline::-ms-track { + -webkit-appearance: none; + box-shadow: none; + border: none; + background: transparent; +} + +/* Style the time info (positioned right below the slider) */ +.now-playing-info { + text-align: center; + font-size: 0.8em; + margin-top: 0.25em; +} + +.player-button, +.sound-button { + background-color: transparent; + border: 0; + cursor: pointer; + padding: 0; +} + +.player-button { + width: var(--player-button-width); + height: var(--player-button-width); +} + +.sound-button { + width: var(--sound-button-width); + height: var(--sound-button-width); +} diff --git a/static/audioplayer.js b/static/audioplayer.js new file mode 100644 index 0000000..d96c932 --- /dev/null +++ b/static/audioplayer.js @@ -0,0 +1,157 @@ +const playerButton = document.querySelector('.player-button'), + audio = document.querySelector('audio'), + timeline = document.querySelector('.timeline'), + soundButton = document.querySelector('.sound-button'), + timeInfo = document.getElementById('timeInfo'), + playIcon = ` + + + + `, + pauseIcon = ` + + + + `, + soundIcon = ` + + + + `, + muteIcon = ` + + + + `; + +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 +}); +timeline.addEventListener('change', changeSeek); + +// 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', () => { + updateTimeInfo(); + timeline.min = 0; + timeline.max = audio.duration; + timeline.value = 0; // Ensures the thumb starts at the left. +}); + +// --- Update the slider thumb position (in seconds) while playing --- +function changeTimelinePosition() { + if (!isSeeking) { + timeline.value = audio.currentTime; + const percentagePosition = (audio.currentTime / audio.duration) * 100; + timeline.style.backgroundSize = `${percentagePosition}% 100%`; + } +} + +// --- Seek function: directly set audio.currentTime using slider's value (in seconds) --- +function changeSeek() { + audio.currentTime = timeline.value; +} + +// --- Sound toggle --- +function toggleSound () { + audio.muted = !audio.muted; + soundButton.innerHTML = audio.muted ? muteIcon : soundIcon; +} +soundButton.addEventListener('click', toggleSound); + +// --- 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 the time display --- +function updateTimeInfo() { + const currentTimeFormatted = formatTime(audio.currentTime); + const durationFormatted = isNaN(audio.duration) ? "00:00" : formatTime(audio.duration); + timeInfo.innerHTML = `${currentTimeFormatted} / ${durationFormatted}`; +} + +// --- Update timeline, time info, and media session on each time update --- +audio.ontimeupdate = function() { + changeTimelinePosition(); + updateTimeInfo(); + if ('mediaSession' in navigator && typeof navigator.mediaSession.setPositionState === 'function') { + navigator.mediaSession.setPositionState({ + duration: audio.duration, + playbackRate: audio.playbackRate, + position: audio.currentTime + }); + } +}; + +// --- Also update media session on play (in case timeupdate lags) --- +audio.onplay = function() { + if ('mediaSession' in navigator && typeof navigator.mediaSession.setPositionState === 'function') { + navigator.mediaSession.setPositionState({ + duration: audio.duration, + playbackRate: audio.playbackRate, + position: audio.currentTime + }); + } +}; + +// --- Handle external seek requests (e.g. from Android or Windows system controls) --- +if ('mediaSession' in navigator) { + navigator.mediaSession.setActionHandler('seekto', function(details) { + if (details.fastSeek && 'fastSeek' in audio) { + audio.fastSeek(details.seekTime); + } else { + audio.currentTime = details.seekTime; + } + changeTimelinePosition(); + }); +} + +// --- When audio ends --- +audio.onended = function() { + playerButton.innerHTML = playIcon; +}; + +function downloadAudio() { + // Get the current audio source URL + const audioSrc = audio.currentSrc || audio.src; + if (audioSrc) { + // Create a temporary link element + const a = document.createElement('a'); + a.href = audioSrc; + // Extract the file name from the URL, decode it, and set as download attribute + let fileName = audioSrc.split('/').pop() || 'audio'; + fileName = decodeURIComponent(fileName); + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } +} + diff --git a/static/styles.css b/static/styles.css index 8a5e44b..1cac5fa 100644 --- a/static/styles.css +++ b/static/styles.css @@ -127,34 +127,6 @@ footer { align-items: center; justify-content: center; } -footer audio { - width: 75%; - transform: scale(1.3); /* Default for other devices */ - padding: 10px 0; -} - -/* Target Safari specifically */ -@supports (-webkit-touch-callout: none) { - footer audio { - width: 60%; - transform: scale(1.6); /* Different scale for Apple devices */ - } -} -.audio-player-container { - background-color: #fff; - border-radius: 8px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - padding: 7px; - width: 100%; -} - -/* Now Playing Info */ -.now-playing-info { - margin-top: 5px; - font-size: 16px; - color: #2c3e50; - text-align: center; -} /* Modal Styles */ #transcriptModal { diff --git a/templates/browse.html b/templates/browse.html index ec6ef64..a97fc60 100644 --- a/templates/browse.html +++ b/templates/browse.html @@ -28,6 +28,7 @@ +