199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
|
|
from src.Library import *
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List, Optional, Any
|
|
|
|
def _dt_to_str(dt: Optional[datetime]) -> Optional[str]:
|
|
return dt.isoformat() if dt else None
|
|
|
|
|
|
def _str_to_dt(s: Optional[str]) -> Optional[datetime]:
|
|
return datetime.fromisoformat(s) if s else None
|
|
|
|
|
|
def _path_to_str(p: Optional[Path]) -> Optional[str]:
|
|
return str(p) if p else None
|
|
|
|
|
|
def _str_to_path(s: Optional[str]) -> Optional[Path]:
|
|
return Path(s) if s else None
|
|
|
|
|
|
class LibrarySaver:
|
|
def __init__(self, library: Library):
|
|
self.library = library
|
|
|
|
def save(self, path: Path) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("w", encoding="utf-8") as f:
|
|
json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"tracks": [self._track_to_dict(t) for t in self.library.tracks],
|
|
"playlists": [self._playlist_to_dict(p) for p in self.library.playlists],
|
|
"tracklists": [self._tracklist_to_dict(tl) for tl in self.library.tracklists],
|
|
"albums": [self._album_to_dict(a) for a in self.library.albums],
|
|
}
|
|
|
|
def _track_to_dict(self, t: Track) -> dict[str, Any]:
|
|
return {
|
|
"id": t.id,
|
|
"title": t.title,
|
|
"artists": t.artists,
|
|
"album": t.album,
|
|
"duration_ms": t.duration_ms,
|
|
"added_at": _dt_to_str(t.added_at),
|
|
"spotify": self._spotify_to_dict(t.spotify),
|
|
"local": self._local_to_dict(t.local),
|
|
}
|
|
|
|
def _playlist_to_dict(self, p: Playlist) -> dict[str, Any]:
|
|
return {
|
|
"name": p.name,
|
|
"tracks": [t.id for t in p.tracks],
|
|
"created_at": _dt_to_str(p.created_at),
|
|
"description": p.description,
|
|
"spotify": self._spotify_to_dict(p.spotify),
|
|
}
|
|
|
|
def _tracklist_to_dict(self, tl: TrackList) -> dict[str, Any]:
|
|
return {"tracks": [t.id for t in tl.tracks]}
|
|
|
|
def _album_to_dict(self, a: Album) -> dict[str, Any]:
|
|
return {
|
|
"title": a.title,
|
|
"release_date": a.release_date,
|
|
"tracks": [t.id for t in a.tracks],
|
|
"spotify": self._spotify_to_dict(a.spotify),
|
|
}
|
|
|
|
def _local_to_dict(self, local):
|
|
if not local:
|
|
return None
|
|
return {"path": local.path}
|
|
|
|
def _spotify_to_dict(self, s: Optional[SpotifyData]) -> Optional[dict[str, Any]]:
|
|
if not s:
|
|
return None
|
|
return {
|
|
"id": s.id,
|
|
"uri": s.uri,
|
|
"url": s.url,
|
|
"href": s.href,
|
|
"file_path": _path_to_str(s.file_path),
|
|
"popularity": s.popularity,
|
|
"is_playable": s.is_playable,
|
|
"is_local": s.is_local,
|
|
"external_ids": s.external_ids,
|
|
"artists": [{"id": a.id, "name": a.name, "uri": a.uri, "url": a.url} for a in s.artists],
|
|
"album": {
|
|
"id": s.album.id,
|
|
"name": s.album.name,
|
|
"release_date": s.album.release_date,
|
|
"total_tracks": s.album.total_tracks,
|
|
"uri": s.album.uri,
|
|
"url": s.album.url,
|
|
} if s.album else None,
|
|
}
|
|
|
|
|
|
class LibraryLoader:
|
|
def __init__(self):
|
|
# Map of track id -> Track object
|
|
self.track_map: dict[str, Track] = {}
|
|
|
|
def load(self, path: Path) -> Library:
|
|
with path.open("r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
# Load tracks first
|
|
tracks = []
|
|
for t in data.get("tracks", []):
|
|
track = self._track_from_dict(t)
|
|
tracks.append(track)
|
|
self.track_map[track.id] = track
|
|
|
|
# Load playlists using track_map
|
|
playlists = []
|
|
for p in data.get("playlists", []):
|
|
pl_tracks = []
|
|
for tid in p.get("tracks", []):
|
|
if tid not in self.track_map:
|
|
raise KeyError(f"Track ID '{tid}' not found in track map")
|
|
pl_tracks.append(self.track_map[tid])
|
|
|
|
playlists.append(Playlist(
|
|
name=p["name"],
|
|
tracks=pl_tracks,
|
|
created_at=_str_to_dt(p.get("created_at")),
|
|
description=p.get("description"),
|
|
spotify=self._spotify_from_dict(p.get("spotify")),
|
|
))
|
|
|
|
|
|
# Load tracklists
|
|
tracklists = []
|
|
for tl in data.get("tracklists", []):
|
|
tl_tracks = []
|
|
for tid in tl.get("tracks", []):
|
|
if tid not in self.track_map:
|
|
raise KeyError(f"Track ID '{tid}' not found in track map (tracklist)")
|
|
tl_tracks.append(self.track_map[tid])
|
|
tracklists.append(TrackList(tracks=tl_tracks))
|
|
|
|
# Load albums
|
|
albums = []
|
|
for a in data.get("albums", []):
|
|
alb_tracks = []
|
|
for tid in a.get("tracks", []):
|
|
if tid not in self.track_map:
|
|
raise KeyError(f"Track ID '{tid}' not found in track map (album '{a.get('title')}')")
|
|
alb_tracks.append(self.track_map[tid])
|
|
albums.append(Album(
|
|
title=a["title"],
|
|
release_date=a.get("release_date"),
|
|
tracks=alb_tracks,
|
|
spotify=self._spotify_from_dict(a.get("spotify")),
|
|
))
|
|
|
|
return Library(playlists=playlists, tracks=tracks, tracklists=tracklists, albums=albums)
|
|
|
|
def _track_from_dict(self, d: dict[str, Any]) -> Track:
|
|
return Track(
|
|
id=d["id"],
|
|
title=d["title"],
|
|
artists=d["artists"],
|
|
album=d.get("album"),
|
|
duration_ms=d.get("duration_ms"),
|
|
added_at=_str_to_dt(d.get("added_at")),
|
|
spotify=self._spotify_from_dict(d.get("spotify")),
|
|
local=self._local_from_dict(d.get("local")),
|
|
)
|
|
|
|
def _local_from_dict(self, d):
|
|
if not d:
|
|
return None
|
|
return LocalData(
|
|
path=d["path"],
|
|
)
|
|
|
|
def _spotify_from_dict(self, d: Optional[dict[str, Any]]) -> Optional[SpotifyData]:
|
|
if not d:
|
|
return None
|
|
return SpotifyData(
|
|
id=d["id"],
|
|
uri=d["uri"],
|
|
url=d["url"],
|
|
href=d["href"],
|
|
file_path=_str_to_path(d.get("file_path")),
|
|
popularity=d.get("popularity"),
|
|
is_playable=d.get("is_playable"),
|
|
is_local=d.get("is_local"),
|
|
external_ids=d.get("external_ids"),
|
|
artists=[SpotifyArtist(**a) for a in d.get("artists", [])],
|
|
album=SpotifyAlbum(**d["album"]) if d.get("album") else None,
|
|
)
|