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