My music library. see library.tsv and any folder for playlists. synced from spotify via ugly autohotkey scripts
1# /// script
2# requires-python = ">=3.12"
3# dependencies = [
4# "rich",
5# "spotipy",
6# "PyYAML",
7# ]
8# ///
9
10#!/usr/bin/env python3
11
12from typing import Literal
13from spotipy import Spotify, SpotifyOAuth, MemoryCacheHandler
14from subprocess import run
15from pathlib import Path
16import re
17import sys
18import json
19import yaml
20from rich import print
21from rich.console import Console
22from rich.table import Table
23
24here = Path(__file__).parent
25
26tokens = json.loads((here / "secrets.json").read_text())
27
28gitignore = Path(".gitignore")
29
30# ensure secrets.json is gitignored
31if not gitignore.exists() or "\nsecrets.json\n" not in gitignore.read_text():
32 gitignore.write_text(
33 (gitignore.read_text() if gitignore.exists() else "") + "\nsecrets.json\n",
34 encoding="utf8",
35 )
36
37
38# Initial setup
39spotify = Spotify(
40 auth_manager=SpotifyOAuth(
41 scope=["user-follow-modify", "user-library-read"],
42 client_id=tokens["id"],
43 client_secret=tokens["secret"],
44 redirect_uri="http://localhost:8080",
45 cache_handler=MemoryCacheHandler()
46 )
47)
48
49
50def sync_tsv_file(results: dict[Literal["items"], list], target: Path):
51 # Fix quoting
52 def fix_quoting(tracks):
53 return {re.sub(r'"([^"]+)"', r"“\1”", track) for track in tracks}
54
55 if not target.exists():
56 print(f"⋆𐙚₊˚⊹♡ Creating [bold][magenta]{target}[reset] ⋆౨ৎ˚⟡˖ ࣪")
57 target.write_text("Artist\tTitle\n", encoding="utf8")
58
59 # Get whole library
60 lib = list(target.read_text("utf8").splitlines())
61 tracks, header = set(lib[1:]), lib[0]
62 tracks = fix_quoting(tracks)
63
64 # Boil them down to (artists, title, album)
65 new_tracks = (
66 fix_quoting(
67 {
68 "\t".join(
69 [
70 ", ".join(a["name"] for a in t["track"].get("artists", [])),
71 t["track"].get("name", None),
72 # t["track"]["album"]["name"],
73 ]
74 )
75 for t in results["items"]
76 }
77 )
78 - tracks
79 )
80
81 if new_tracks:
82 print(
83 f"⋆𐙚₊˚⊹♡ I got [bold][cyan]{len(new_tracks)}[reset] new tracks for ya in [bold][magenta]{target}[reset] 💖 ⋆౨ৎ˚⟡˖ ࣪"
84 )
85
86 table = Table.grid(padding=(0, 2))
87 table.add_column(style="bold dim")
88 table.add_column()
89 for new_track in new_tracks:
90 artist, title = new_track.split("\t")
91 table.add_row(artist, title)
92 Console().print(table)
93 else:
94 print(
95 f"⋆𐙚₊˚⊹♡ Nyathing new to add to [magenta][bold]{target}[reset]. Go listen to sum new music :3 ⋆౨ৎ˚⟡˖ ࣪"
96 )
97 return
98
99 print("")
100
101 # Add our tracks
102 tracks |= new_tracks
103 # Sort
104 tracks = list(tracks)
105 tracks.sort()
106 # Write back library
107 target.write_text("\n".join([header] + tracks), encoding="utf8")
108
109 run(["git", "add", target], capture_output=True)
110
111
112# Get playlists defined on Spotify by user
113playlists_resp = spotify.current_user_playlists()
114playlists = playlists_resp["items"]
115while playlists_resp["next"]:
116 playlists_resp = spotify.next(playlists_resp)
117 playlists.extend(playlists_resp["items"])
118
119# Store IDs of playlists we have to autocreate
120autocreate_playlists = set(
121 [
122 playlist["external_urls"]["spotify"]
123 for playlist in playlists
124 if playlist["owner"]["id"] == spotify.current_user()["id"]
125 ]
126)
127
128
129# Get tracks from API
130results = spotify.current_user_saved_tracks()
131
132sync_tsv_file(results, here / "library.tsv")
133
134for playlist_definition_file in here.glob("**/autofill.yaml"):
135 definition = yaml.safe_load(playlist_definition_file.read_text())
136 if not definition.get("from", "").startswith("https://open.spotify.com/playlist/"):
137 continue
138
139 autocreate_playlists.discard(definition["from"])
140
141 results = spotify.playlist_tracks(
142 definition["from"],
143 limit=100,
144 )
145 tracks = results["items"]
146 get_all = not Path("tracklist.tsv").exists()
147 get_all = True
148 while get_all and results["next"]:
149 results = spotify.next(results)
150 tracks.extend(results["items"])
151
152 sync_tsv_file(
153 {"items": tracks},
154 playlist_definition_file.parent / "tracklist.tsv",
155 )
156
157# Create playlists we have to create
158for spotifyurl in autocreate_playlists:
159 name = next(
160 playlist["name"]
161 for playlist in playlists
162 if playlist["external_urls"]["spotify"] == spotifyurl
163 )
164 print(f"⋆𐙚₊˚⊹♡ Creating playlist [bold][magenta]{name}[reset] ⋆౨ৎ˚⟡˖ ࣪")
165 try:
166 Path(here, name).mkdir(exist_ok=True, parents=True)
167 Path(here, name, "autofill.yaml").write_text(
168 f"from: {spotifyurl}", encoding="utf8"
169 )
170 run(["git", "add", str(Path(here, name))], capture_output=True)
171 except Exception as e:
172 print(f"\tCouldn't create playlist: {e}")
173
174
175# Git add commti and push
176print("⋆𐙚₊˚⊹♡ Beaming up to github ⋆౨ৎ˚⟡˖ ࣪")
177run(["git", "commit", "-m", "update"], capture_output=True)
178run(["git", "pull", "--autostash"], capture_output=True)
179run(["git", "push"], capture_output=True)