Files
SaraTune/PlayQueue.py
T
2026-10-09 15:33:43 +02:00

155 lines
5.5 KiB
Python

"""Music mode's queue: what plays next, shuffle and repeat. Shuffling
reorders the queue itself (the current track first), so the Queue page
always shows the real play order; turning it off brings the original order
back. Saved between runs as Session.toml."""
import random
from dataclasses import dataclass, field
from typing import Optional
import tomlkit
from Core import ConfigDir, SessionPath
RepeatModes = ["Off", "All", "One"]
RestartAfterMs = 3000 # Previous restarts the track when it's further in than this
@dataclass
class PlayQueue:
Paths: list[str] = field(default_factory=list)
Index: int = -1 # into Paths; -1 = nothing
Shuffle: bool = False
Repeat: str = "Off"
Unshuffled: list[str] = field(default_factory=list) # the order before shuffling
@property
def Current(self) -> Optional[str]:
return self.Paths[self.Index] if 0 <= self.Index < len(self.Paths) else None
def Set(self, Paths: list[str], Start: int = 0) -> None:
"""Replaces the queue, e.g. with an album, starting at Paths[Start]."""
self.Paths = list(Paths)
self.Index = Start if Paths else -1
self.Unshuffled = list(Paths)
if self.Shuffle and Paths:
self._ShuffleRest()
def _ShuffleRest(self) -> None:
Current = self.Paths[self.Index]
Rest = self.Paths[: self.Index] + self.Paths[self.Index + 1 :]
random.shuffle(Rest)
self.Paths = [Current] + Rest
self.Index = 0
def SetShuffle(self, Enabled: bool) -> None:
if Enabled == self.Shuffle:
return
self.Shuffle = Enabled
if not self.Paths:
return
if Enabled:
self.Unshuffled = list(self.Paths)
self._ShuffleRest()
else:
Current = self.Current
self.Paths = self.Unshuffled or self.Paths
self.Index = self.Paths.index(Current) if Current in self.Paths else 0
def CycleRepeat(self) -> str:
self.Repeat = RepeatModes[(RepeatModes.index(self.Repeat) + 1) % len(RepeatModes)]
return self.Repeat
def Next(self, Finished: bool = False) -> Optional[int]:
"""The index to play after the current one (None: stop). Finished means
the track ran out by itself, which is when Repeat One applies."""
if not self.Paths:
return None
if Finished and self.Repeat == "One":
return self.Index
if self.Index + 1 < len(self.Paths):
return self.Index + 1
return 0 if self.Repeat != "Off" or not Finished else None
def Previous(self) -> Optional[int]:
if not self.Paths:
return None
if self.Index > 0:
return self.Index - 1
return len(self.Paths) - 1 if self.Repeat == "All" else 0
# --- Editing ------------------------------------------------------------
def PlayNext(self, Paths: list[str]) -> None:
At = self.Index + 1 if self.Index >= 0 else 0
self.Paths[At:At] = Paths
self.Unshuffled.extend(Paths)
if self.Index < 0:
self.Index = 0
def Append(self, Paths: list[str]) -> None:
self.Paths.extend(Paths)
self.Unshuffled.extend(Paths)
if self.Index < 0 and self.Paths:
self.Index = 0
def Remove(self, At: int) -> None:
Removed = self.Paths.pop(At)
if Removed in self.Unshuffled:
self.Unshuffled.remove(Removed)
if At < self.Index or (At == self.Index and self.Index >= len(self.Paths)):
self.Index -= 1
if not self.Paths:
self.Index = -1
def Move(self, Old: int, New: int) -> None:
Item = self.Paths.pop(Old)
self.Paths.insert(New, Item)
if Old == self.Index:
self.Index = New
elif Old < self.Index <= New:
self.Index -= 1
elif New <= self.Index < Old:
self.Index += 1
def Clear(self) -> None:
self.Paths, self.Unshuffled, self.Index = [], [], -1
@dataclass
class Session:
Queue: PlayQueue = field(default_factory=PlayQueue)
Position: int = 0 # milliseconds into the current track
View: str = "Songs"
def Save(self) -> None:
Document = tomlkit.document()
Document["View"] = self.View
Document["Position"] = int(self.Position)
Document["Index"] = self.Queue.Index
Document["Shuffle"] = self.Queue.Shuffle
Document["Repeat"] = self.Queue.Repeat
for Key, Paths in (("Paths", self.Queue.Paths), ("Unshuffled", self.Queue.Unshuffled)):
Document[Key] = tomlkit.array()
Document[Key].multiline(True)
Document[Key].extend(Paths)
ConfigDir.mkdir(parents=True, exist_ok=True)
SessionPath.write_text(tomlkit.dumps(Document), encoding="utf-8")
def LoadSession() -> Session:
try:
Data = tomlkit.parse(SessionPath.read_text(encoding="utf-8")).unwrap()
except (OSError, tomlkit.exceptions.TOMLKitError):
return Session()
Paths = [str(Item) for Item in Data.get("Paths", []) if isinstance(Item, str)]
Index = Data.get("Index", -1)
Queue = PlayQueue(
Paths=Paths,
Index=Index if isinstance(Index, int) and 0 <= Index < len(Paths) else (0 if Paths else -1),
Shuffle=bool(Data.get("Shuffle", False)),
Repeat=Data.get("Repeat") if Data.get("Repeat") in RepeatModes else "Off",
Unshuffled=[str(Item) for Item in Data.get("Unshuffled", []) if isinstance(Item, str)],
)
return Session(Queue=Queue, Position=int(Data.get("Position", 0) or 0), View=str(Data.get("View", "Songs")))