spotify-monitor 2.3.1__py3-none-any.whl → 2.4__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.
Potentially problematic release.
This version of spotify-monitor might be problematic. Click here for more details.
- {spotify_monitor-2.3.1.dist-info → spotify_monitor-2.4.dist-info}/METADATA +55 -7
- spotify_monitor-2.4.dist-info/RECORD +7 -0
- spotify_monitor.py +86 -26
- spotify_monitor-2.3.1.dist-info/RECORD +0 -7
- {spotify_monitor-2.3.1.dist-info → spotify_monitor-2.4.dist-info}/WHEEL +0 -0
- {spotify_monitor-2.3.1.dist-info → spotify_monitor-2.4.dist-info}/entry_points.txt +0 -0
- {spotify_monitor-2.3.1.dist-info → spotify_monitor-2.4.dist-info}/licenses/LICENSE +0 -0
- {spotify_monitor-2.3.1.dist-info → spotify_monitor-2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spotify_monitor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4
|
|
4
4
|
Summary: Tool implementing real-time tracking of Spotify friends music activity
|
|
5
5
|
Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -26,7 +26,7 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
# spotify_monitor
|
|
28
28
|
|
|
29
|
-
Tool for real-time monitoring of Spotify friends' music activity feed
|
|
29
|
+
Tool for real-time monitoring of **Spotify friends' music activity feed**.
|
|
30
30
|
|
|
31
31
|
✨ If you're interested in tracking changes to Spotify users' profiles including their playlists, take a look at another tool I've developed: [spotify_profile_monitor](https://github.com/misiektoja/spotify_profile_monitor).
|
|
32
32
|
|
|
@@ -77,6 +77,8 @@ Tool for real-time monitoring of Spotify friends' music activity feed.
|
|
|
77
77
|
* [Signal Controls (macOS/Linux/Unix)](#signal-controls-macoslinuxunix)
|
|
78
78
|
* [Coloring Log Output with GRC](#coloring-log-output-with-grc)
|
|
79
79
|
6. [Debugging Tools](#debugging-tools)
|
|
80
|
+
* [Access Token Retrieval via sp_dc Cookie and TOTP](#access-token-retrieval-via-sp_dc-cookie-and-totp)
|
|
81
|
+
* [Secret Key Extraction from Spotify Web Player Bundles](#secret-key-extraction-from-spotify-web-player-bundles)
|
|
80
82
|
7. [Change Log](#change-log)
|
|
81
83
|
8. [License](#license)
|
|
82
84
|
|
|
@@ -112,7 +114,7 @@ Download the *[spotify_monitor.py](https://raw.githubusercontent.com/misiektoja/
|
|
|
112
114
|
Install dependencies via pip:
|
|
113
115
|
|
|
114
116
|
```sh
|
|
115
|
-
pip install requests python-dateutil urllib3 pyotp python-dotenv
|
|
117
|
+
pip install requests python-dateutil urllib3 pyotp python-dotenv wcwidth
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
Alternatively, from the downloaded *[requirements.txt](https://raw.githubusercontent.com/misiektoja/spotify_monitor/refs/heads/main/requirements.txt)*:
|
|
@@ -199,7 +201,7 @@ If your `sp_dc` cookie expires, the tool will notify you via the console and ema
|
|
|
199
201
|
|
|
200
202
|
If you store the `SP_DC_COOKIE` in a dotenv file you can update its value and send a `SIGHUP` signal to reload the file with the new `sp_dc` cookie without restarting the tool. More info in [Storing Secrets](#storing-secrets) and [Signal Controls (macOS/Linux/Unix)](#signal-controls-macoslinuxunix).
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
> **NOTE:** secrets used for TOTP generation (`SECRET_CIPHER_DICT`) expire every two days, that's why since v2.4 the tool fetches it from remote URL (see `SECRET_CIPHER_DICT_URL`); you can also run the [spotify_monitor_secret_grabber](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_secret_grabber.py) and extract it by yourself (see [Secret Key Extraction from Spotify Web Player Bundles](#secret-key-extraction-from-spotify-web-player-bundles) for more info).
|
|
203
205
|
|
|
204
206
|
<a id="spotify-desktop-client"></a>
|
|
205
207
|
#### Spotify Desktop Client
|
|
@@ -572,27 +574,73 @@ grc tail -F -n 100 spotify_monitor_<user_uri_id/file_suffix>.log
|
|
|
572
574
|
<a id="debugging-tools"></a>
|
|
573
575
|
## Debugging Tools
|
|
574
576
|
|
|
575
|
-
To help with troubleshooting and development, two debug utilities are available in the
|
|
577
|
+
To help with troubleshooting and development, two debug utilities are available in the [debug](https://github.com/misiektoja/spotify_monitor/tree/dev/debug) directory.
|
|
576
578
|
|
|
577
|
-
|
|
579
|
+
<a id="access-token-retrieval-via-sp_dc-cookie-and-totp"></a>
|
|
580
|
+
### Access Token Retrieval via sp_dc Cookie and TOTP
|
|
581
|
+
|
|
582
|
+
The [spotify_monitor_totp_test](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_totp_test.py) tool retrieves a Spotify access token using a Web Player `sp_dc` cookie and TOTP parameters.
|
|
583
|
+
|
|
584
|
+
Download from [here](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_totp_test.py) or:
|
|
585
|
+
|
|
586
|
+
```sh
|
|
587
|
+
wget https://raw.githubusercontent.com/misiektoja/spotify_monitor/refs/heads/dev/debug/spotify_monitor_totp_test.py
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Install requirements:
|
|
578
591
|
|
|
579
592
|
```sh
|
|
580
593
|
pip install requests python-dateutil pyotp
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Run:
|
|
597
|
+
|
|
598
|
+
```sh
|
|
581
599
|
python3 spotify_monitor_totp_test.py --sp-dc "your_sp_dc_cookie_value"
|
|
582
600
|
```
|
|
583
601
|
|
|
584
|
-
|
|
602
|
+
You should get a valid Spotify access token, example output:
|
|
603
|
+
|
|
604
|
+
<p align="center">
|
|
605
|
+
<img src="https://raw.githubusercontent.com/misiektoja/spotify_monitor/refs/heads/main/assets/spotify_monitor_totp_test.png" alt="spotify_monitor_totp_test" width="100%"/>
|
|
606
|
+
</p>
|
|
607
|
+
|
|
608
|
+
> **NOTE:** secrets used for TOTP generation (`SECRET_CIPHER_DICT`) expire every two days; you can either run the [spotify_monitor_secret_grabber](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_secret_grabber.py) and extract it by yourself (see [here](#secret-key-extraction-from-spotify-web-player-bundles) for more info) or you can pass `--fetch-secrets` flag in [spotify_monitor_totp_test](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_totp_test.py) (available since v1.6). There is also a [Thereallo1026/spotify-secrets](https://github.com/Thereallo1026/spotify-secrets) repo which offers JSON files that are automatically updated with current secrets.
|
|
609
|
+
|
|
610
|
+
<a id="secret-key-extraction-from-spotify-web-player-bundles"></a>
|
|
611
|
+
### Secret Key Extraction from Spotify Web Player Bundles
|
|
612
|
+
|
|
613
|
+
The [spotify_monitor_secret_grabber](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_secret_grabber.py) tool automatically extracts secret keys used for TOTP generation in Spotify Web Player JavaScript bundles.
|
|
614
|
+
|
|
615
|
+
Download from [here](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_secret_grabber.py) or:
|
|
616
|
+
|
|
617
|
+
```sh
|
|
618
|
+
wget https://raw.githubusercontent.com/misiektoja/spotify_monitor/refs/heads/dev/debug/spotify_monitor_secret_grabber.py
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Install requirements:
|
|
585
622
|
|
|
586
623
|
```sh
|
|
587
624
|
pip install playwright
|
|
588
625
|
playwright install
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Run:
|
|
629
|
+
|
|
630
|
+
```sh
|
|
589
631
|
python3 spotify_monitor_secret_grabber.py
|
|
590
632
|
```
|
|
591
633
|
|
|
634
|
+
You should get output similar to below:
|
|
635
|
+
|
|
592
636
|
<p align="center">
|
|
593
637
|
<img src="https://raw.githubusercontent.com/misiektoja/spotify_monitor/refs/heads/main/assets/spotify_monitor_secret_grabber.png" alt="spotify_monitor_secret_grabber" width="100%"/>
|
|
594
638
|
</p>
|
|
595
639
|
|
|
640
|
+
You can now update the secrets used for TOTP generation (for example `SECRET_CIPHER_DICT` in `spotify_monitor_totp_test`, `spotify_monitor` and `spotify_profile_monitor`).
|
|
641
|
+
|
|
642
|
+
> **NOTE:** you can also use [Thereallo1026/spotify-secrets](https://github.com/Thereallo1026/spotify-secrets) repo which offers JSON files that are automatically updated with current secrets (its secret extraction code is based on `spotify_monitor_secret_grabber`).
|
|
643
|
+
|
|
596
644
|
<a id="change-log"></a>
|
|
597
645
|
## Change Log
|
|
598
646
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
spotify_monitor.py,sha256=TtB0dWvl5Yn1ti4y8VFk61V8rqXEZfnLY2PJZ26hVsw,161638
|
|
2
|
+
spotify_monitor-2.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
spotify_monitor-2.4.dist-info/METADATA,sha256=rcnYf1XgMu4k55OO0m1yfP61TXRA_CkZLLDXUzDLHLM,27747
|
|
4
|
+
spotify_monitor-2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
spotify_monitor-2.4.dist-info/entry_points.txt,sha256=8HzePfUcCSXrYaXOwLbNNYO8GJcnhgCSl4wcDNECht8,57
|
|
6
|
+
spotify_monitor-2.4.dist-info/top_level.txt,sha256=EP6IPD4vHT12rLM5b_jo2i3nrfOuwk3ehhr2gWdQx9Y,16
|
|
7
|
+
spotify_monitor-2.4.dist-info/RECORD,,
|
spotify_monitor.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Author: Michal Szymanski <misiektoja-github@rm-rf.ninja>
|
|
4
|
-
v2.
|
|
4
|
+
v2.4
|
|
5
5
|
|
|
6
6
|
Tool implementing real-time tracking of Spotify friends music activity:
|
|
7
7
|
https://github.com/misiektoja/spotify_monitor/
|
|
@@ -16,7 +16,7 @@ python-dotenv (optional)
|
|
|
16
16
|
wcwidth (optional, needed by TRUNCATE_CHARS feature)
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
VERSION = "2.
|
|
19
|
+
VERSION = "2.4"
|
|
20
20
|
|
|
21
21
|
# ---------------------------
|
|
22
22
|
# CONFIGURATION SECTION START
|
|
@@ -244,13 +244,14 @@ SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 30 # 30 seconds
|
|
|
244
244
|
# The section below is used when the token source is set to 'cookie'
|
|
245
245
|
|
|
246
246
|
# Maximum number of attempts to get a valid access token in a single run of the spotify_get_access_token_from_sp_dc() function
|
|
247
|
-
TOKEN_MAX_RETRIES =
|
|
247
|
+
TOKEN_MAX_RETRIES = 3
|
|
248
248
|
|
|
249
249
|
# Interval between access token retry attempts; in seconds
|
|
250
250
|
TOKEN_RETRY_TIMEOUT = 0.5 # 0.5 second
|
|
251
251
|
|
|
252
|
-
# Mapping of TOTP version identifiers to the
|
|
253
|
-
# Newest
|
|
252
|
+
# Mapping of TOTP version identifiers to the secrets needed for TOTP generation
|
|
253
|
+
# Newest secrets are downloaded automatically from SECRET_CIPHER_DICT_URL (see below)
|
|
254
|
+
# Can also be fetched via spotify_monitor_secret_grabber.py utility - see debug dir
|
|
254
255
|
SECRET_CIPHER_DICT = {
|
|
255
256
|
"12": [107, 81, 49, 57, 67, 93, 87, 81, 69, 67, 40, 93, 48, 50, 46, 91, 94, 113, 41, 108, 77, 107, 34],
|
|
256
257
|
"11": [111, 45, 40, 73, 95, 74, 35, 85, 105, 107, 60, 110, 55, 72, 69, 70, 114, 83, 63, 88, 91],
|
|
@@ -262,7 +263,11 @@ SECRET_CIPHER_DICT = {
|
|
|
262
263
|
"5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54],
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
#
|
|
266
|
+
# Remote URL used to fetch updated secrets needed for TOTP generation
|
|
267
|
+
# Set to empty string to disable
|
|
268
|
+
SECRET_CIPHER_DICT_URL = "https://github.com/Thereallo1026/spotify-secrets/blob/main/secrets/secretDict.json?raw=true"
|
|
269
|
+
|
|
270
|
+
# Identifier used to select the appropriate secret from SECRET_CIPHER_DICT when generating a TOTP token
|
|
266
271
|
# Set to 0 to auto-select the highest available version
|
|
267
272
|
TOTP_VER = 0
|
|
268
273
|
|
|
@@ -467,6 +472,7 @@ SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 0
|
|
|
467
472
|
TOKEN_MAX_RETRIES = 0
|
|
468
473
|
TOKEN_RETRY_TIMEOUT = 0.0
|
|
469
474
|
SECRET_CIPHER_DICT = {}
|
|
475
|
+
SECRET_CIPHER_DICT_URL = ""
|
|
470
476
|
TOTP_VER = 0
|
|
471
477
|
FLAG_FILE = ""
|
|
472
478
|
TRUNCATE_CHARS = 0
|
|
@@ -1276,7 +1282,10 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
|
|
|
1276
1282
|
def generate_totp():
|
|
1277
1283
|
import pyotp
|
|
1278
1284
|
|
|
1279
|
-
|
|
1285
|
+
if str((ver := TOTP_VER or max(map(int, SECRET_CIPHER_DICT)))) not in SECRET_CIPHER_DICT:
|
|
1286
|
+
raise Exception(f"generate_totp(): Defined TOTP_VER ({ver}) is missing in SECRET_CIPHER_DICT")
|
|
1287
|
+
|
|
1288
|
+
secret_cipher_bytes = SECRET_CIPHER_DICT[str(ver)]
|
|
1280
1289
|
|
|
1281
1290
|
transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
|
|
1282
1291
|
joined = "".join(str(num) for num in transformed)
|
|
@@ -1286,6 +1295,34 @@ def generate_totp():
|
|
|
1286
1295
|
return pyotp.TOTP(secret, digits=6, interval=30)
|
|
1287
1296
|
|
|
1288
1297
|
|
|
1298
|
+
def fetch_and_update_secrets():
|
|
1299
|
+
global SECRET_CIPHER_DICT
|
|
1300
|
+
|
|
1301
|
+
if not SECRET_CIPHER_DICT_URL:
|
|
1302
|
+
return False
|
|
1303
|
+
|
|
1304
|
+
try:
|
|
1305
|
+
response = req.get(SECRET_CIPHER_DICT_URL, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1306
|
+
response.raise_for_status()
|
|
1307
|
+
secrets = response.json()
|
|
1308
|
+
|
|
1309
|
+
if not isinstance(secrets, dict) or not secrets:
|
|
1310
|
+
raise ValueError("fetch_and_update_secrets(): Fetched payload not a non‑empty dict")
|
|
1311
|
+
|
|
1312
|
+
for key, value in secrets.items():
|
|
1313
|
+
if not isinstance(key, str) or not key.isdigit():
|
|
1314
|
+
raise ValueError(f"fetch_and_update_secrets(): Invalid key format: {key}")
|
|
1315
|
+
if not isinstance(value, list) or not all(isinstance(x, int) for x in value):
|
|
1316
|
+
raise ValueError(f"fetch_and_update_secrets(): Invalid value format for key {key}")
|
|
1317
|
+
|
|
1318
|
+
SECRET_CIPHER_DICT = secrets
|
|
1319
|
+
return True
|
|
1320
|
+
|
|
1321
|
+
except Exception as e:
|
|
1322
|
+
print(f"fetch_and_update_secrets(): Failed to get new secrets: {e}")
|
|
1323
|
+
return False
|
|
1324
|
+
|
|
1325
|
+
|
|
1289
1326
|
# Refreshes the Spotify access token using the sp_dc cookie, tries first with mode "transport" and if needed with "init"
|
|
1290
1327
|
def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
|
|
1291
1328
|
transport = True
|
|
@@ -1385,29 +1422,52 @@ def spotify_get_access_token_from_sp_dc(sp_dc: str):
|
|
|
1385
1422
|
max_retries = TOKEN_MAX_RETRIES
|
|
1386
1423
|
retry = 0
|
|
1387
1424
|
|
|
1388
|
-
|
|
1389
|
-
token_data = refresh_access_token_from_sp_dc(sp_dc)
|
|
1390
|
-
token = token_data["access_token"]
|
|
1391
|
-
client_id = token_data.get("client_id", "")
|
|
1392
|
-
length = token_data["length"]
|
|
1425
|
+
last_error = ""
|
|
1393
1426
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1427
|
+
while retry < max_retries:
|
|
1428
|
+
try:
|
|
1429
|
+
token_data = refresh_access_token_from_sp_dc(sp_dc)
|
|
1430
|
+
token = token_data["access_token"]
|
|
1431
|
+
client_id = token_data.get("client_id", "")
|
|
1432
|
+
length = token_data["length"]
|
|
1433
|
+
|
|
1434
|
+
SP_CACHED_ACCESS_TOKEN = token
|
|
1435
|
+
SP_ACCESS_TOKEN_EXPIRES_AT = token_data["expires_at"]
|
|
1436
|
+
SP_CACHED_CLIENT_ID = client_id
|
|
1437
|
+
|
|
1438
|
+
if SP_CACHED_ACCESS_TOKEN is None or not check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, USER_AGENT):
|
|
1439
|
+
retry += 1
|
|
1440
|
+
time.sleep(TOKEN_RETRY_TIMEOUT)
|
|
1441
|
+
else:
|
|
1442
|
+
break
|
|
1443
|
+
except Exception as e:
|
|
1444
|
+
last_error = str(e)
|
|
1399
1445
|
retry += 1
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
break
|
|
1446
|
+
if retry < max_retries:
|
|
1447
|
+
time.sleep(TOKEN_RETRY_TIMEOUT)
|
|
1403
1448
|
|
|
1404
1449
|
if retry == max_retries:
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1450
|
+
|
|
1451
|
+
if fetch_and_update_secrets():
|
|
1452
|
+
try:
|
|
1453
|
+
token_data = refresh_access_token_from_sp_dc(sp_dc)
|
|
1454
|
+
token = token_data["access_token"]
|
|
1455
|
+
client_id = token_data.get("client_id", "")
|
|
1456
|
+
length = token_data["length"]
|
|
1457
|
+
|
|
1458
|
+
SP_CACHED_ACCESS_TOKEN = token
|
|
1459
|
+
SP_ACCESS_TOKEN_EXPIRES_AT = token_data["expires_at"]
|
|
1460
|
+
SP_CACHED_CLIENT_ID = client_id
|
|
1461
|
+
|
|
1462
|
+
if SP_CACHED_ACCESS_TOKEN and check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, USER_AGENT):
|
|
1463
|
+
return SP_CACHED_ACCESS_TOKEN
|
|
1464
|
+
except Exception as e:
|
|
1465
|
+
last_error = str(e)
|
|
1466
|
+
|
|
1467
|
+
error_msg = f"Failed to obtain a valid Spotify access token after {max_retries} attempts"
|
|
1468
|
+
if last_error:
|
|
1469
|
+
error_msg += f": {last_error}"
|
|
1470
|
+
raise RuntimeError(error_msg)
|
|
1411
1471
|
|
|
1412
1472
|
return SP_CACHED_ACCESS_TOKEN
|
|
1413
1473
|
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
spotify_monitor.py,sha256=msF-xUpmB_dch3C1dtA31mvU0OgtRUgUeOtXh20xDLw,159386
|
|
2
|
-
spotify_monitor-2.3.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
spotify_monitor-2.3.1.dist-info/METADATA,sha256=z37aFQ2NUa5jYzU5pF6ru5UXmu65Ctee8J_EDvjG1iI,25204
|
|
4
|
-
spotify_monitor-2.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
spotify_monitor-2.3.1.dist-info/entry_points.txt,sha256=8HzePfUcCSXrYaXOwLbNNYO8GJcnhgCSl4wcDNECht8,57
|
|
6
|
-
spotify_monitor-2.3.1.dist-info/top_level.txt,sha256=EP6IPD4vHT12rLM5b_jo2i3nrfOuwk3ehhr2gWdQx9Y,16
|
|
7
|
-
spotify_monitor-2.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|