Skip to content

Commit 26986cb

Browse files
committed
add opus api endpoint
1 parent b1e6544 commit 26986cb

File tree

1 file changed

+89
-1
lines changed

1 file changed

+89
-1
lines changed

src/tauon/t_modules/t_webserve.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import logging
2626
import os
2727
import struct
28+
import subprocess
2829
import time
2930
from http.server import BaseHTTPRequestHandler, HTTPServer
3031
from socketserver import ThreadingMixIn
@@ -412,6 +413,73 @@ def run_command(self, callback) -> None:
412413
callback()
413414
self.wfile.write(b"OK")
414415

416+
def stream_opus_file(self, track: TrackClass) -> None:
417+
ffmpeg_path = tauon.get_ffmpeg()
418+
if ffmpeg_path is None:
419+
self.send_response(503)
420+
self.end_headers()
421+
self.wfile.write(b"ffmpeg unavailable")
422+
return
423+
424+
command = [str(ffmpeg_path), "-v", "error"]
425+
if track.start_time:
426+
command.extend(["-ss", str(track.start_time)])
427+
if track.length > 0:
428+
command.extend(["-t", str(track.length)])
429+
command.extend([
430+
"-i", track.fullpath,
431+
"-vn",
432+
"-c:a", "libopus",
433+
"-b:a", "84k",
434+
"-f", "ogg",
435+
"-",
436+
])
437+
438+
try:
439+
encoder = subprocess.Popen(
440+
command,
441+
stdin=subprocess.DEVNULL,
442+
stdout=subprocess.PIPE,
443+
stderr=subprocess.DEVNULL,
444+
)
445+
except OSError:
446+
logging.exception("Failed to start ffmpeg for /api1/fileopus")
447+
self.send_response(500)
448+
self.end_headers()
449+
self.wfile.write(b"Transcode start failed")
450+
return
451+
452+
if encoder.stdout is None:
453+
self.send_response(500)
454+
self.end_headers()
455+
self.wfile.write(b"Transcode stream unavailable")
456+
return
457+
458+
self.send_response(200)
459+
self.send_header("Content-type", "audio/ogg")
460+
self.send_header("Content-Disposition", 'attachment; filename="track.opus"')
461+
self.send_header("Connection", "close")
462+
self.end_headers()
463+
self.close_connection = True
464+
465+
try:
466+
while True:
467+
data = encoder.stdout.read(65536)
468+
if not data:
469+
break
470+
self.wfile.write(data)
471+
except (BrokenPipeError, ConnectionResetError):
472+
pass
473+
finally:
474+
encoder.stdout.close()
475+
if encoder.poll() is None:
476+
encoder.terminate()
477+
try:
478+
encoder.wait(timeout=1)
479+
except subprocess.TimeoutExpired:
480+
encoder.kill()
481+
encoder.wait(timeout=1)
482+
415483
def toggle_album_shuffle(self) -> None:
416484
pctl.album_shuffle_mode ^= True
417485
tauon.gui.update += 1
@@ -472,7 +540,9 @@ def do_GET(self) -> None:
472540
self.wfile.write(b"404 Not found")
473541
return
474542
if tauon.remote_limited and (
475-
not path.startswith("/api1/pic/medium/") and not path.startswith("/api1/file/")
543+
not path.startswith("/api1/pic/medium/")
544+
and not path.startswith("/api1/file/")
545+
and not path.startswith("/api1/fileopus")
476546
):
477547
self.send_response(404)
478548
self.end_headers()
@@ -595,6 +665,24 @@ def do_GET(self) -> None:
595665
self.end_headers()
596666
self.wfile.write(b"Invalid parameter")
597667

668+
elif path.startswith("/api1/fileopus/"):
669+
param = path[15:].split("?", 1)[0]
670+
671+
play_timer.hit()
672+
673+
if param.isdigit() and int(param) in pctl.master_library:
674+
track = pctl.master_library[int(param)]
675+
if not track.fullpath or not os.path.isfile(track.fullpath):
676+
self.send_response(404)
677+
self.end_headers()
678+
self.wfile.write(b"File unavailable")
679+
else:
680+
self.stream_opus_file(track)
681+
else:
682+
self.send_response(404)
683+
self.end_headers()
684+
self.wfile.write(b"Invalid parameter")
685+
598686
elif path.startswith("/api1/start/"):
599687
levels, _ = self.parse_trail(path)
600688
if len(levels) == 5:

0 commit comments

Comments
 (0)