- Python package stemroller/ -> stemify/, Android package com.stemroller.mobile -> com.stemify.mobile, app names, QML module, icons, desktop entries, MPRIS and Plex product names - Done-markers are now .stemify.json; stemd renames the old ones once at startup so songs separated before the rename aren't separated again - A top-level README covering the server, the Android and Android Auto app, the EQ, Stem Now and the desktop apps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
"""UI-side owner of the separation worker process and its socket."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
from PySide6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, Signal
|
|
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
|
|
|
|
|
class WorkerClient(QObject):
|
|
stateChanged = Signal(str) # starting | ready | crashed
|
|
hello = Signal(dict)
|
|
vram = Signal(str, int, int) # device, free_mb, total_mb
|
|
progress = Signal(str, float, str) # id, pct, stage
|
|
done = Signal(str, dict)
|
|
failed = Signal(str, str) # id, message
|
|
cancelled = Signal(str)
|
|
crashed = Signal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
runtime = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
|
|
self._sock_path = os.path.join(runtime, f"stemify-{os.getpid()}.sock")
|
|
QLocalServer.removeServer(self._sock_path)
|
|
self._server = QLocalServer(self)
|
|
self._server.newConnection.connect(self._on_connection)
|
|
if not self._server.listen(self._sock_path):
|
|
raise RuntimeError(f"cannot listen on {self._sock_path}: {self._server.errorString()}")
|
|
self._sock: QLocalSocket | None = None
|
|
self._buf = b""
|
|
self._proc: QProcess | None = None
|
|
self._quitting = False
|
|
self.state = "starting"
|
|
self._restart_delay = 500
|
|
|
|
def start(self):
|
|
self._set_state("starting")
|
|
proc = QProcess(self)
|
|
env = QProcessEnvironment.systemEnvironment()
|
|
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
env.insert("PYTHONPATH", pkg_root + os.pathsep + env.value("PYTHONPATH", ""))
|
|
env.insert("PYTHONUNBUFFERED", "1")
|
|
proc.setProcessEnvironment(env)
|
|
proc.setProcessChannelMode(QProcess.ProcessChannelMode.ForwardedChannels)
|
|
proc.finished.connect(self._on_finished)
|
|
proc.start(sys.executable, ["-m", "stemify.worker", "--socket", self._sock_path])
|
|
self._proc = proc
|
|
|
|
def send(self, type_, **payload):
|
|
if self._sock and self._sock.state() == QLocalSocket.LocalSocketState.ConnectedState:
|
|
self._sock.write((json.dumps({"type": type_, **payload}) + "\n").encode())
|
|
self._sock.flush()
|
|
return True
|
|
return False
|
|
|
|
def pid(self):
|
|
return int(self._proc.processId()) if self._proc else 0
|
|
|
|
def shutdown(self):
|
|
self._quitting = True
|
|
self.send("quit")
|
|
if self._proc and self._proc.state() != QProcess.ProcessState.NotRunning:
|
|
if not self._proc.waitForFinished(1500):
|
|
self._proc.kill()
|
|
self._proc.waitForFinished(1000)
|
|
self._server.close()
|
|
|
|
def _set_state(self, s):
|
|
self.state = s
|
|
self.stateChanged.emit(s)
|
|
|
|
def _on_connection(self):
|
|
sock = self._server.nextPendingConnection()
|
|
if self._sock:
|
|
self._sock.deleteLater()
|
|
self._sock = sock
|
|
self._buf = b""
|
|
sock.readyRead.connect(self._on_ready_read)
|
|
|
|
def _on_ready_read(self):
|
|
self._buf += bytes(self._sock.readAll())
|
|
while b"\n" in self._buf:
|
|
line, self._buf = self._buf.split(b"\n", 1)
|
|
if line.strip():
|
|
self._dispatch(json.loads(line))
|
|
|
|
def _dispatch(self, msg):
|
|
t = msg.pop("type")
|
|
if t == "hello":
|
|
self._restart_delay = 500
|
|
self._set_state("ready")
|
|
self.hello.emit(msg)
|
|
elif t == "vram":
|
|
self.vram.emit(msg["device"], msg["free_mb"], msg["total_mb"])
|
|
elif t == "progress":
|
|
self.progress.emit(msg["id"], msg["pct"], msg["stage"])
|
|
elif t == "done":
|
|
self.done.emit(msg.pop("id"), msg)
|
|
elif t == "error":
|
|
self.failed.emit(msg.get("id") or "", msg.get("msg", "unknown error"))
|
|
elif t == "cancelled":
|
|
self.cancelled.emit(msg["id"])
|
|
|
|
def _on_finished(self, code, status):
|
|
self._sock = None
|
|
if self._quitting:
|
|
return
|
|
self._set_state("crashed")
|
|
self.crashed.emit()
|
|
QTimer.singleShot(self._restart_delay, self.start)
|
|
self._restart_delay = min(self._restart_delay * 2, 10000)
|