StemRoller: desktop stem separator, Android stems player, and server daemons
- stemroller/: PySide6/QML desktop app and player for separating music into stems - android/: Plex music player that plays per-stem mixes, with Android Auto support, a graphic + parametric (Equalizer APO-style) EQ, and offline downloads - server/: stemserve (serves stems to the app) and stemd (separates the library on the server's GPU, with "Stem Now" priority requests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
716c5499ff
commit
1845f03c41
184 files changed
+24648
No files matched your search
@@ -0,0 +1,82 @@
|
||||
"""Queue of separation jobs as a QML list model."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, Qt, Signal
|
||||
|
||||
|
||||
@dataclass
|
||||
class Job:
|
||||
id: str
|
||||
file: str
|
||||
file_name: str
|
||||
model_id: str
|
||||
model_label: str
|
||||
two_stems: bool
|
||||
device_id: str
|
||||
cache_dir: str
|
||||
status: str = "queued" # queued | running | done | error | cancelled
|
||||
progress: float = 0.0
|
||||
stage: str = "Waiting"
|
||||
message: str = ""
|
||||
started: float = 0.0
|
||||
elapsed: float = 0.0
|
||||
result: dict = field(default_factory=dict)
|
||||
exported_to: str = ""
|
||||
|
||||
|
||||
ROLES = ["jobId", "fileName", "filePath", "modelLabel", "status", "progress", "stage", "message", "elapsed"]
|
||||
_ATTR = {"jobId": "id", "fileName": "file_name", "filePath": "file", "modelLabel": "model_label"}
|
||||
|
||||
|
||||
class JobsModel(QAbstractListModel):
|
||||
countChanged = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.jobs: list[Job] = []
|
||||
self._roles = {Qt.ItemDataRole.UserRole + i: name for i, name in enumerate(ROLES)}
|
||||
|
||||
def roleNames(self):
|
||||
return {k: QByteArray(v.encode()) for k, v in self._roles.items()}
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return 0 if parent.isValid() else len(self.jobs)
|
||||
|
||||
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid() or role not in self._roles:
|
||||
return None
|
||||
name = self._roles[role]
|
||||
return getattr(self.jobs[index.row()], _ATTR.get(name, name))
|
||||
|
||||
def get(self, job_id) -> Job | None:
|
||||
return next((j for j in self.jobs if j.id == job_id), None)
|
||||
|
||||
def row_of(self, job_id) -> int:
|
||||
return next((i for i, j in enumerate(self.jobs) if j.id == job_id), -1)
|
||||
|
||||
def add(self, job: Job):
|
||||
n = len(self.jobs)
|
||||
self.beginInsertRows(QModelIndex(), n, n)
|
||||
self.jobs.append(job)
|
||||
self.endInsertRows()
|
||||
self.countChanged.emit()
|
||||
|
||||
def remove(self, job_id):
|
||||
row = self.row_of(job_id)
|
||||
if row >= 0:
|
||||
self.beginRemoveRows(QModelIndex(), row, row)
|
||||
del self.jobs[row]
|
||||
self.endRemoveRows()
|
||||
self.countChanged.emit()
|
||||
|
||||
def update(self, job_id, **changes):
|
||||
row = self.row_of(job_id)
|
||||
if row < 0:
|
||||
return
|
||||
job = self.jobs[row]
|
||||
for k, v in changes.items():
|
||||
setattr(job, k, v)
|
||||
idx = self.index(row)
|
||||
roles = [r for r, name in self._roles.items() if _ATTR.get(name, name) in changes or name in changes]
|
||||
self.dataChanged.emit(idx, idx, roles)
|
||||
Reference in new issue
Block a user