Web player served by stemserve
Plex browsing, stem mixer with presets, shuffle, repeat all/one, playback speed, queue, waveform seek. stemserve serves web/ at / without the token. stemserve image now builds from the repo root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
605b88f1eb
commit
9299fee9a0
8 files changed
+1113
-20
No files matched your search
@@ -16,6 +16,7 @@ Endpoints (all GET unless noted; add ?token=... or X-Stem-Token when TOKEN is se
|
||||
POST /api/prepare?key=<key>&fmt=opus&br=160 start transcodes in the background
|
||||
POST /api/stem {"paths": [...]} "Stem Now": stemd separates these Plex files next
|
||||
/file/<key>/<file>[?fmt=opus&br=160] the stem itself, with Range support
|
||||
/ the web player (web/, or WEB_DIR); no token needed
|
||||
|
||||
<key> is the track's folder relative to STEMS_DIR, e.g. "Artist/Album/01 Song".
|
||||
"""
|
||||
@@ -50,6 +51,10 @@ FFMPEG = shutil.which("ffmpeg")
|
||||
OPUS_RATES = (64, 96, 128, 160, 192, 256)
|
||||
PEAK_RATE = 2000 # Hz the envelope is computed at; plenty for a few hundred bars
|
||||
CHUNK = 1 << 20
|
||||
# The web player. Served without the token; it asks for one in its settings when TOKEN is set.
|
||||
WEB_DIR = os.path.realpath(os.environ.get("WEB_DIR", os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "web")))
|
||||
WEB_TYPES = {".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8",
|
||||
".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon", ".json": "application/json"}
|
||||
|
||||
_pool = ThreadPoolExecutor(max_workers=max(2, (os.cpu_count() or 2) // 2))
|
||||
_locks: dict[str, threading.Lock] = {}
|
||||
@@ -267,6 +272,17 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def _error(self, status, msg):
|
||||
self._send(status, json.dumps({"error": msg}).encode())
|
||||
|
||||
def _web(self, path):
|
||||
if path == "/":
|
||||
path = "/index.html"
|
||||
full = os.path.realpath(os.path.join(WEB_DIR, path.lstrip("/")))
|
||||
if not full.startswith(WEB_DIR + os.sep) or not os.path.isfile(full):
|
||||
return self._error(404, "not found")
|
||||
with open(full, "rb") as f:
|
||||
body = f.read()
|
||||
ctype = WEB_TYPES.get(os.path.splitext(full)[1], "application/octet-stream")
|
||||
self._send(200, body, ctype, {"Cache-Control": "no-cache"})
|
||||
|
||||
def do_HEAD(self):
|
||||
self.do_GET()
|
||||
|
||||
@@ -277,6 +293,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
url = urllib.parse.urlsplit(self.path)
|
||||
query = urllib.parse.parse_qs(url.query)
|
||||
path = urllib.parse.unquote(url.path)
|
||||
if not path.startswith(("/api/", "/file/")):
|
||||
return self._web(path)
|
||||
if not self._authorized(query):
|
||||
return self._error(HTTPStatus.UNAUTHORIZED, "bad token")
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user