tunewrangler 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tunewrangler/__init__.py +32 -0
- tunewrangler/__main__.py +4 -0
- tunewrangler/cli.py +415 -0
- tunewrangler/errors.py +22 -0
- tunewrangler/last_fm_client.py +68 -0
- tunewrangler/spotify_client.py +518 -0
- tunewrangler/tidal_api.py +1443 -0
- tunewrangler/tidal_client.py +480 -0
- tunewrangler-1.0.0.dist-info/METADATA +148 -0
- tunewrangler-1.0.0.dist-info/RECORD +14 -0
- tunewrangler-1.0.0.dist-info/WHEEL +5 -0
- tunewrangler-1.0.0.dist-info/entry_points.txt +2 -0
- tunewrangler-1.0.0.dist-info/licenses/LICENSE +21 -0
- tunewrangler-1.0.0.dist-info/top_level.txt +1 -0
tunewrangler/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from platformdirs import user_data_dir
|
|
4
|
+
|
|
5
|
+
APP_NAME = "TuneWrangler"
|
|
6
|
+
APP_AUTHOR = "Alexandre Chabot-Leclerc"
|
|
7
|
+
|
|
8
|
+
SPOTIFY_USERNAME = "example-spotify-user"
|
|
9
|
+
LASTFM_USERNAME = "example-lastfm-user"
|
|
10
|
+
|
|
11
|
+
HERE_DIR = Path(__file__).parent.absolute()
|
|
12
|
+
DATA_DIR = Path(user_data_dir(APP_NAME, APP_AUTHOR))
|
|
13
|
+
|
|
14
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
TRACK_PLAYLISTS = {
|
|
17
|
+
2021: "YOUR_PLAYLIST_01_ID",
|
|
18
|
+
2022: "YOUR_PLAYLIST_02_ID",
|
|
19
|
+
2023: "YOUR_PLAYLIST_03_ID",
|
|
20
|
+
2024: "YOUR_PLAYLIST_04_ID",
|
|
21
|
+
2025: "YOUR_PLAYLIST_05_ID",
|
|
22
|
+
}
|
|
23
|
+
ALBUM_PLAYLIST = {
|
|
24
|
+
2021: "YOUR_PLAYLIST_06_ID",
|
|
25
|
+
2022: "YOUR_PLAYLIST_07_ID",
|
|
26
|
+
2023: "YOUR_PLAYLIST_08_ID",
|
|
27
|
+
2024: "YOUR_PLAYLIST_09_ID",
|
|
28
|
+
2025: "YOUR_PLAYLIST_10_ID",
|
|
29
|
+
}
|
|
30
|
+
RELEASE_RADAR_PLAYLIST = "YOUR_PLAYLIST_11_ID"
|
|
31
|
+
RELEASE_SINGLES_PLAYLIST = "YOUR_PLAYLIST_12_ID"
|
|
32
|
+
RELEASE_ALBUMS_PLAYLIST = "YOUR_PLAYLIST_13_ID"
|
tunewrangler/__main__.py
ADDED
tunewrangler/cli.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automation to save tracks or albums in Spotify.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from getpass import getpass
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import keyring
|
|
14
|
+
|
|
15
|
+
from tunewrangler import HERE_DIR
|
|
16
|
+
from tunewrangler.tidal_api import TidalRateLimitError, TidalUnsupportedFeatureError
|
|
17
|
+
from tunewrangler.tidal_client import TidalClient
|
|
18
|
+
from tunewrangler.spotify_client import SpotifyClient
|
|
19
|
+
from tunewrangler.errors import (
|
|
20
|
+
AlbumIsSingleError,
|
|
21
|
+
PlaylistExistsError,
|
|
22
|
+
NotPlayingError,
|
|
23
|
+
TrackNotFoundError,
|
|
24
|
+
)
|
|
25
|
+
from tunewrangler.last_fm_client import LastFmClient
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_tidal_rate_limit(func):
|
|
31
|
+
@wraps(func)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
try:
|
|
34
|
+
return func(*args, **kwargs)
|
|
35
|
+
except TidalRateLimitError as error:
|
|
36
|
+
raise click.ClickException(str(error)) from error
|
|
37
|
+
except TrackNotFoundError as error:
|
|
38
|
+
raise click.ClickException(str(error)) from error
|
|
39
|
+
except TidalUnsupportedFeatureError as error:
|
|
40
|
+
raise click.ClickException(str(error)) from error
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def notify(title, message, subtitle=""):
|
|
46
|
+
notificator_path = (
|
|
47
|
+
HERE_DIR.parent
|
|
48
|
+
/ "TuneWranglerNotificator.app"
|
|
49
|
+
/ "Contents"
|
|
50
|
+
/ "Resources"
|
|
51
|
+
/ "Scripts"
|
|
52
|
+
/ "notificator"
|
|
53
|
+
)
|
|
54
|
+
if not shutil.which(notificator_path):
|
|
55
|
+
logger.error("Notification script not found.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
cmd = [
|
|
59
|
+
notificator_path,
|
|
60
|
+
"--title",
|
|
61
|
+
title,
|
|
62
|
+
"--subtitle",
|
|
63
|
+
subtitle,
|
|
64
|
+
"--message",
|
|
65
|
+
message,
|
|
66
|
+
]
|
|
67
|
+
subprocess.run(cmd)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Client:
|
|
71
|
+
def __init__(self, music_client, lastfm_client):
|
|
72
|
+
self.music_client = music_client
|
|
73
|
+
self.lastfm_client = lastfm_client
|
|
74
|
+
|
|
75
|
+
def save_current_track(self):
|
|
76
|
+
track_info = self.music_client.save_current_track()
|
|
77
|
+
if track_info is None:
|
|
78
|
+
notify("Oops!", "Something went wrong. Nothing is playing.")
|
|
79
|
+
else:
|
|
80
|
+
artist_name, track_name, track_id = track_info
|
|
81
|
+
self.music_client.save_track_to_weekly_playlist(track_id)
|
|
82
|
+
self.lastfm_client.save_track(artist_name, track_name)
|
|
83
|
+
|
|
84
|
+
# Success, don't notify
|
|
85
|
+
# notify("🎶 Saved Track", f"Saved track {artist_name} - {track_name}")
|
|
86
|
+
|
|
87
|
+
def save_current_album(self):
|
|
88
|
+
artist_name, album_name = self.music_client.save_current_album()
|
|
89
|
+
# success, don't notify
|
|
90
|
+
# notify("💿 Saved Album", f"Saved album {artist_name} - {album_name}")
|
|
91
|
+
|
|
92
|
+
def favorite_current_track(self):
|
|
93
|
+
self.music_client.favorite_current_track()
|
|
94
|
+
|
|
95
|
+
def favorite_current_album(self):
|
|
96
|
+
self.music_client.favorite_current_album()
|
|
97
|
+
|
|
98
|
+
def save_album_of_current_track_in_new_playlist(self):
|
|
99
|
+
try:
|
|
100
|
+
(
|
|
101
|
+
artist_name,
|
|
102
|
+
album_name,
|
|
103
|
+
) = self.music_client.save_album_of_current_track_in_new_playlist()
|
|
104
|
+
except NotPlayingError:
|
|
105
|
+
notify("Nothing Playing", "⛔️🎵")
|
|
106
|
+
notify(
|
|
107
|
+
"❌ Album is a Single",
|
|
108
|
+
f"This album wasn't saved because it's a single.",
|
|
109
|
+
)
|
|
110
|
+
except PlaylistExistsError:
|
|
111
|
+
notify(
|
|
112
|
+
"❌ Album already exists", f"There's already a playlist with that name."
|
|
113
|
+
)
|
|
114
|
+
except TrackNotFoundError:
|
|
115
|
+
notify("❌ Track Not Found", f"Could not find a matching track in Tidal.")
|
|
116
|
+
else:
|
|
117
|
+
# Success, don't notify
|
|
118
|
+
# notify(
|
|
119
|
+
# "✅ Saved New Playlist",
|
|
120
|
+
# f"Saved new playlist {artist_name} - {album_name}.",
|
|
121
|
+
# )
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@click.group()
|
|
126
|
+
@click.option(
|
|
127
|
+
"-s",
|
|
128
|
+
"--service",
|
|
129
|
+
type=click.Choice(["tidal", "spotify"], case_sensitive=False),
|
|
130
|
+
default="tidal",
|
|
131
|
+
help="Music provider. Default is Tidal.",
|
|
132
|
+
)
|
|
133
|
+
@click.option("-v", "--verbose", count=True, help="Increase verbosity.")
|
|
134
|
+
@click.option("-d", "--debug", is_flag=True, help="Start debugger on exceptions.")
|
|
135
|
+
@click.pass_context
|
|
136
|
+
def cli(ctx, service, verbose, debug):
|
|
137
|
+
logging_level = logging.WARN - 10 * verbose
|
|
138
|
+
logging.basicConfig(level=logging_level)
|
|
139
|
+
ctx.ensure_object(dict)
|
|
140
|
+
ctx.obj["DEBUG"] = debug
|
|
141
|
+
if service == "tidal":
|
|
142
|
+
ctx.obj["MUSIC_CLIENT"] = TidalClient()
|
|
143
|
+
elif service == "spotify":
|
|
144
|
+
ctx.obj["MUSIC_CLIENT"] = SpotifyClient()
|
|
145
|
+
ctx.obj["LASTFM_CLIENT"] = LastFmClient()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@cli.group(name="save")
|
|
149
|
+
def save_group():
|
|
150
|
+
"""Save current playback items to your music services."""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@save_group.command(name="track")
|
|
154
|
+
@click.pass_context
|
|
155
|
+
@handle_tidal_rate_limit
|
|
156
|
+
def save_track(ctx):
|
|
157
|
+
"""Save current track to library, weekly playlist, and Last.fm."""
|
|
158
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
159
|
+
ctx.obj["LASTFM_CLIENT"].authenticate()
|
|
160
|
+
|
|
161
|
+
client = Client(ctx.obj["MUSIC_CLIENT"], ctx.obj["LASTFM_CLIENT"])
|
|
162
|
+
client.save_current_track()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@save_group.command(name="album")
|
|
166
|
+
@click.pass_context
|
|
167
|
+
@handle_tidal_rate_limit
|
|
168
|
+
def save_album(ctx):
|
|
169
|
+
"""Save current album to your music library."""
|
|
170
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
171
|
+
|
|
172
|
+
client = Client(ctx.obj["MUSIC_CLIENT"], None)
|
|
173
|
+
client.save_current_album()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@cli.group(name="collect")
|
|
177
|
+
def collect_group():
|
|
178
|
+
"""Collect current playback items for later listening."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@collect_group.command(name="album")
|
|
182
|
+
@click.pass_context
|
|
183
|
+
@handle_tidal_rate_limit
|
|
184
|
+
def collect_album(ctx):
|
|
185
|
+
"""Create a playlist from the current album."""
|
|
186
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
187
|
+
|
|
188
|
+
client = Client(ctx.obj["MUSIC_CLIENT"], None)
|
|
189
|
+
client.save_album_of_current_track_in_new_playlist()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@cli.group(name="favorite")
|
|
193
|
+
def favorite_group():
|
|
194
|
+
"""Favorite current playback items and file them by year."""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@favorite_group.command(name="track")
|
|
198
|
+
@click.pass_context
|
|
199
|
+
@handle_tidal_rate_limit
|
|
200
|
+
def favorite_track(ctx):
|
|
201
|
+
"""Save current track to library and this year's playlist."""
|
|
202
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
203
|
+
|
|
204
|
+
client = Client(ctx.obj["MUSIC_CLIENT"], None)
|
|
205
|
+
client.favorite_current_track()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@favorite_group.command(name="album")
|
|
209
|
+
@click.pass_context
|
|
210
|
+
@handle_tidal_rate_limit
|
|
211
|
+
def favorite_album(ctx):
|
|
212
|
+
"""Save current album to library and this year's albums playlist."""
|
|
213
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
214
|
+
|
|
215
|
+
client = Client(ctx.obj["MUSIC_CLIENT"], None)
|
|
216
|
+
client.favorite_current_album()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@cli.group(name="list")
|
|
220
|
+
def list_group():
|
|
221
|
+
"""List music service resources."""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@list_group.command(name="playlists")
|
|
225
|
+
@click.pass_context
|
|
226
|
+
@handle_tidal_rate_limit
|
|
227
|
+
def list_playlists(ctx):
|
|
228
|
+
"""List all playlists."""
|
|
229
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
230
|
+
|
|
231
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
232
|
+
for name in music_client.list_playlist_names():
|
|
233
|
+
click.echo(name)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@cli.group(name="refresh")
|
|
237
|
+
def refresh_group():
|
|
238
|
+
"""Refresh locally cached music service data."""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@refresh_group.command(name="playlists")
|
|
242
|
+
@click.pass_context
|
|
243
|
+
@handle_tidal_rate_limit
|
|
244
|
+
def refresh_playlists(ctx):
|
|
245
|
+
"""Refresh playlist cache where supported."""
|
|
246
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
247
|
+
|
|
248
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
249
|
+
music_client.update_playlist_cache()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@cli.group(name="url")
|
|
253
|
+
def url_group():
|
|
254
|
+
"""Print URLs for current playback items."""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@url_group.command(name="track")
|
|
258
|
+
@click.pass_context
|
|
259
|
+
@handle_tidal_rate_limit
|
|
260
|
+
def track_url(ctx):
|
|
261
|
+
"""Print URL of current track."""
|
|
262
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
263
|
+
|
|
264
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
265
|
+
click.echo(music_client.get_url_of_current_track())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@url_group.command(name="album")
|
|
269
|
+
@click.pass_context
|
|
270
|
+
@handle_tidal_rate_limit
|
|
271
|
+
def album_url(ctx):
|
|
272
|
+
"""Print URL of current album."""
|
|
273
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
274
|
+
|
|
275
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
276
|
+
click.echo(music_client.get_url_of_current_album())
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@url_group.command(name="weekly")
|
|
280
|
+
@click.pass_context
|
|
281
|
+
@handle_tidal_rate_limit
|
|
282
|
+
def weekly_playlist_url(ctx):
|
|
283
|
+
"""Print this week's playlist URL."""
|
|
284
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
285
|
+
|
|
286
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
287
|
+
url = music_client.get_weekly_playlist_url()
|
|
288
|
+
click.echo(url)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@cli.group(name="link")
|
|
292
|
+
def link_group():
|
|
293
|
+
"""Print formatted links for current playback items."""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@link_group.command(name="track")
|
|
297
|
+
@click.pass_context
|
|
298
|
+
@handle_tidal_rate_limit
|
|
299
|
+
def track_link(ctx):
|
|
300
|
+
"""Print Markdown link for current track."""
|
|
301
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
302
|
+
|
|
303
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
304
|
+
info = music_client.get_current_track_info()
|
|
305
|
+
url = music_client.get_url_of_current_track()
|
|
306
|
+
click.echo(f"[{info['artist']} - {info['title']}]({url})")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@link_group.command(name="album")
|
|
310
|
+
@click.pass_context
|
|
311
|
+
@handle_tidal_rate_limit
|
|
312
|
+
def album_link(ctx):
|
|
313
|
+
"""Print Markdown link for current album."""
|
|
314
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
315
|
+
|
|
316
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
317
|
+
info = music_client.get_current_track_info()
|
|
318
|
+
url = music_client.get_url_of_current_album()
|
|
319
|
+
click.echo(f"[{info['artist']} - {info['album']}]({url})")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@link_group.command(name="weekly")
|
|
323
|
+
@click.pass_context
|
|
324
|
+
@handle_tidal_rate_limit
|
|
325
|
+
def weekly_link(ctx):
|
|
326
|
+
"""Print Markdown link for this week's playlist."""
|
|
327
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
328
|
+
|
|
329
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
330
|
+
url = music_client.get_weekly_playlist_url()
|
|
331
|
+
click.echo(f"[{datetime.now().strftime('%Y-W%V')}]({url})")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@cli.group(name="split")
|
|
335
|
+
def split_group():
|
|
336
|
+
"""Split music service resources."""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@split_group.command(name="releases")
|
|
340
|
+
@click.pass_context
|
|
341
|
+
@handle_tidal_rate_limit
|
|
342
|
+
def split_releases(ctx):
|
|
343
|
+
"""Split new release singles and albums."""
|
|
344
|
+
ctx.obj["MUSIC_CLIENT"].authenticate()
|
|
345
|
+
|
|
346
|
+
music_client = ctx.obj["MUSIC_CLIENT"]
|
|
347
|
+
music_client.split_new_release_singles_and_albums()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@cli.group(name="auth")
|
|
351
|
+
def auth_group():
|
|
352
|
+
"""Manage service credentials."""
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@auth_group.command(name="set")
|
|
356
|
+
@click.option(
|
|
357
|
+
"--service",
|
|
358
|
+
type=click.Choice(["spotify", "lastfm", "tidal"]),
|
|
359
|
+
help="Specify service for setting keys",
|
|
360
|
+
)
|
|
361
|
+
@click.pass_context
|
|
362
|
+
def keys_set(ctx, service):
|
|
363
|
+
"""Set API keys for Spotify, LastFM, or TIDAL."""
|
|
364
|
+
if service == "spotify":
|
|
365
|
+
client_id = getpass("Enter your Spotify client-id: ")
|
|
366
|
+
client_secret = getpass("Enter your Spotify client-secret: ")
|
|
367
|
+
keyring.set_password(
|
|
368
|
+
"tunewrangler-spotify-client-id",
|
|
369
|
+
"tunewrangler-spotify-client-id",
|
|
370
|
+
client_id,
|
|
371
|
+
)
|
|
372
|
+
keyring.set_password(
|
|
373
|
+
"tunewrangler-spotify-client-secret",
|
|
374
|
+
"tunewrangler-spotify-client-secret",
|
|
375
|
+
client_secret,
|
|
376
|
+
)
|
|
377
|
+
click.echo("Spotify keys set successfully.")
|
|
378
|
+
elif service == "lastfm":
|
|
379
|
+
api_key = getpass("Enter your LastFM api-key: ")
|
|
380
|
+
shared_secret = getpass("Enter your LastFM shared-secret: ")
|
|
381
|
+
password = getpass("Enter your LastFM password: ")
|
|
382
|
+
keyring.set_password(
|
|
383
|
+
"tunewrangler-last-fm-api-key", "tunewrangler-last-fm-api-key", api_key
|
|
384
|
+
)
|
|
385
|
+
keyring.set_password(
|
|
386
|
+
"tunewrangler-last-fm-shared-secret",
|
|
387
|
+
"tunewrangler-last-fm-shared-secret",
|
|
388
|
+
shared_secret,
|
|
389
|
+
)
|
|
390
|
+
keyring.set_password(
|
|
391
|
+
"tunewrangler-last-fm-password",
|
|
392
|
+
"tunewrangler-last-fm-password",
|
|
393
|
+
password,
|
|
394
|
+
)
|
|
395
|
+
click.echo("LastFM keys set successfully.")
|
|
396
|
+
elif service == "tidal":
|
|
397
|
+
client_id = getpass("Enter your TIDAL client-id: ")
|
|
398
|
+
redirect_uri = getpass("Enter your TIDAL redirect URI: ")
|
|
399
|
+
keyring.set_password(
|
|
400
|
+
"tunewrangler-tidal-client-id",
|
|
401
|
+
"tunewrangler-tidal-client-id",
|
|
402
|
+
client_id,
|
|
403
|
+
)
|
|
404
|
+
keyring.set_password(
|
|
405
|
+
"tunewrangler-tidal-redirect-uri",
|
|
406
|
+
"tunewrangler-tidal-redirect-uri",
|
|
407
|
+
redirect_uri,
|
|
408
|
+
)
|
|
409
|
+
click.echo("TIDAL keys set successfully.")
|
|
410
|
+
else:
|
|
411
|
+
click.echo("Please specify a service: --service [spotify|lastfm|tidal]")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
if __name__ == "__main__":
|
|
415
|
+
cli(obj={})
|
tunewrangler/errors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class SpotifyControlError(BaseException):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AlbumIsSingleError(SpotifyControlError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlaylistExistsError(SpotifyControlError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(SpotifyControlError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NotPlayingError(SpotifyControlError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TrackNotFoundError(SpotifyControlError):
|
|
22
|
+
pass
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from tunewrangler import LASTFM_USERNAME
|
|
2
|
+
from tunewrangler.errors import AuthenticationError
|
|
3
|
+
|
|
4
|
+
import keyring
|
|
5
|
+
import pylast
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def missing_key_message(key_name):
|
|
12
|
+
return (
|
|
13
|
+
f"Cannot find {key_name!r} in the keychain. "
|
|
14
|
+
"Have you set your keys with `tunewrangler auth set`?"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LastFmClient:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.username = LASTFM_USERNAME
|
|
21
|
+
self.lastfm = None
|
|
22
|
+
|
|
23
|
+
def authenticate(self):
|
|
24
|
+
try:
|
|
25
|
+
logging.info("Logging in to LastFM")
|
|
26
|
+
api_key = keyring.get_password(
|
|
27
|
+
"tunewrangler-last-fm-api-key", "tunewrangler-last-fm-api-key"
|
|
28
|
+
)
|
|
29
|
+
if api_key is None:
|
|
30
|
+
raise AuthenticationError(
|
|
31
|
+
missing_key_message("tunewrangler-last-fm-api-key")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
api_secret = keyring.get_password(
|
|
35
|
+
"tunewrangler-last-fm-shared-secret",
|
|
36
|
+
"tunewrangler-last-fm-shared-secret",
|
|
37
|
+
)
|
|
38
|
+
if api_secret is None:
|
|
39
|
+
raise AuthenticationError(
|
|
40
|
+
missing_key_message("tunewrangler-last-fm-shared-secret")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
password = keyring.get_password(
|
|
44
|
+
"tunewrangler-last-fm-password", "tunewrangler-last-fm-password"
|
|
45
|
+
)
|
|
46
|
+
if password is None:
|
|
47
|
+
raise AuthenticationError(
|
|
48
|
+
missing_key_message("tunewrangler-last-fm-password")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.lastfm = pylast.LastFMNetwork(
|
|
52
|
+
api_key=api_key,
|
|
53
|
+
api_secret=api_secret,
|
|
54
|
+
username=self.username,
|
|
55
|
+
password_hash=pylast.md5(password),
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.error("Authentication failed: %s", e)
|
|
59
|
+
raise AuthenticationError("Can't authenticate to LastFM.")
|
|
60
|
+
|
|
61
|
+
def save_track(self, artist_name, track_name):
|
|
62
|
+
logging.info(
|
|
63
|
+
f"LastFmClient: getting current track: {artist_name} - {track_name}"
|
|
64
|
+
)
|
|
65
|
+
track = self.lastfm.get_track(artist_name, track_name)
|
|
66
|
+
logging.debug(f"Found track: {track}, {vars(track)}")
|
|
67
|
+
track.love()
|
|
68
|
+
logger.info(f"LastFM loved track: {track.artist.name} - {track.title}")
|