"""Saved playlists: one TOML file each in ~/.config/SaraSuite/SaraTune/Playlists/, holding the playlist's name and its files' paths in order. Also reads and writes .m3u/.m3u8, so playlists can move to and from other players.""" import re from dataclasses import dataclass, field from pathlib import Path import tomlkit from Core import IsAudioFile, IsWindows, PlaylistsDir @dataclass class Playlist: Name: str Tracks: list[str] = field(default_factory=list) @property def FilePath(self) -> Path: return PlaylistsDir / f"{SafeFileName(self.Name)}.toml" def Save(self) -> None: PlaylistsDir.mkdir(parents=True, exist_ok=True) Document = tomlkit.document() Document["Name"] = self.Name Document["Tracks"] = tomlkit.array() Document["Tracks"].multiline(True) Document["Tracks"].extend(self.Tracks) self.FilePath.write_text(tomlkit.dumps(Document), encoding="utf-8") def Delete(self) -> None: self.FilePath.unlink(missing_ok=True) def SafeFileName(Name: str) -> str: """A name that works as a file name on Linux and Windows.""" return re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", Name).strip(" .") or "Playlist" def LoadPlaylists() -> list[Playlist]: Result: list[Playlist] = [] if not PlaylistsDir.is_dir(): return Result for FilePath in PlaylistsDir.glob("*.toml"): try: Data = tomlkit.parse(FilePath.read_text(encoding="utf-8")).unwrap() except (OSError, tomlkit.exceptions.TOMLKitError): continue Tracks = [str(Item) for Item in Data.get("Tracks", []) if isinstance(Item, str)] Result.append(Playlist(str(Data.get("Name") or FilePath.stem), Tracks)) return sorted(Result, key=lambda Item: Item.Name.lower()) def NameTaken(Name: str, Except: str = "") -> bool: """Two names that make the same file count as the same playlist.""" Wanted = SafeFileName(Name).lower() return any(SafeFileName(Item.Name).lower() == Wanted and Item.Name != Except for Item in LoadPlaylists()) def ExportM3U(Item: Playlist, Destination: Path) -> None: Lines = ["#EXTM3U", f"#PLAYLIST:{Item.Name}"] + Item.Tracks Destination.write_text("\n".join(Lines) + "\n", encoding="utf-8") def ReadM3U(Source: Path) -> Playlist: """Relative entries count from the .m3u file's folder; URLs and missing files are skipped.""" Name = Source.stem Tracks: list[str] = [] for Line in Source.read_text(encoding="utf-8", errors="replace").splitlines(): Line = Line.strip().lstrip("") if Line.startswith("#PLAYLIST:"): Name = Line.split(":", 1)[1].strip() or Name continue if not Line or Line.startswith("#") or "://" in Line: continue # Playlists made on Windows use backslashes, which Linux paths don't split on. Entry = Path(Line if IsWindows else Line.replace("\\", "/")) if not Entry.is_absolute(): Entry = Source.parent / Entry if Entry.is_file() and IsAudioFile(Entry): Tracks.append(str(Entry.resolve())) return Playlist(Name, Tracks)