mirror of
https://git.srhtnin.garden/sarah/SaraTune.git
synced 2026-10-13 18:18:11 +02:00
151 lines
5.4 KiB
Python
151 lines
5.4 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 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")))
|