Skip to content

Commit 9b10c19

Browse files
committed
Work for the srubbing for the Tesla
1 parent ac01fc8 commit 9b10c19

File tree

4 files changed

+80
-26
lines changed

4 files changed

+80
-26
lines changed

routes/stream.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from services.background_tasks import get_transcription_queue, TranscriptionJob
1212
from services.database import add_to_history, get_history, clear_history
1313
from services.path_utils import expand_path
14+
from services.streaming import get_audio_duration
1415
from services.youtube import get_video_metadata, extract_video_id
1516

1617
logger = logging.getLogger(__name__)
@@ -181,19 +182,24 @@ def get_audio_file(video_id: str):
181182

182183
if _audio_is_ready(video_id):
183184
file_size = audio_path.stat().st_size
185+
headers = {
186+
"Accept-Ranges": "bytes",
187+
"Cache-Control": "public, max-age=3600",
188+
"Content-Length": str(file_size),
189+
"Access-Control-Allow-Origin": "*",
190+
"Access-Control-Expose-Headers": "X-Audio-Duration",
191+
"Connection": "keep-alive",
192+
}
193+
duration = get_audio_duration(video_id)
194+
if duration is not None:
195+
headers["X-Audio-Duration"] = str(duration)
184196

185197
# FileResponse automatically handles streaming, chunking, and seeking (Range headers)
186198
return FileResponse(
187199
path=audio_path,
188200
media_type="audio/mpeg",
189201
filename=f"{video_id}.mp3",
190-
headers={
191-
"Accept-Ranges": "bytes",
192-
"Cache-Control": "public, max-age=3600",
193-
"Content-Length": str(file_size),
194-
"Access-Control-Allow-Origin": "*",
195-
"Connection": "keep-alive",
196-
},
202+
headers=headers,
197203
)
198204

199205
return JSONResponse(
@@ -210,17 +216,18 @@ def check_audio_file(video_id: str):
210216

211217
if _audio_is_ready(video_id):
212218
file_size = audio_path.stat().st_size
213-
return JSONResponse(
214-
status_code=200,
215-
content={},
216-
headers={
217-
"Accept-Ranges": "bytes",
218-
"Cache-Control": "public, max-age=3600",
219-
"Content-Length": str(file_size),
220-
"Content-Type": "audio/mpeg",
221-
"Access-Control-Allow-Origin": "*",
222-
},
223-
)
219+
headers = {
220+
"Accept-Ranges": "bytes",
221+
"Cache-Control": "public, max-age=3600",
222+
"Content-Length": str(file_size),
223+
"Content-Type": "audio/mpeg",
224+
"Access-Control-Allow-Origin": "*",
225+
"Access-Control-Expose-Headers": "X-Audio-Duration",
226+
}
227+
duration = get_audio_duration(video_id)
228+
if duration is not None:
229+
headers["X-Audio-Duration"] = str(duration)
230+
return JSONResponse(status_code=200, content={}, headers=headers)
224231

225232
return JSONResponse(status_code=404, content={}, headers={"Retry-After": "2"})
226233

services/streaming.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
Downloads audio from YouTube using yt-dlp and saves as MP3.
55
"""
66

7+
import json
78
import logging
89
import os
910
import subprocess
11+
from typing import Optional
1012

1113
from services.cache import get_audio_cache
1214
from services.path_utils import expand_path, expand_path_str
@@ -15,6 +17,41 @@
1517
logger = logging.getLogger(__name__)
1618
config = get_config()
1719

20+
_audio_duration_cache: dict[str, float] = {}
21+
22+
23+
def get_audio_duration(youtube_video_id: str) -> Optional[float]:
24+
"""Return duration in seconds from ffprobe. Cached per session."""
25+
if youtube_video_id in _audio_duration_cache:
26+
return _audio_duration_cache[youtube_video_id]
27+
28+
audio_path = expand_path(config.get_audio_path(youtube_video_id))
29+
if not audio_path.exists():
30+
return None
31+
32+
try:
33+
result = subprocess.run(
34+
[
35+
"ffprobe",
36+
"-v",
37+
"quiet",
38+
"-print_format",
39+
"json",
40+
"-show_format",
41+
str(audio_path),
42+
],
43+
capture_output=True,
44+
text=True,
45+
timeout=10,
46+
)
47+
data = json.loads(result.stdout)
48+
duration = float(data["format"]["duration"])
49+
_audio_duration_cache[youtube_video_id] = duration
50+
return duration
51+
except Exception as e:
52+
logger.warning(f"ffprobe failed for {youtube_video_id}: {e}")
53+
return None
54+
1855

1956
def _get_download_marker(youtube_video_id: str) -> str:
2057
"""Get the path for the download-in-progress marker file."""

static/app.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let currentVideoId = null;
1212
let currentQueueId = null;
1313
let isPlaying = false;
1414
let currentTrackTitle = null;
15+
let serverAudioDuration = null; // Authoritative duration from server (ffprobe)
1516
const defaultTitle = 'YouTube Radio';
1617

1718
// Prefetch configuration
@@ -176,6 +177,11 @@ async function waitForAudioFile(videoId, maxWaitSeconds = 60) {
176177

177178
if (response.ok) {
178179
console.log(`Audio file ready after ${attemptCount} attempts (${Date.now() - startTime}ms)`);
180+
const durationHeader = response.headers.get('X-Audio-Duration');
181+
if (durationHeader) {
182+
serverAudioDuration = parseFloat(durationHeader);
183+
remoteLog('log', '[MediaSession] Server audio duration', { duration: serverAudioDuration });
184+
}
179185
return true;
180186
}
181187

@@ -513,25 +519,27 @@ function updatePositionState() {
513519
return;
514520
}
515521

516-
// Validate that we have a valid duration
517-
if (!player.duration || isNaN(player.duration) || !isFinite(player.duration) || player.duration <= 0) {
518-
return;
519-
}
522+
// Use server duration as authoritative fallback when browser reports NaN/Infinity
523+
const duration = (player.duration && !isNaN(player.duration) && isFinite(player.duration) && player.duration > 0)
524+
? player.duration
525+
: serverAudioDuration;
526+
527+
if (!duration) return;
520528

521529
try {
522530
// Clamp position to be within valid range [0, duration]
523-
const position = Math.min(Math.max(0, player.currentTime || 0), player.duration);
531+
const position = Math.min(Math.max(0, player.currentTime || 0), duration);
524532
const playbackRate = player.playbackRate || 1.0;
525533

526534
// All parameters must be valid: duration > 0, position >= 0 && position <= duration, playbackRate != 0
527535
navigator.mediaSession.setPositionState({
528-
duration: player.duration,
536+
duration: duration,
529537
playbackRate: playbackRate,
530538
position: position
531539
});
532540

533541
remoteLog('log', '[MediaSession] Position state updated', {
534-
duration: player.duration,
542+
duration: duration,
535543
position: position,
536544
playbackRate: playbackRate
537545
});
@@ -889,6 +897,7 @@ async function startStreamFromQueue(youtube_video_id, queue_id) {
889897

890898
// Wait for audio file to be ready before playing
891899
showStreamStatus('Starting download from YouTube...');
900+
serverAudioDuration = null; // Reset for new track
892901
const fileReady = await waitForAudioFile(data.youtube_video_id, 60);
893902

894903
if (!fileReady) {
@@ -1582,6 +1591,7 @@ async function startStream() {
15821591

15831592
// Wait for audio file to be ready before playing
15841593
showStreamStatus('Starting download from YouTube...');
1594+
serverAudioDuration = null; // Reset for new track
15851595
const fileReady = await waitForAudioFile(youtube_video_id, 60);
15861596

15871597
if (!fileReady) {

templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ <h2>
218218
<script src="/static/update-checker.js?v=1737842302"></script>
219219

220220
<!-- Main Application Script -->
221-
<script src="/static/app.js?v=1770739501"></script>
221+
<script src="/static/app.js?v=1770739502"></script>
222222
</body>
223223

224224
</html>

0 commit comments

Comments
 (0)