refactor 2
This commit is contained in:
parent
a620e7a4f5
commit
5f4bbbaa59
7 changed files with 426 additions and 503 deletions
65
Flow.py
Normal file
65
Flow.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
|
||||||
|
from Env import client_id, client_secret, local_library_path
|
||||||
|
from Env import work_dir
|
||||||
|
|
||||||
|
from src.LibrarySerializer import LibrarySaver, LibraryLoader
|
||||||
|
|
||||||
|
from src.spotify.SpotifyWebAPI import fetch_all
|
||||||
|
from src.spotify.ParseSpotify import Parser
|
||||||
|
|
||||||
|
from src.local.LocalLibraryIndexer import update_local_library
|
||||||
|
from src.local.ParseLocal import LocalParser
|
||||||
|
|
||||||
|
from src.MapLocalToSpotify import map_local_to_spotify
|
||||||
|
|
||||||
|
|
||||||
|
class Flow:
|
||||||
|
spotify_library = None
|
||||||
|
local_library = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_spotify(self):
|
||||||
|
fetch_all(client_id, client_secret, work_dir)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_local():
|
||||||
|
update_local_library(local_library_path, work_dir)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_spotify_library():
|
||||||
|
parser = Parser()
|
||||||
|
|
||||||
|
library_parsed = parser.parse(
|
||||||
|
work_dir / "spotify" / "playlistTracks.json",
|
||||||
|
work_dir / "spotify" / "tracks.json",
|
||||||
|
work_dir / "spotify" / "top_tracks.json",
|
||||||
|
work_dir / "spotify" / "top_artists.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
LibrarySaver(library_parsed).save(work_dir / "spotify_library.json")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_local_library():
|
||||||
|
parser = LocalParser()
|
||||||
|
library_parsed = parser.parse(work_dir / "local" / "local_library.json")
|
||||||
|
LibrarySaver(library_parsed).save(work_dir / "local_library.json")
|
||||||
|
|
||||||
|
def load_libraries(self):
|
||||||
|
self.spotify_library = LibraryLoader().load(work_dir / "spotify_library.json")
|
||||||
|
self.local_library = LibraryLoader().load(work_dir / "local_library.json")
|
||||||
|
|
||||||
|
def map_local_to_spotify(self):
|
||||||
|
map_local_to_spotify(client_id, client_secret, self.local_library, self.spotify_library, work_dir)
|
||||||
|
|
||||||
|
def run(self, arg):
|
||||||
|
self.fetch_local()
|
||||||
|
# self.do_fetch_spotify()
|
||||||
|
|
||||||
|
self.parse_local_library()
|
||||||
|
self.parse_spotify_library()
|
||||||
|
|
||||||
|
self.load_libraries()
|
||||||
|
|
||||||
|
# self.map_local_to_spotify()
|
||||||
|
|
||||||
|
return True
|
||||||
86
main.py
86
main.py
|
|
@ -1,77 +1,17 @@
|
||||||
|
|
||||||
import cmd
|
import Flow
|
||||||
|
|
||||||
from Env import client_id, client_secret, local_library_path
|
|
||||||
from Env import work_dir
|
|
||||||
|
|
||||||
from src.LibrarySerializer import LibrarySaver, LibraryLoader
|
|
||||||
|
|
||||||
from src.spotify.SpotifyWebAPI import fetch_all
|
|
||||||
from src.spotify.ParseSpotify import Parser
|
|
||||||
|
|
||||||
from src.local.LocalLibraryIndexer import update_local_library
|
|
||||||
from src.local.ParseLocal import LocalParser
|
|
||||||
|
|
||||||
from src.MapLocalToSpotify import map_local_to_spotify
|
|
||||||
|
|
||||||
|
|
||||||
class Interpreter(cmd.Cmd):
|
|
||||||
intro = "Welcome to the interactive command loop. Type help or ? to list commands.\n"
|
|
||||||
prompt = "(kuw) "
|
|
||||||
|
|
||||||
spotify_library = None
|
|
||||||
local_library = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def do_fetch_spotify(self):
|
|
||||||
fetch_all(client_id, client_secret, work_dir)
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def do_fetch_local():
|
|
||||||
update_local_library(local_library_path, work_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def do_parse_spotify_library(self):
|
|
||||||
parser = Parser()
|
|
||||||
library_parsed = parser.parse(work_dir / "spotify" / "playlistTracks.json", work_dir / "spotify" / "tracks.json")
|
|
||||||
LibrarySaver(library_parsed).save(work_dir / "spotify_library.json")
|
|
||||||
|
|
||||||
def do_parse_local_library(self):
|
|
||||||
parser = LocalParser()
|
|
||||||
library_parsed = parser.parse(work_dir / "local" / "local_library.json")
|
|
||||||
LibrarySaver(library_parsed).save(work_dir / "local_library.json")
|
|
||||||
|
|
||||||
def do_load_libraries(self):
|
|
||||||
self.spotify_library = LibraryLoader().load(work_dir / "spotify_library.json")
|
|
||||||
self.local_library = LibraryLoader().load(work_dir / "local_library.json")
|
|
||||||
|
|
||||||
def do_map_local_to_spotify(self):
|
|
||||||
map_local_to_spotify(client_id, client_secret, self.local_library, self.spotify_library, work_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def do_run(self, arg):
|
|
||||||
# self.do_fetch_local()
|
|
||||||
# self.do_fetch_spotify()
|
|
||||||
|
|
||||||
# self.do_parse_local_library()
|
|
||||||
# self.do_parse_spotify_library()
|
|
||||||
|
|
||||||
self.do_load_libraries()
|
|
||||||
|
|
||||||
self.do_map_local_to_spotify()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def do_exit(self, arg):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
try:
|
flow = Flow.Flow()
|
||||||
Interpreter().do_run("")
|
|
||||||
# Interpreter().cmdloop()
|
|
||||||
|
|
||||||
except KeyboardInterrupt as kb:
|
# flow.fetch_spotify()
|
||||||
print("process terminated")
|
flow.fetch_local()
|
||||||
|
|
||||||
|
flow.parse_spotify_library()
|
||||||
|
flow.parse_local_library()
|
||||||
|
|
||||||
|
flow.load_libraries()
|
||||||
|
|
||||||
|
flow.map_local_to_spotify()
|
||||||
|
|
||||||
|
print("asd")
|
||||||
|
|
|
||||||
176
src/Library.py
176
src/Library.py
|
|
@ -1,159 +1,51 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import json
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Any
|
from typing import List, Optional, Any
|
||||||
|
|
||||||
|
class Artist:
|
||||||
|
def __init__(self):
|
||||||
|
self.id : str = None
|
||||||
|
self.title : str = None
|
||||||
|
self.tracks : List[Track] = []
|
||||||
|
|
||||||
# =========================
|
|
||||||
# Core models
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
class Track:
|
class Track:
|
||||||
def __init__(
|
def __init__(self):
|
||||||
self,
|
self.id : str = None
|
||||||
*,
|
self.title : str = None
|
||||||
id: str,
|
self.artists : List[Artist] = []
|
||||||
title: str,
|
self.album : Album = None
|
||||||
artists: List[str],
|
self.duration_ms : int = None
|
||||||
album: Optional[str] = None,
|
self.added_at : datetime = None
|
||||||
duration_ms: Optional[int] = None,
|
self.local_path : Path = None
|
||||||
added_at: Optional[datetime] = None,
|
|
||||||
spotify: Optional["SpotifyData"] = None,
|
|
||||||
local: Optional["LocalData"] = None,
|
|
||||||
):
|
|
||||||
self.id = id
|
|
||||||
self.title = title
|
|
||||||
self.artists = artists
|
|
||||||
self.album = album
|
|
||||||
self.duration_ms = duration_ms
|
|
||||||
self.added_at = added_at
|
|
||||||
self.spotify = spotify
|
|
||||||
self.local = local
|
|
||||||
|
|
||||||
|
|
||||||
class Playlist:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
name: str,
|
|
||||||
tracks: Optional[List[Track]] = None,
|
|
||||||
created_at: Optional[datetime] = None,
|
|
||||||
description: Optional[str] = None,
|
|
||||||
spotify: Optional["SpotifyData"] = None,
|
|
||||||
):
|
|
||||||
self.name = name
|
|
||||||
self.tracks = tracks or []
|
|
||||||
self.created_at = created_at
|
|
||||||
self.description = description
|
|
||||||
self.spotify = spotify
|
|
||||||
|
|
||||||
|
|
||||||
class Album:
|
class Album:
|
||||||
def __init__(
|
def __init__(self):
|
||||||
self,
|
self.id : str = None
|
||||||
*,
|
self.title : str = None
|
||||||
title: str,
|
self.release_date : datetime = None
|
||||||
release_date: Optional[str] = None,
|
self.tracks : List[Track] = []
|
||||||
tracks: Optional[List[Track]] = None,
|
self.artists : List[Artist] = []
|
||||||
spotify: Optional["SpotifyData"] = None,
|
|
||||||
):
|
|
||||||
self.title = title
|
|
||||||
self.release_date = release_date
|
|
||||||
self.tracks = tracks or []
|
|
||||||
self.spotify = spotify
|
|
||||||
|
|
||||||
|
|
||||||
class TrackList:
|
class Playlist:
|
||||||
def __init__(self, *, tracks: Optional[List[Track]] = None):
|
def __init__(self):
|
||||||
self.tracks = tracks or []
|
self.title : str = None
|
||||||
|
self.tracks : List[Track] = []
|
||||||
|
self.created_at : datetime = None
|
||||||
|
self.description : str = None
|
||||||
|
|
||||||
def total_duration_ms(self) -> int:
|
|
||||||
return sum(t.duration_ms or 0 for t in self.tracks)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# Spotify models
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
class SpotifyArtist:
|
|
||||||
def __init__(self, *, id: str, name: str, uri: str, url: str):
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.uri = uri
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyAlbum:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
id: str,
|
|
||||||
name: str,
|
|
||||||
release_date: str,
|
|
||||||
total_tracks: int,
|
|
||||||
uri: str,
|
|
||||||
url: str,
|
|
||||||
):
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.release_date = release_date
|
|
||||||
self.total_tracks = total_tracks
|
|
||||||
self.uri = uri
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyData:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
id: str,
|
|
||||||
uri: str,
|
|
||||||
url: str,
|
|
||||||
href: str,
|
|
||||||
file_path: Optional[Path] = None,
|
|
||||||
popularity: Optional[int] = None,
|
|
||||||
is_playable: Optional[bool] = None,
|
|
||||||
is_local: Optional[bool] = None,
|
|
||||||
artists: Optional[List[SpotifyArtist]] = None,
|
|
||||||
album: Optional[SpotifyAlbum] = None,
|
|
||||||
external_ids: Optional[dict[str, str]] = None,
|
|
||||||
):
|
|
||||||
self.id = id
|
|
||||||
self.uri = uri
|
|
||||||
self.url = url
|
|
||||||
self.href = href
|
|
||||||
self.file_path = file_path
|
|
||||||
self.popularity = popularity
|
|
||||||
self.is_playable = is_playable
|
|
||||||
self.is_local = is_local
|
|
||||||
self.artists = artists or []
|
|
||||||
self.album = album
|
|
||||||
self.external_ids = external_ids or {}
|
|
||||||
|
|
||||||
class LocalData:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
path: str,
|
|
||||||
):
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# Library container
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
class Library:
|
class Library:
|
||||||
def __init__(
|
def __init__(self):
|
||||||
self,
|
self.artists : List[Artist] = []
|
||||||
*,
|
self.playlists : List[Playlist] = []
|
||||||
playlists: Optional[List[Playlist]] = None,
|
self.tracks : List[Track] = []
|
||||||
tracks: Optional[List[Track]] = None,
|
self.albums : List[Album] = []
|
||||||
tracklists: Optional[List[TrackList]] = None,
|
self.liked_tracks : List[Track] = []
|
||||||
albums: Optional[List[Album]] = None,
|
self.top_tracks : List[Track]= []
|
||||||
):
|
self.top_artists : List[Artist] = []
|
||||||
self.playlists = playlists or []
|
|
||||||
self.tracks = tracks or []
|
|
||||||
self.tracklists = tracklists or []
|
|
||||||
self.albums = albums or []
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,174 +26,112 @@ class LibrarySaver:
|
||||||
def __init__(self, library: Library):
|
def __init__(self, library: Library):
|
||||||
self.library = library
|
self.library = library
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _artist_to_dict(artist: Artist) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": artist.id,
|
||||||
|
"title": artist.title,
|
||||||
|
"tracks": [track.id for track in artist.tracks],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _track_to_dict(track: Track) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": track.id,
|
||||||
|
"title": track.title,
|
||||||
|
"artists": [artist.id for artist in track.artists],
|
||||||
|
"album": track.album.id,
|
||||||
|
"duration_ms": track.duration_ms,
|
||||||
|
"added_at": _dt_to_str(track.added_at),
|
||||||
|
"local_path": track.local_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _playlist_to_dict(playlist: Playlist) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"title": playlist.title,
|
||||||
|
"tracks": [track.id for track in playlist.tracks],
|
||||||
|
"created_at": _dt_to_str(playlist.created_at),
|
||||||
|
"description": playlist.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _album_to_dict(album: Album) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": album.id,
|
||||||
|
"title": album.title,
|
||||||
|
"release_date": album.release_date,
|
||||||
|
"tracks": [track.id for track in album.tracks],
|
||||||
|
"artists": [artist.id for artist in album.artists]
|
||||||
|
}
|
||||||
|
|
||||||
def save(self, path: Path) -> None:
|
def save(self, path: Path) -> None:
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"tracks": [self._track_to_dict(track) for track in self.library.tracks],
|
||||||
|
"albums": [self._album_to_dict(album) for album in self.library.albums],
|
||||||
|
"artists": [self._artist_to_dict(artist) for artist in self.library.artists],
|
||||||
|
"playlists": [self._playlist_to_dict(playlist) for playlist in self.library.playlists],
|
||||||
|
"top_tracks": [track.id for track in self.library.top_tracks],
|
||||||
|
"top_artists": [artist.id for artist in self.library.top_artists],
|
||||||
|
"liked_tracks": [track.id for track in self.library.liked_tracks],
|
||||||
|
}
|
||||||
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with path.open("w", encoding="utf-8") as f:
|
with path.open("w", encoding="utf-8") as f:
|
||||||
json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
|
json.dump(data, 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:
|
class LibraryLoader:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Map of track id -> Track object
|
|
||||||
self.track_map: dict[str, Track] = {}
|
self.track_map: dict[str, Track] = {}
|
||||||
|
self.album_map: dict[str, Album] = {}
|
||||||
|
self.artist_map: dict[str, Artist] = {}
|
||||||
|
|
||||||
|
def load_track(self, data: dict[str, Any]) -> Track:
|
||||||
|
track = self.track_map.get(data.get("id"), Track())
|
||||||
|
track.id = data.get("id")
|
||||||
|
track.title = data.get("title")
|
||||||
|
track.duration_ms = data.get("duration_ms")
|
||||||
|
track.added_at = _str_to_dt(data.get("added_at"))
|
||||||
|
track.local_path = data.get("local_path")
|
||||||
|
track.artists = [self.artist_map.get(artist_id, Artist()) for artist_id in data.get("artists")]
|
||||||
|
track.album = self.album_map.get(data.get("album"), Album())
|
||||||
|
|
||||||
|
self.track_map[track.id] = track
|
||||||
|
return track
|
||||||
|
|
||||||
|
def load_artist(self, data: dict[str, Any]):
|
||||||
|
artist = self.artist_map.get(data.get("id"), Artist())
|
||||||
|
artist.id = data.get("id")
|
||||||
|
artist.title = data.get("title")
|
||||||
|
artist.tracks = [self.track_map.get(track_id, Track()) for track_id in data.get("tracks")]
|
||||||
|
|
||||||
|
self.artist_map[artist.id] = artist
|
||||||
|
return artist
|
||||||
|
|
||||||
|
def load_album(self, data: dict[str, Any]):
|
||||||
|
album = self.album_map.get(data.get("id"), Album())
|
||||||
|
album.id = data.get("id")
|
||||||
|
album.tracks = [self.track_map.get(track_id, Track()) for track_id in data.get("tracks")]
|
||||||
|
album.title = data.get("title")
|
||||||
|
album.artists = [self.artist_map.get(artist_id, Artist()) for artist_id in data.get("artists")]
|
||||||
|
|
||||||
|
self.album_map[album.id] = album
|
||||||
|
return album
|
||||||
|
|
||||||
def load(self, path: Path) -> Library:
|
def load(self, path: Path) -> Library:
|
||||||
with path.open("r", encoding="utf-8") as f:
|
with path.open("r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Load tracks first
|
library = Library()
|
||||||
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
|
library.tracks = [self.load_track(track) for track in data.get("tracks")]
|
||||||
playlists = []
|
library.albums = [self.load_album(album) for album in data.get("albums")]
|
||||||
for p in data.get("playlists", []):
|
library.artists = [self.load_artist(artist) for artist in data.get("artists")]
|
||||||
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(
|
library.top_artists = [self.artist_map[artist] for artist in data.get("top_artists")]
|
||||||
name=p["name"],
|
library.top_tracks = [self.track_map[track] for track in data.get("top_tracks")]
|
||||||
tracks=pl_tracks,
|
library.liked_tracks = [self.track_map[track] for track in data.get("liked_tracks")]
|
||||||
created_at=_str_to_dt(p.get("created_at")),
|
|
||||||
description=p.get("description"),
|
|
||||||
spotify=self._spotify_from_dict(p.get("spotify")),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
return library
|
||||||
# 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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,17 @@ def map_local_to_spotify(client_id, client_secret, local_lib : Library, spotify_
|
||||||
with tqdm(total=total, desc='Searching local songs on spotify') as pbar:
|
with tqdm(total=total, desc='Searching local songs on spotify') as pbar:
|
||||||
for track in local_lib.tracks:
|
for track in local_lib.tracks:
|
||||||
|
|
||||||
parts = [track.title, track.artists[0] if len(track.artists) else "", track.album]
|
parts = [track.title, track.artists[0].title if len(track.artists) else "", track.album.title]
|
||||||
search_pattern = " ".join(p for p in parts if p)
|
search_pattern = " ".join(p for p in parts if p)
|
||||||
|
|
||||||
if search_pattern == "":
|
if search_pattern == "":
|
||||||
print(f"cannot generate search pattern for the track - {track.local.path}")
|
print(f"cannot generate search pattern for the track - {track.local_path}")
|
||||||
pbar.update(1)
|
pbar.update(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found_tracks = find_song(search_pattern)[0:10]
|
found_tracks = find_song(search_pattern)[0:10]
|
||||||
found_tracks_map[track.id] = found_tracks
|
found_tracks_map[track.id] = found_tracks
|
||||||
search_log.append({"path" : track.local.path, "search_pattern": search_pattern, "result" : found_tracks_map[track.id]})
|
search_log.append({"path" : track.local_path, "search_pattern": search_pattern, "result" : found_tracks_map[track.id]})
|
||||||
pbar.update(1)
|
pbar.update(1)
|
||||||
|
|
||||||
save_data(search_log, "spotify_search_log")
|
save_data(search_log, "spotify_search_log")
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,139 @@
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Iterable
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from src.Library import *
|
from src.Library import *
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
PROTECTED_ARTISTS = {
|
||||||
|
"Tyler, The Creator",
|
||||||
|
"The Good, The Bad & The Queen",
|
||||||
|
}
|
||||||
|
|
||||||
|
def split_artists(artist_tag: str) -> list[str]:
|
||||||
|
if not artist_tag:
|
||||||
|
return []
|
||||||
|
|
||||||
|
placeholder_comma = "§COMMA§"
|
||||||
|
placeholder_amp = "§AMP§"
|
||||||
|
|
||||||
|
protected_map = {}
|
||||||
|
|
||||||
|
# Protect known artist names
|
||||||
|
for artist in PROTECTED_ARTISTS:
|
||||||
|
protected = (
|
||||||
|
artist
|
||||||
|
.replace(",", placeholder_comma)
|
||||||
|
.replace("&", placeholder_amp)
|
||||||
|
)
|
||||||
|
protected_map[protected] = artist
|
||||||
|
artist_tag = artist_tag.replace(artist, protected)
|
||||||
|
|
||||||
|
# Split remaining separators
|
||||||
|
parts = re.split(r"[,&]", artist_tag)
|
||||||
|
|
||||||
|
# Restore protected names and trim spaces
|
||||||
|
result = []
|
||||||
|
for part in parts:
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
|
||||||
|
part = (
|
||||||
|
part
|
||||||
|
.replace(placeholder_comma, ",")
|
||||||
|
.replace(placeholder_amp, "&")
|
||||||
|
)
|
||||||
|
|
||||||
|
result.append(part)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class LocalParser:
|
class LocalParser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.track_map: dict[str, Track] = {}
|
||||||
|
self.artist_map: dict[str, Artist] = {}
|
||||||
def parse_track_item(self, id, track: dict[str, Any]) -> Track:
|
self.album_map: dict[str, Album] = {}
|
||||||
track = Track(
|
self.library = Library()
|
||||||
title=track["name"],
|
|
||||||
artists=[track.get("artist", [])],
|
|
||||||
album=track["album"] if track.get("album") else None,
|
|
||||||
duration_ms=None,
|
|
||||||
added_at=None,
|
|
||||||
spotify=None,
|
|
||||||
id=id,
|
|
||||||
local=LocalData(path=track["path"])
|
|
||||||
)
|
|
||||||
return track
|
|
||||||
|
|
||||||
|
|
||||||
def parse_all_tracks(self, items: dict[str, list[dict[str, Any]]]) -> list[Track]:
|
def parse_artists(self, data: str) -> List[Artist]:
|
||||||
return [self.parse_track_item(id, track) for id, track in items.items()]
|
if not data:
|
||||||
|
data = "unknown"
|
||||||
|
|
||||||
def build_library_from_tracks(self, tracks_content_json: dict[str, list[dict[str, Any]]]) -> Library:
|
data = split_artists(data)
|
||||||
library = Library()
|
|
||||||
|
|
||||||
tracks = self.parse_all_tracks(tracks_content_json)
|
artists = []
|
||||||
library.tracklists.append(TrackList(tracks=list(tracks)))
|
|
||||||
library.tracks.extend(tracks)
|
for artist_data in data:
|
||||||
|
name = artist_data
|
||||||
|
artist_id = name
|
||||||
|
|
||||||
|
if artist_id in self.artist_map:
|
||||||
|
artists.append(self.artist_map[artist_id])
|
||||||
|
continue
|
||||||
|
|
||||||
|
artist = Artist()
|
||||||
|
artist.id = artist_id
|
||||||
|
artist.title = name
|
||||||
|
|
||||||
|
self.artist_map[artist_id] = artist
|
||||||
|
self.library.artists.append(artist)
|
||||||
|
|
||||||
|
artists.append(artist)
|
||||||
|
|
||||||
|
return artists
|
||||||
|
|
||||||
|
def parse_album(self, data: str) -> Album:
|
||||||
|
if not data:
|
||||||
|
data = "unknown"
|
||||||
|
|
||||||
|
name = data
|
||||||
|
album_id = name
|
||||||
|
|
||||||
|
if album_id in self.album_map:
|
||||||
|
return self.album_map[album_id]
|
||||||
|
|
||||||
|
album = Album()
|
||||||
|
album.id = album_id
|
||||||
|
album.title = name
|
||||||
|
|
||||||
|
self.album_map[album_id] = album
|
||||||
|
self.library.albums.append(album)
|
||||||
|
|
||||||
|
return album
|
||||||
|
|
||||||
|
def add_tracks_to_albums_and_artists(self):
|
||||||
|
for track_id, track in self.track_map.items():
|
||||||
|
for artist in track.artists:
|
||||||
|
if track not in artist.tracks:
|
||||||
|
artist.tracks.append(track)
|
||||||
|
|
||||||
|
if track not in track.album.tracks:
|
||||||
|
track.album.tracks.append(track)
|
||||||
|
|
||||||
return library
|
|
||||||
|
|
||||||
def parse(self, tracks_file: Path) -> Library:
|
def parse(self, tracks_file: Path) -> Library:
|
||||||
tracks_content = json.loads(tracks_file.read_text())
|
tracks_content = json.loads(tracks_file.read_text())
|
||||||
return self.build_library_from_tracks(tracks_content)
|
|
||||||
|
for track_id, data in tracks_content.items():
|
||||||
|
track = Track()
|
||||||
|
|
||||||
|
track.id = track_id
|
||||||
|
track.title = data.get("name")
|
||||||
|
track.artists = self.parse_artists(data.get("artist"))
|
||||||
|
track.album = self.parse_album(data.get("album"))
|
||||||
|
track.local_path = data.get("path")
|
||||||
|
|
||||||
|
self.track_map[track.id] = track
|
||||||
|
self.library.tracks.append(track)
|
||||||
|
|
||||||
|
self.add_tracks_to_albums_and_artists()
|
||||||
|
|
||||||
|
return self.library
|
||||||
|
|
||||||
|
|
||||||
|
def parse_track_item(self, id, track: dict[str, Any]) -> Track:
|
||||||
|
|
||||||
|
return track
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from datetime import datetime
|
from src.Library import *
|
||||||
|
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from src.Library import *
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def get_artists_str(artists_pack):
|
def get_artists_str(artists_pack):
|
||||||
|
|
@ -11,129 +12,117 @@ def get_artists_str(artists_pack):
|
||||||
artists = " ".join(track_artists)
|
artists = " ".join(track_artists)
|
||||||
return artists
|
return artists
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# state: Spotify track ID -> Track object
|
|
||||||
self.track_map: dict[str, Track] = {}
|
self.track_map: dict[str, Track] = {}
|
||||||
|
self.artist_map: dict[str, Artist] = {}
|
||||||
|
self.album_map: dict[str, Album] = {}
|
||||||
|
self.library = Library()
|
||||||
|
|
||||||
# -------------------------
|
def parse(self, playlists: Path, tracks: Path, top_tracks: Path, top_artists: Path) -> Library:
|
||||||
# Low-level helpers
|
self.library = Library()
|
||||||
# -------------------------
|
|
||||||
@staticmethod
|
|
||||||
def _parse_spotify_datetime(value: str | None) -> datetime | None:
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
||||||
|
|
||||||
@staticmethod
|
self.parse_tracks(json.loads(tracks.read_text()))
|
||||||
def _parse_spotify_artist(a: dict[str, Any]) -> SpotifyArtist:
|
self.parse_playlists(json.loads(playlists.read_text()))
|
||||||
return SpotifyArtist(
|
|
||||||
id=a["id"],
|
|
||||||
name=a["name"],
|
|
||||||
uri=a["uri"],
|
|
||||||
url=a["external_urls"]["spotify"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
self.parse_top_artists(json.loads(top_artists.read_text()))
|
||||||
def _parse_spotify_album(a: dict[str, Any]) -> SpotifyAlbum:
|
self.parse_top_tracks(json.loads(top_tracks.read_text()))
|
||||||
return SpotifyAlbum(
|
|
||||||
id=a["id"],
|
|
||||||
name=a["name"],
|
|
||||||
release_date=a.get("release_date"),
|
|
||||||
total_tracks=a.get("total_tracks", 0),
|
|
||||||
uri=a["uri"],
|
|
||||||
url=a["external_urls"]["spotify"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _parse_spotify_data(self, track_json: dict[str, Any]) -> SpotifyData:
|
self.add_tracks_to_albums_and_artists()
|
||||||
return SpotifyData(
|
|
||||||
id=track_json["id"],
|
|
||||||
uri=track_json["uri"],
|
|
||||||
url=track_json["external_urls"]["spotify"],
|
|
||||||
href=track_json["href"],
|
|
||||||
popularity=track_json.get("popularity"),
|
|
||||||
is_playable=track_json.get("is_playable"),
|
|
||||||
is_local=track_json.get("is_local"),
|
|
||||||
external_ids=track_json.get("external_ids", {}),
|
|
||||||
artists=[self._parse_spotify_artist(a) for a in track_json.get("artists", [])],
|
|
||||||
album=self._parse_spotify_album(track_json["album"])
|
|
||||||
if track_json.get("album")
|
|
||||||
else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# -------------------------
|
return self.library
|
||||||
# Track parsers
|
|
||||||
# -------------------------
|
def add_tracks_to_albums_and_artists(self):
|
||||||
def parse_track_item(self, item: dict[str, Any]) -> Track:
|
for track_id, track in self.track_map.items():
|
||||||
"""
|
for artist in track.artists:
|
||||||
Parses a single track item and returns Track object.
|
if track not in artist.tracks:
|
||||||
Reuses existing Track from track_map if already parsed.
|
artist.tracks.append(track)
|
||||||
"""
|
|
||||||
t = item["track"]
|
if track not in track.album.tracks:
|
||||||
track_id = t["id"]
|
track.album.tracks.append(track)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_playlists(self, data):
|
||||||
|
for name, tracks in data.items():
|
||||||
|
playlist = Playlist()
|
||||||
|
playlist.title = name
|
||||||
|
for track in tracks:
|
||||||
|
track_data = track.get("track")
|
||||||
|
if track_data:
|
||||||
|
track_id = track_data.get("id")
|
||||||
|
playlist.tracks.append(self.parse_track(track_data, track.get("added_at")))
|
||||||
|
|
||||||
|
self.library.playlists.append(playlist)
|
||||||
|
|
||||||
|
def parse_top_tracks(self, data):
|
||||||
|
for track in data:
|
||||||
|
if track.get("id") in self.track_map:
|
||||||
|
self.library.top_tracks.append(self.track_map.get(track.get("id")))
|
||||||
|
|
||||||
|
def parse_top_artists(self, data):
|
||||||
|
for artist in data:
|
||||||
|
if artist.get("id") in self.artist_map:
|
||||||
|
self.library.top_artists.append(self.artist_map.get(artist.get("id")))
|
||||||
|
|
||||||
|
def parse_tracks(self, data):
|
||||||
|
for track in data:
|
||||||
|
self.parse_track(track.get("track"), track.get("added_at"))
|
||||||
|
|
||||||
|
for track in data:
|
||||||
|
self.library.liked_tracks.append(self.track_map.get(track.get("track").get("id")))
|
||||||
|
|
||||||
|
def parse_track(self, data: dict[str, Any], added_time : datetime) -> Track:
|
||||||
|
track_id = data.get("id")
|
||||||
|
|
||||||
# return existing track if already parsed
|
|
||||||
if track_id in self.track_map:
|
if track_id in self.track_map:
|
||||||
return self.track_map[track_id]
|
return self.track_map[track_id]
|
||||||
|
|
||||||
track = Track(
|
track = Track()
|
||||||
title=t["name"],
|
|
||||||
artists=[a["name"] for a in t.get("artists", [])],
|
|
||||||
album=t["album"]["name"] if t.get("album") else None,
|
|
||||||
duration_ms=t.get("duration_ms"),
|
|
||||||
added_at=self._parse_spotify_datetime(item.get("added_at")),
|
|
||||||
spotify=self._parse_spotify_data(t),
|
|
||||||
id=self._parse_spotify_data(t).id
|
|
||||||
)
|
|
||||||
|
|
||||||
self.track_map[track_id] = track
|
self.track_map[track_id] = track
|
||||||
|
|
||||||
|
track.id = track_id
|
||||||
|
track.title = data.get("name")
|
||||||
|
track.artists = [self.parse_artist(artist) for artist in data.get("artists")]
|
||||||
|
track.album = self.parse_album(data.get("album"))
|
||||||
|
track.duration_ms = data.get("duration_ms")
|
||||||
|
|
||||||
|
self.library.tracks.append(track)
|
||||||
|
|
||||||
return track
|
return track
|
||||||
|
|
||||||
def parse_track_items(self, items: Iterable[dict[str, Any]]) -> list[Track]:
|
def parse_album(self, data):
|
||||||
return [self.parse_track_item(i) for i in items if i.get("track")]
|
album_id = data.get("id")
|
||||||
|
|
||||||
# -------------------------
|
if album_id in self.album_map:
|
||||||
# Playlist parsers
|
return self.album_map[album_id]
|
||||||
# -------------------------
|
|
||||||
def parse_playlist_content(self, playlist_name: str, items: list[dict[str, Any]]) -> Playlist:
|
|
||||||
tracks = self.parse_track_items(items)
|
|
||||||
return Playlist(name=playlist_name, tracks=tracks)
|
|
||||||
|
|
||||||
def parse_all_playlist_content(self, data: dict[str, list[dict[str, Any]]]) -> list[Playlist]:
|
album = Album()
|
||||||
playlists: list[Playlist] = []
|
|
||||||
for name, items in data.items():
|
|
||||||
playlists.append(self.parse_playlist_content(name, items))
|
|
||||||
return playlists
|
|
||||||
|
|
||||||
def parse_all_tracks(self, items: dict[str, list[dict[str, Any]]]) -> list[Track]:
|
self.album_map[album_id] = album
|
||||||
return [self.parse_track_item(i) for i in items]
|
|
||||||
|
|
||||||
# -------------------------
|
album.id = data.get("id")
|
||||||
# Library builder
|
album.title = data.get("name")
|
||||||
# -------------------------
|
album.artists = [self.parse_artist(artist) for artist in data.get("artists")]
|
||||||
def build_library_from_playlist_content(
|
|
||||||
self, playlist_content_json: dict[str, list[dict[str, Any]]],
|
|
||||||
tracks_content_json: dict[str, list[dict[str, Any]]]
|
|
||||||
) -> Library:
|
|
||||||
library = Library()
|
|
||||||
|
|
||||||
self.parse_all_tracks(tracks_content_json)
|
self.library.albums.append(album)
|
||||||
|
|
||||||
library.tracklists.append(TrackList(tracks=list((self.track_map.values()))))
|
return album
|
||||||
|
|
||||||
library.playlists = self.parse_all_playlist_content(playlist_content_json)
|
def parse_artist(self, data: dict[str, Any]) -> Artist:
|
||||||
library.tracks.extend(self.track_map.values())
|
artist_id = data.get("id")
|
||||||
|
|
||||||
return library
|
if artist_id in self.artist_map:
|
||||||
|
return self.artist_map[artist_id]
|
||||||
|
|
||||||
# -------------------------
|
artist = Artist()
|
||||||
# Convenience method
|
|
||||||
# -------------------------
|
self.artist_map[artist_id] = artist
|
||||||
def parse(
|
|
||||||
self,
|
artist.id = data.get("id")
|
||||||
playlist_content_file: Path,
|
artist.title = data.get("name")
|
||||||
tracks_file: Path,
|
|
||||||
) -> Library:
|
self.library.artists.append(artist)
|
||||||
playlist_content = json.loads(playlist_content_file.read_text())
|
|
||||||
tracks_content = json.loads(tracks_file.read_text())
|
return artist
|
||||||
return self.build_library_from_playlist_content(playlist_content, tracks_content)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue