"""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")))