spotify-monitor 2.1.1__py3-none-any.whl → 2.2__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.1.1.dist-info → spotify_monitor-2.2.dist-info}/METADATA +40 -40
- spotify_monitor-2.2.dist-info/RECORD +7 -0
- spotify_monitor.py +393 -240
- spotify_monitor-2.1.1.dist-info/RECORD +0 -7
- {spotify_monitor-2.1.1.dist-info → spotify_monitor-2.2.dist-info}/WHEEL +0 -0
- {spotify_monitor-2.1.1.dist-info → spotify_monitor-2.2.dist-info}/entry_points.txt +0 -0
- {spotify_monitor-2.1.1.dist-info → spotify_monitor-2.2.dist-info}/licenses/LICENSE +0 -0
- {spotify_monitor-2.1.1.dist-info → spotify_monitor-2.2.dist-info}/top_level.txt +0 -0
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.2
|
|
5
5
|
|
|
6
6
|
Tool implementing real-time tracking of Spotify friends music activity:
|
|
7
7
|
https://github.com/misiektoja/spotify_monitor/
|
|
@@ -11,11 +11,11 @@ Python pip3 requirements:
|
|
|
11
11
|
requests
|
|
12
12
|
python-dateutil
|
|
13
13
|
urllib3
|
|
14
|
-
pyotp (needed when the token source is set to cookie)
|
|
14
|
+
pyotp (optional, needed when the token source is set to cookie)
|
|
15
15
|
python-dotenv (optional)
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
VERSION = "2.
|
|
18
|
+
VERSION = "2.2"
|
|
19
19
|
|
|
20
20
|
# ---------------------------
|
|
21
21
|
# CONFIGURATION SECTION START
|
|
@@ -24,27 +24,25 @@ VERSION = "2.1.1"
|
|
|
24
24
|
CONFIG_BLOCK = """
|
|
25
25
|
# Select the method used to obtain the Spotify access token
|
|
26
26
|
# Available options:
|
|
27
|
-
# cookie
|
|
28
|
-
# client
|
|
27
|
+
# cookie - uses the sp_dc cookie to retrieve a token via the Spotify web endpoint (recommended)
|
|
28
|
+
# client - uses captured credentials from the Spotify desktop client and a Protobuf-based login flow (for advanced users)
|
|
29
29
|
TOKEN_SOURCE = "cookie"
|
|
30
30
|
|
|
31
|
-
#
|
|
31
|
+
# ---------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
# The section below is used when the token source is set to 'cookie'
|
|
34
34
|
# (to configure the alternative 'client' method, see the section at the end of this config block)
|
|
35
35
|
#
|
|
36
|
-
# Log in to Spotify web client (https://open.spotify.com/) and retrieve your sp_dc cookie
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# Provide the SP_DC_COOKIE secret using one of the following methods:
|
|
36
|
+
# - Log in to Spotify web client (https://open.spotify.com/) and retrieve your sp_dc cookie
|
|
37
|
+
# (use your web browser's dev console or "Cookie-Editor" by cgagnier to extract it easily: https://cookie-editor.com/)
|
|
38
|
+
# - Provide the SP_DC_COOKIE secret using one of the following methods:
|
|
40
39
|
# - Pass it at runtime with -u / --spotify-dc-cookie
|
|
41
40
|
# - Set it as an environment variable (e.g. export SP_DC_COOKIE=...)
|
|
42
41
|
# - Add it to ".env" file (SP_DC_COOKIE=...) for persistent use
|
|
43
|
-
# Fallback:
|
|
44
|
-
# - Hard-code it in the code or config file
|
|
42
|
+
# - Fallback: hard-code it in the code or config file
|
|
45
43
|
SP_DC_COOKIE = "your_sp_dc_cookie_value"
|
|
46
44
|
|
|
47
|
-
#
|
|
45
|
+
# ---------------------------------------------------------------------
|
|
48
46
|
|
|
49
47
|
# SMTP settings for sending email notifications
|
|
50
48
|
# If left as-is, no notifications will be sent
|
|
@@ -151,6 +149,19 @@ SP_USER_GOT_OFFLINE_TRACK_ID = ""
|
|
|
151
149
|
# Set to 0 to keep playing indefinitely until manually paused
|
|
152
150
|
SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 5 # 5 seconds
|
|
153
151
|
|
|
152
|
+
# Optional: specify user agent manually
|
|
153
|
+
#
|
|
154
|
+
# When the token source is 'cookie' - set it to web browser user agent, some examples:
|
|
155
|
+
# Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
|
|
156
|
+
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0
|
|
157
|
+
#
|
|
158
|
+
# When the token source is 'client' - set it to Spotify desktop client user agent, some examples:
|
|
159
|
+
# Spotify/126200580 Win32_x86_64/0 (PC desktop)
|
|
160
|
+
# Spotify/126400408 OSX_ARM64/OS X 15.5.0 [arm 2]
|
|
161
|
+
#
|
|
162
|
+
# Leave empty to auto-generate it randomly for specific token source
|
|
163
|
+
USER_AGENT = ""
|
|
164
|
+
|
|
154
165
|
# How often to print a "liveness check" message to the output; in seconds
|
|
155
166
|
# Set to 0 to disable
|
|
156
167
|
LIVENESS_CHECK_INTERVAL = 43200 # 12 hours
|
|
@@ -219,44 +230,64 @@ TOKEN_MAX_RETRIES = 10
|
|
|
219
230
|
# Used only when the token source is set to 'cookie'
|
|
220
231
|
TOKEN_RETRY_TIMEOUT = 0.5 # 0.5 second
|
|
221
232
|
|
|
222
|
-
#
|
|
233
|
+
# ---------------------------------------------------------------------
|
|
234
|
+
|
|
223
235
|
# The section below is used when the token source is set to 'client'
|
|
224
236
|
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# -
|
|
229
|
-
# note: the 'login' part is suffixed with one or more digits
|
|
230
|
-
# - export the login request body (a binary Protobuf payload) to a file
|
|
237
|
+
# - Run an intercepting proxy of your choice (like Proxyman)
|
|
238
|
+
# - Launch the Spotify desktop client and look for requests to: https://login{n}.spotify.com/v3/login
|
|
239
|
+
# (the 'login' part is suffixed with one or more digits)
|
|
240
|
+
# - Export the login request body (a binary Protobuf payload) to a file
|
|
231
241
|
# (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <login-request-body-file>)
|
|
232
242
|
#
|
|
233
|
-
#
|
|
243
|
+
# To automatically extract DEVICE_ID, SYSTEM_ID, USER_URI_ID and REFRESH_TOKEN from the exported binary login
|
|
244
|
+
# request Protobuf file:
|
|
245
|
+
#
|
|
246
|
+
# - Run the tool with the -w flag to indicate an exported file or specify its file name below
|
|
234
247
|
LOGIN_REQUEST_BODY_FILE = ""
|
|
235
248
|
|
|
236
|
-
# Alternatively, set the
|
|
249
|
+
# Alternatively, you can manually set the DEVICE_ID, SYSTEM_ID, USER_URI_ID and REFRESH_TOKEN options
|
|
250
|
+
# (however, using the automated method described above is recommended)
|
|
251
|
+
#
|
|
252
|
+
# These values can be extracted using one of the following methods:
|
|
253
|
+
#
|
|
254
|
+
# - Run spotify_profile_monitor with the -w flag without specifying SPOTIFY_USER_URI_ID - it will decode the file and
|
|
255
|
+
# print the values to stdout, example:
|
|
256
|
+
# spotify_profile_monitor --token-source client -w <path-to-login-request-body-file>
|
|
237
257
|
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
# - USER_URI_ID
|
|
242
|
-
# - REFRESH_TOKEN
|
|
243
|
-
# and assign them to respective configuration options
|
|
258
|
+
# - Use the protoc tool (part of protobuf pip package):
|
|
259
|
+
# pip install protobuf
|
|
260
|
+
# protoc --decode_raw < <path-to-login-request-body-file>
|
|
244
261
|
#
|
|
245
|
-
#
|
|
246
|
-
# protoc --decode_raw < <path-to-login-request-body-file>
|
|
262
|
+
# - Use the built-in Protobuf decoder in your intercepting proxy (if supported)
|
|
247
263
|
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
264
|
+
# The Protobuf structure is as follows:
|
|
265
|
+
#
|
|
266
|
+
# {
|
|
267
|
+
# 1: {
|
|
268
|
+
# 1: "DEVICE_ID",
|
|
269
|
+
# 2: "SYSTEM_ID"
|
|
270
|
+
# },
|
|
271
|
+
# 100: {
|
|
272
|
+
# 1: "USER_URI_ID",
|
|
273
|
+
# 2: "REFRESH_TOKEN"
|
|
274
|
+
# }
|
|
275
|
+
# }
|
|
276
|
+
#
|
|
277
|
+
# Provide the extracted values below (DEVICE_ID, SYSTEM_ID, USER_URI_ID). The REFRESH_TOKEN secret can be
|
|
278
|
+
# supplied using one of the following methods:
|
|
250
279
|
# - Set it as an environment variable (e.g. export REFRESH_TOKEN=...)
|
|
251
280
|
# - Add it to ".env" file (REFRESH_TOKEN=...) for persistent use
|
|
252
|
-
# Fallback:
|
|
253
|
-
# - Hard-code it in the code or config file
|
|
281
|
+
# - Fallback: hard-code it in the code or config file
|
|
254
282
|
DEVICE_ID = "your_spotify_app_device_id"
|
|
255
283
|
SYSTEM_ID = "your_spotify_app_system_id"
|
|
256
284
|
USER_URI_ID = "your_spotify_user_uri_id"
|
|
257
285
|
REFRESH_TOKEN = "your_spotify_app_refresh_token"
|
|
258
286
|
|
|
259
|
-
#
|
|
287
|
+
# ----------------------------------------------
|
|
288
|
+
# Advanced options for 'client' token source
|
|
289
|
+
# Modifying the values below is NOT recommended!
|
|
290
|
+
# ----------------------------------------------
|
|
260
291
|
|
|
261
292
|
# Spotify login URL
|
|
262
293
|
LOGIN_URL = "https://login5.spotify.com/v3/login"
|
|
@@ -264,20 +295,58 @@ LOGIN_URL = "https://login5.spotify.com/v3/login"
|
|
|
264
295
|
# Spotify client token URL
|
|
265
296
|
CLIENTTOKEN_URL = "https://clienttoken.spotify.com/v1/clienttoken"
|
|
266
297
|
|
|
267
|
-
#
|
|
298
|
+
# Platform-specific values for token generation so the Spotify client token requests match your exact Spotify desktop
|
|
299
|
+
# client build (arch, OS build, app version etc.)
|
|
268
300
|
#
|
|
269
|
-
#
|
|
270
|
-
# Spotify
|
|
301
|
+
# - Run an intercepting proxy of your choice (like Proxyman)
|
|
302
|
+
# - Launch the Spotify desktop client and look for requests to: https://clienttoken.spotify.com/v1/clienttoken
|
|
303
|
+
# (these requests are sent every time client token expires, usually every 2 weeks)
|
|
304
|
+
# - Export the client token request body (a binary Protobuf payload) to a file
|
|
305
|
+
# (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <clienttoken-request-body-file>)
|
|
271
306
|
#
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
#
|
|
276
|
-
|
|
277
|
-
APP_VERSION = ""
|
|
307
|
+
# To automatically extract APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR and CLIENT_MODEL from the
|
|
308
|
+
# exported binary client token request Protobuf file:
|
|
309
|
+
#
|
|
310
|
+
# - Run the tool with the hidden -z flag to indicate an exported file or specify its file name below
|
|
311
|
+
CLIENTTOKEN_REQUEST_BODY_FILE = ""
|
|
278
312
|
|
|
279
|
-
#
|
|
280
|
-
#
|
|
313
|
+
# Alternatively, you can manually set the APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR and
|
|
314
|
+
# CLIENT_MODEL options
|
|
315
|
+
#
|
|
316
|
+
# These values can be extracted using one of the following methods:
|
|
317
|
+
#
|
|
318
|
+
# - run spotify_profile_monitor with the hidden -z flag without specifying SPOTIFY_USER_URI_ID - it will decode the file
|
|
319
|
+
# and print the values to stdout, example:
|
|
320
|
+
# spotify_profile_monitor --token-source client -z <path-to-clienttoken-request-body-file>
|
|
321
|
+
#
|
|
322
|
+
# - use the protoc tool (part of protobuf pip package):
|
|
323
|
+
# pip install protobuf
|
|
324
|
+
# protoc --decode_raw < <path-to-clienttoken-request-body-file>
|
|
325
|
+
#
|
|
326
|
+
# - use the built-in Protobuf decoder in your intercepting proxy (if supported)
|
|
327
|
+
#
|
|
328
|
+
# The Protobuf structure is as follows:
|
|
329
|
+
#
|
|
330
|
+
# 1: 1
|
|
331
|
+
# 2 {
|
|
332
|
+
# 1: "APP_VERSION"
|
|
333
|
+
# 2: "DEVICE_ID"
|
|
334
|
+
# 3 {
|
|
335
|
+
# 1 {
|
|
336
|
+
# 4 {
|
|
337
|
+
# 1: "CPU_ARCH"
|
|
338
|
+
# 3: "OS_BUILD"
|
|
339
|
+
# 4: "PLATFORM"
|
|
340
|
+
# 5: "OS_MAJOR"
|
|
341
|
+
# 6: "OS_MINOR"
|
|
342
|
+
# 8: "CLIENT_MODEL"
|
|
343
|
+
# }
|
|
344
|
+
# }
|
|
345
|
+
# 2: "SYSTEM_ID"
|
|
346
|
+
# }
|
|
347
|
+
# }
|
|
348
|
+
#
|
|
349
|
+
# Provide the extracted values below (except for DEVICE_ID and SYSTEM_ID as it was already provided via -w)
|
|
281
350
|
CPU_ARCH = 10
|
|
282
351
|
OS_BUILD = 19045
|
|
283
352
|
PLATFORM = 2
|
|
@@ -285,19 +354,11 @@ OS_MAJOR = 9
|
|
|
285
354
|
OS_MINOR = 9
|
|
286
355
|
CLIENT_MODEL = 34404
|
|
287
356
|
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
# - run an intercepting proxy of your choice (like Proxyman)
|
|
292
|
-
# - launch the Spotify desktop client and look for requests to: https://clienttoken.spotify.com/v1/clienttoken
|
|
293
|
-
# (these requests are sent every time client token expires, usually every 2 weeks)
|
|
294
|
-
# - export the client token request body (a binary Protobuf payload) to a file
|
|
295
|
-
# (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <clienttoken-request-body-file>)
|
|
296
|
-
#
|
|
297
|
-
# Can also be set via the -z flag
|
|
298
|
-
CLIENTTOKEN_REQUEST_BODY_FILE = ""
|
|
357
|
+
# App version (e.g. '1.2.62.580.g7e3d9a4f')
|
|
358
|
+
# Leave empty to auto-generate from USER_AGENT
|
|
359
|
+
APP_VERSION = ""
|
|
299
360
|
|
|
300
|
-
#
|
|
361
|
+
# ---------------------------------------------------------------------
|
|
301
362
|
"""
|
|
302
363
|
|
|
303
364
|
# -------------------------
|
|
@@ -311,7 +372,6 @@ SP_DC_COOKIE = ""
|
|
|
311
372
|
LOGIN_REQUEST_BODY_FILE = ""
|
|
312
373
|
CLIENTTOKEN_REQUEST_BODY_FILE = ""
|
|
313
374
|
LOGIN_URL = ""
|
|
314
|
-
USER_AGENT = ""
|
|
315
375
|
DEVICE_ID = ""
|
|
316
376
|
SYSTEM_ID = ""
|
|
317
377
|
USER_URI_ID = ""
|
|
@@ -349,6 +409,7 @@ SONG_ON_LOOP_VALUE = 0
|
|
|
349
409
|
SKIPPED_SONG_THRESHOLD = 0
|
|
350
410
|
SP_USER_GOT_OFFLINE_TRACK_ID = ""
|
|
351
411
|
SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 0
|
|
412
|
+
USER_AGENT = ""
|
|
352
413
|
LIVENESS_CHECK_INTERVAL = 0
|
|
353
414
|
CHECK_INTERNET_URL = ""
|
|
354
415
|
CHECK_INTERNET_TIMEOUT = 0
|
|
@@ -393,13 +454,9 @@ SP_CACHED_ACCESS_TOKEN = None
|
|
|
393
454
|
SP_CACHED_REFRESH_TOKEN = None
|
|
394
455
|
SP_ACCESS_TOKEN_EXPIRES_AT = 0
|
|
395
456
|
SP_CACHED_CLIENT_ID = ""
|
|
396
|
-
SP_CACHED_USER_AGENT = ""
|
|
397
457
|
|
|
398
458
|
# URL of the Spotify Web Player endpoint to get access token
|
|
399
|
-
TOKEN_URL = "https://open.spotify.com/
|
|
400
|
-
|
|
401
|
-
# URL of the endpoint to get server time needed to create TOTP object
|
|
402
|
-
SERVER_TIME_URL = "https://open.spotify.com/server-time"
|
|
459
|
+
TOKEN_URL = "https://open.spotify.com/api/token"
|
|
403
460
|
|
|
404
461
|
# Variables for caching functionality of the Spotify client token to avoid unnecessary refreshing
|
|
405
462
|
SP_CACHED_CLIENT_TOKEN = None
|
|
@@ -451,6 +508,7 @@ import shutil
|
|
|
451
508
|
from pathlib import Path
|
|
452
509
|
import secrets
|
|
453
510
|
from typing import Optional
|
|
511
|
+
from email.utils import parsedate_to_datetime
|
|
454
512
|
|
|
455
513
|
import urllib3
|
|
456
514
|
if not VERIFY_SSL:
|
|
@@ -468,7 +526,8 @@ retry = Retry(
|
|
|
468
526
|
backoff_factor=1,
|
|
469
527
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
470
528
|
allowed_methods=["GET", "HEAD", "OPTIONS"],
|
|
471
|
-
raise_on_status=False
|
|
529
|
+
raise_on_status=False,
|
|
530
|
+
respect_retry_after_header=True
|
|
472
531
|
)
|
|
473
532
|
|
|
474
533
|
adapter = HTTPAdapter(max_retries=retry, pool_connections=100, pool_maxsize=100)
|
|
@@ -512,7 +571,7 @@ def signal_handler(sig, frame):
|
|
|
512
571
|
# Checks internet connectivity
|
|
513
572
|
def check_internet(url=CHECK_INTERNET_URL, timeout=CHECK_INTERNET_TIMEOUT, verify=VERIFY_SSL):
|
|
514
573
|
try:
|
|
515
|
-
_ = req.get(url, headers={'User-Agent':
|
|
574
|
+
_ = req.get(url, headers={'User-Agent': USER_AGENT}, timeout=timeout, verify=verify)
|
|
516
575
|
return True
|
|
517
576
|
except req.RequestException as e:
|
|
518
577
|
print(f"* No connectivity, please check your network:\n\n{e}")
|
|
@@ -930,7 +989,7 @@ def reload_secrets_signal_handler(sig, frame):
|
|
|
930
989
|
if LOGIN_REQUEST_BODY_FILE:
|
|
931
990
|
if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
|
|
932
991
|
try:
|
|
933
|
-
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN =
|
|
992
|
+
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN = parse_login_request_body_file(LOGIN_REQUEST_BODY_FILE)
|
|
934
993
|
except Exception as e:
|
|
935
994
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
|
|
936
995
|
else:
|
|
@@ -1091,30 +1150,10 @@ def get_random_user_agent() -> str:
|
|
|
1091
1150
|
return ""
|
|
1092
1151
|
|
|
1093
1152
|
|
|
1094
|
-
#
|
|
1095
|
-
def
|
|
1096
|
-
data = data.replace(" ", "")
|
|
1097
|
-
return bytes.fromhex(data)
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
# Creates a TOTP object using a secret derived from transformed cipher bytes
|
|
1101
|
-
def generate_totp(ua: str):
|
|
1102
|
-
import pyotp
|
|
1103
|
-
|
|
1104
|
-
secret_cipher_bytes = [
|
|
1105
|
-
12, 56, 76, 33, 88, 44, 88, 33,
|
|
1106
|
-
78, 78, 11, 66, 22, 22, 55, 69, 54,
|
|
1107
|
-
]
|
|
1108
|
-
|
|
1109
|
-
transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
|
|
1110
|
-
joined = "".join(str(num) for num in transformed)
|
|
1111
|
-
utf8_bytes = joined.encode("utf-8")
|
|
1112
|
-
hex_str = "".join(format(b, 'x') for b in utf8_bytes)
|
|
1113
|
-
secret_bytes = hex_to_bytes(hex_str)
|
|
1114
|
-
secret = base64.b32encode(secret_bytes).decode().rstrip('=')
|
|
1153
|
+
# Returns Spotify edge-server Unix time
|
|
1154
|
+
def fetch_server_time(session: req.Session, ua: str) -> int:
|
|
1115
1155
|
|
|
1116
1156
|
headers = {
|
|
1117
|
-
"Host": "open.spotify.com",
|
|
1118
1157
|
"User-Agent": ua,
|
|
1119
1158
|
"Accept": "*/*",
|
|
1120
1159
|
}
|
|
@@ -1123,28 +1162,38 @@ def generate_totp(ua: str):
|
|
|
1123
1162
|
if platform.system() != 'Windows':
|
|
1124
1163
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1125
1164
|
signal.alarm(FUNCTION_TIMEOUT + 2)
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1165
|
+
response = session.head("https://open.spotify.com/", headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1166
|
+
response.raise_for_status()
|
|
1167
|
+
except TimeoutException as e:
|
|
1168
|
+
raise Exception(f"fetch_server_time() head network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
|
|
1169
|
+
except Exception as e:
|
|
1170
|
+
raise Exception(f"fetch_server_time() head network request error: {e}")
|
|
1129
1171
|
finally:
|
|
1130
1172
|
if platform.system() != 'Windows':
|
|
1131
1173
|
signal.alarm(0)
|
|
1132
1174
|
|
|
1133
|
-
|
|
1175
|
+
return int(parsedate_to_datetime(response.headers["Date"]).timestamp())
|
|
1134
1176
|
|
|
1135
|
-
json_data = resp.json()
|
|
1136
|
-
server_time = json_data.get("serverTime")
|
|
1137
1177
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1178
|
+
# Creates a TOTP object using a secret derived from transformed cipher bytes
|
|
1179
|
+
def generate_totp():
|
|
1180
|
+
import pyotp
|
|
1181
|
+
|
|
1182
|
+
secret_cipher_bytes = [
|
|
1183
|
+
12, 56, 76, 33, 88, 44, 88, 33,
|
|
1184
|
+
78, 78, 11, 66, 22, 22, 55, 69, 54,
|
|
1185
|
+
]
|
|
1140
1186
|
|
|
1141
|
-
|
|
1187
|
+
transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
|
|
1188
|
+
joined = "".join(str(num) for num in transformed)
|
|
1189
|
+
hex_str = joined.encode().hex()
|
|
1190
|
+
secret = base64.b32encode(bytes.fromhex(hex_str)).decode().rstrip("=")
|
|
1142
1191
|
|
|
1143
|
-
return
|
|
1192
|
+
return pyotp.TOTP(secret, digits=6, interval=30)
|
|
1144
1193
|
|
|
1145
1194
|
|
|
1146
|
-
#
|
|
1147
|
-
def
|
|
1195
|
+
# Refreshes the Spotify access token using the sp_dc cookie, tries first with mode "transport" and if needed with "init"
|
|
1196
|
+
def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
|
|
1148
1197
|
transport = True
|
|
1149
1198
|
init = True
|
|
1150
1199
|
session = req.Session()
|
|
@@ -1152,8 +1201,8 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1152
1201
|
data: dict = {}
|
|
1153
1202
|
token = ""
|
|
1154
1203
|
|
|
1155
|
-
|
|
1156
|
-
totp_obj
|
|
1204
|
+
server_time = fetch_server_time(session, USER_AGENT)
|
|
1205
|
+
totp_obj = generate_totp()
|
|
1157
1206
|
client_time = int(time_ns() / 1000 / 1000)
|
|
1158
1207
|
otp_value = totp_obj.at(server_time)
|
|
1159
1208
|
|
|
@@ -1165,14 +1214,20 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1165
1214
|
"totpVer": 5,
|
|
1166
1215
|
"sTime": server_time,
|
|
1167
1216
|
"cTime": client_time,
|
|
1217
|
+
"buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
|
|
1218
|
+
"buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
|
|
1168
1219
|
}
|
|
1169
1220
|
|
|
1170
1221
|
headers = {
|
|
1171
|
-
"User-Agent":
|
|
1222
|
+
"User-Agent": USER_AGENT,
|
|
1172
1223
|
"Accept": "application/json",
|
|
1224
|
+
"Referer": "https://open.spotify.com/",
|
|
1225
|
+
"App-Platform": "WebPlayer",
|
|
1173
1226
|
"Cookie": f"sp_dc={sp_dc}",
|
|
1174
1227
|
}
|
|
1175
1228
|
|
|
1229
|
+
last_err = ""
|
|
1230
|
+
|
|
1176
1231
|
try:
|
|
1177
1232
|
if platform.system() != "Windows":
|
|
1178
1233
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
@@ -1183,13 +1238,14 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1183
1238
|
data = response.json()
|
|
1184
1239
|
token = data.get("accessToken", "")
|
|
1185
1240
|
|
|
1186
|
-
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1241
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError) as e:
|
|
1187
1242
|
transport = False
|
|
1243
|
+
last_err = str(e)
|
|
1188
1244
|
finally:
|
|
1189
1245
|
if platform.system() != "Windows":
|
|
1190
1246
|
signal.alarm(0)
|
|
1191
1247
|
|
|
1192
|
-
if not transport or (transport and not check_token_validity(token, data.get("clientId", ""),
|
|
1248
|
+
if not transport or (transport and not check_token_validity(token, data.get("clientId", ""), USER_AGENT)):
|
|
1193
1249
|
params["reason"] = "init"
|
|
1194
1250
|
|
|
1195
1251
|
try:
|
|
@@ -1202,58 +1258,52 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1202
1258
|
data = response.json()
|
|
1203
1259
|
token = data.get("accessToken", "")
|
|
1204
1260
|
|
|
1205
|
-
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1261
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError) as e:
|
|
1206
1262
|
init = False
|
|
1263
|
+
last_err = str(e)
|
|
1207
1264
|
finally:
|
|
1208
1265
|
if platform.system() != "Windows":
|
|
1209
1266
|
signal.alarm(0)
|
|
1210
1267
|
|
|
1211
1268
|
if not init or not data or "accessToken" not in data:
|
|
1212
|
-
raise Exception("
|
|
1269
|
+
raise Exception(f"refresh_access_token_from_sp_dc(): Unsuccessful token request{': ' + last_err if last_err else ''}")
|
|
1213
1270
|
|
|
1214
1271
|
return {
|
|
1215
1272
|
"access_token": token,
|
|
1216
1273
|
"expires_at": data["accessTokenExpirationTimestampMs"] // 1000,
|
|
1217
1274
|
"client_id": data.get("clientId", ""),
|
|
1218
|
-
"user_agent": ua,
|
|
1219
1275
|
"length": len(token)
|
|
1220
1276
|
}
|
|
1221
1277
|
|
|
1222
1278
|
|
|
1223
1279
|
# Fetches Spotify access token based on provided SP_DC value
|
|
1224
1280
|
def spotify_get_access_token_from_sp_dc(sp_dc: str):
|
|
1225
|
-
global SP_CACHED_ACCESS_TOKEN, SP_ACCESS_TOKEN_EXPIRES_AT, SP_CACHED_CLIENT_ID
|
|
1281
|
+
global SP_CACHED_ACCESS_TOKEN, SP_ACCESS_TOKEN_EXPIRES_AT, SP_CACHED_CLIENT_ID
|
|
1226
1282
|
|
|
1227
1283
|
now = time.time()
|
|
1228
1284
|
|
|
1229
|
-
if SP_CACHED_ACCESS_TOKEN and now < SP_ACCESS_TOKEN_EXPIRES_AT and check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID,
|
|
1285
|
+
if SP_CACHED_ACCESS_TOKEN and now < SP_ACCESS_TOKEN_EXPIRES_AT and check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, USER_AGENT):
|
|
1230
1286
|
return SP_CACHED_ACCESS_TOKEN
|
|
1231
1287
|
|
|
1232
1288
|
max_retries = TOKEN_MAX_RETRIES
|
|
1233
1289
|
retry = 0
|
|
1234
1290
|
|
|
1235
1291
|
while retry < max_retries:
|
|
1236
|
-
token_data =
|
|
1292
|
+
token_data = refresh_access_token_from_sp_dc(sp_dc)
|
|
1237
1293
|
token = token_data["access_token"]
|
|
1238
1294
|
client_id = token_data.get("client_id", "")
|
|
1239
|
-
user_agent = token_data.get("user_agent", get_random_user_agent())
|
|
1240
1295
|
length = token_data["length"]
|
|
1241
1296
|
|
|
1242
1297
|
SP_CACHED_ACCESS_TOKEN = token
|
|
1243
1298
|
SP_ACCESS_TOKEN_EXPIRES_AT = token_data["expires_at"]
|
|
1244
1299
|
SP_CACHED_CLIENT_ID = client_id
|
|
1245
|
-
SP_CACHED_USER_AGENT = user_agent
|
|
1246
1300
|
|
|
1247
|
-
if SP_CACHED_ACCESS_TOKEN is None or not check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID,
|
|
1301
|
+
if SP_CACHED_ACCESS_TOKEN is None or not check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, USER_AGENT):
|
|
1248
1302
|
retry += 1
|
|
1249
1303
|
time.sleep(TOKEN_RETRY_TIMEOUT)
|
|
1250
1304
|
else:
|
|
1251
|
-
# print("* Token is valid")
|
|
1252
1305
|
break
|
|
1253
1306
|
|
|
1254
|
-
# print("Spotify Access Token:", SP_CACHED_ACCESS_TOKEN)
|
|
1255
|
-
# print("Token expires at:", time.ctime(SP_TOKEN_EXPIRES_AT))
|
|
1256
|
-
|
|
1257
1307
|
if retry == max_retries:
|
|
1258
1308
|
if SP_CACHED_ACCESS_TOKEN is not None:
|
|
1259
1309
|
print(f"* Token appears to be still invalid after {max_retries} attempts, returning token anyway")
|
|
@@ -1326,13 +1376,15 @@ def encode_nested_field(tag, nested_bytes):
|
|
|
1326
1376
|
# Builds the Spotify Protobuf login request body
|
|
1327
1377
|
def build_spotify_auth_protobuf(device_id, system_id, user_uri_id, refresh_token):
|
|
1328
1378
|
"""
|
|
1329
|
-
|
|
1330
|
-
1:
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1379
|
+
{
|
|
1380
|
+
1: {
|
|
1381
|
+
1: "device_id",
|
|
1382
|
+
2: "system_id"
|
|
1383
|
+
},
|
|
1384
|
+
100: {
|
|
1385
|
+
1: "user_uri_id",
|
|
1386
|
+
2: "refresh_token"
|
|
1387
|
+
}
|
|
1336
1388
|
}
|
|
1337
1389
|
"""
|
|
1338
1390
|
device_info_msg = encode_string_field(1, device_id) + encode_string_field(2, system_id)
|
|
@@ -1397,7 +1449,7 @@ def parse_protobuf_message(data):
|
|
|
1397
1449
|
|
|
1398
1450
|
# Parses the Protobuf-encoded login request body file (as dumped for example by Proxyman) and returns a tuple:
|
|
1399
1451
|
# (device_id, system_id, user_uri_id, refresh_token)
|
|
1400
|
-
def
|
|
1452
|
+
def parse_login_request_body_file(file_path):
|
|
1401
1453
|
"""
|
|
1402
1454
|
{
|
|
1403
1455
|
1: {
|
|
@@ -1474,6 +1526,26 @@ def ensure_dict(value):
|
|
|
1474
1526
|
# Parses the Protobuf-encoded client token request body file (as dumped for example by Proxyman) and returns a tuple:
|
|
1475
1527
|
# (app_version, device_id, system_id, cpu_arch, os_build, platform, os_major, os_minor, client_model)
|
|
1476
1528
|
def parse_clienttoken_request_body_file(file_path):
|
|
1529
|
+
"""
|
|
1530
|
+
1: 1 (const)
|
|
1531
|
+
2: {
|
|
1532
|
+
1: "app_version"
|
|
1533
|
+
2: "device_id"
|
|
1534
|
+
3: {
|
|
1535
|
+
1: {
|
|
1536
|
+
4: {
|
|
1537
|
+
1: "cpu_arch"
|
|
1538
|
+
3: "os_build"
|
|
1539
|
+
4: "platform"
|
|
1540
|
+
5: "os_major"
|
|
1541
|
+
6: "os_minor"
|
|
1542
|
+
8: "client_model"
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
2: "system_id"
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
"""
|
|
1477
1549
|
|
|
1478
1550
|
with open(file_path, "rb") as f:
|
|
1479
1551
|
data = f.read()
|
|
@@ -1534,13 +1606,20 @@ def build_clienttoken_request_protobuf(app_version, device_id, system_id, cpu_ar
|
|
|
1534
1606
|
"""
|
|
1535
1607
|
1: 1 (const)
|
|
1536
1608
|
2: {
|
|
1537
|
-
1: app_version
|
|
1538
|
-
2: device_id
|
|
1609
|
+
1: "app_version"
|
|
1610
|
+
2: "device_id"
|
|
1539
1611
|
3: {
|
|
1540
1612
|
1: {
|
|
1541
|
-
4:
|
|
1613
|
+
4: {
|
|
1614
|
+
1: "cpu_arch"
|
|
1615
|
+
3: "os_build"
|
|
1616
|
+
4: "platform"
|
|
1617
|
+
5: "os_major"
|
|
1618
|
+
6: "os_minor"
|
|
1619
|
+
8: "client_model"
|
|
1620
|
+
}
|
|
1542
1621
|
}
|
|
1543
|
-
2: system_id
|
|
1622
|
+
2: "system_id"
|
|
1544
1623
|
}
|
|
1545
1624
|
}
|
|
1546
1625
|
"""
|
|
@@ -1598,8 +1677,10 @@ def spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refr
|
|
|
1598
1677
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1599
1678
|
signal.alarm(FUNCTION_TIMEOUT + 2)
|
|
1600
1679
|
response = req.post(LOGIN_URL, headers=headers, data=protobuf_body, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1601
|
-
except
|
|
1680
|
+
except TimeoutException as e:
|
|
1602
1681
|
raise Exception(f"spotify_get_access_token_from_client() network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
|
|
1682
|
+
except Exception as e:
|
|
1683
|
+
raise Exception(f"spotify_get_access_token_from_client() network request error: {e}")
|
|
1603
1684
|
finally:
|
|
1604
1685
|
if platform.system() != 'Windows':
|
|
1605
1686
|
signal.alarm(0)
|
|
@@ -1610,6 +1691,22 @@ def spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refr
|
|
|
1610
1691
|
elif response.headers.get("client-token-error") == "EXPIRED_CLIENTTOKEN":
|
|
1611
1692
|
raise Exception(f"Request failed with status {response.status_code}: expired client token")
|
|
1612
1693
|
|
|
1694
|
+
try:
|
|
1695
|
+
error_json = response.json()
|
|
1696
|
+
except ValueError:
|
|
1697
|
+
error_json = {}
|
|
1698
|
+
|
|
1699
|
+
if error_json.get("error") == "invalid_grant":
|
|
1700
|
+
desc = error_json.get("error_description", "")
|
|
1701
|
+
if "refresh token" in desc.lower() and "revoked" in desc.lower():
|
|
1702
|
+
raise Exception(f"Request failed with status {response.status_code}: refresh token has been revoked")
|
|
1703
|
+
elif "refresh token" in desc.lower() and "expired" in desc.lower():
|
|
1704
|
+
raise Exception(f"Request failed with status {response.status_code}: refresh token has expired")
|
|
1705
|
+
elif "invalid refresh token" in desc.lower():
|
|
1706
|
+
raise Exception(f"Request failed with status {response.status_code}: refresh token is invalid")
|
|
1707
|
+
else:
|
|
1708
|
+
raise Exception(f"Request failed with status {response.status_code}: invalid grant during refresh ({desc})")
|
|
1709
|
+
|
|
1613
1710
|
raise Exception(f"Request failed with status code {response.status_code}\nResponse Headers: {response.headers}\nResponse Content (raw): {response.content}\nResponse text: {response.text}")
|
|
1614
1711
|
|
|
1615
1712
|
parsed = parse_protobuf_message(response.content)
|
|
@@ -1670,8 +1767,10 @@ def spotify_get_client_token(app_version, device_id, system_id, **device_overrid
|
|
|
1670
1767
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1671
1768
|
signal.alarm(FUNCTION_TIMEOUT + 2)
|
|
1672
1769
|
response = req.post(CLIENTTOKEN_URL, headers=headers, data=body, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1673
|
-
except
|
|
1770
|
+
except TimeoutException as e:
|
|
1674
1771
|
raise Exception(f"spotify_get_client_token() network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
|
|
1772
|
+
except Exception as e:
|
|
1773
|
+
raise Exception(f"spotify_get_client_token() network request error: {e}")
|
|
1675
1774
|
finally:
|
|
1676
1775
|
if platform.system() != 'Windows':
|
|
1677
1776
|
signal.alarm(0)
|
|
@@ -1738,16 +1837,14 @@ def spotify_get_access_token_from_client_auto(device_id, system_id, user_uri_id,
|
|
|
1738
1837
|
# Fetches list of Spotify friends
|
|
1739
1838
|
def spotify_get_friends_json(access_token):
|
|
1740
1839
|
url = "https://guc-spclient.spotify.com/presence-view/v1/buddylist"
|
|
1741
|
-
headers = {
|
|
1840
|
+
headers = {
|
|
1841
|
+
"Authorization": f"Bearer {access_token}",
|
|
1842
|
+
"User-Agent": USER_AGENT
|
|
1843
|
+
}
|
|
1742
1844
|
|
|
1743
1845
|
if TOKEN_SOURCE == "cookie":
|
|
1744
1846
|
headers.update({
|
|
1745
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1746
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1747
|
-
})
|
|
1748
|
-
elif TOKEN_SOURCE == "client":
|
|
1749
|
-
headers.update({
|
|
1750
|
-
"User-Agent": USER_AGENT
|
|
1847
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1751
1848
|
})
|
|
1752
1849
|
|
|
1753
1850
|
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
@@ -1836,8 +1933,8 @@ def spotify_list_friends(friend_activity):
|
|
|
1836
1933
|
|
|
1837
1934
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
1838
1935
|
|
|
1839
|
-
print(f"Apple
|
|
1840
|
-
print(f"YouTube Music
|
|
1936
|
+
print(f"Apple Music URL:\t\t{apple_search_url}")
|
|
1937
|
+
print(f"YouTube Music URL:\t\t{youtube_music_search_url}")
|
|
1841
1938
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
1842
1939
|
|
|
1843
1940
|
print(f"\nLast activity:\t\t\t{get_date_from_ts(float(str(sp_ts)[0:-3]))} ({calculate_timespan(int(time.time()), datetime.fromtimestamp(float(str(sp_ts)[0:-3])))} ago)")
|
|
@@ -1869,16 +1966,14 @@ def spotify_get_friend_info(friend_activity, uri):
|
|
|
1869
1966
|
def spotify_get_track_info(access_token, track_uri):
|
|
1870
1967
|
track_id = track_uri.split(':', 2)[2]
|
|
1871
1968
|
url = "https://api.spotify.com/v1/tracks/" + track_id
|
|
1872
|
-
headers = {
|
|
1969
|
+
headers = {
|
|
1970
|
+
"Authorization": f"Bearer {access_token}",
|
|
1971
|
+
"User-Agent": USER_AGENT
|
|
1972
|
+
}
|
|
1873
1973
|
|
|
1874
1974
|
if TOKEN_SOURCE == "cookie":
|
|
1875
1975
|
headers.update({
|
|
1876
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1877
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1878
|
-
})
|
|
1879
|
-
elif TOKEN_SOURCE == "client":
|
|
1880
|
-
headers.update({
|
|
1881
|
-
"User-Agent": USER_AGENT
|
|
1976
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1882
1977
|
})
|
|
1883
1978
|
# add si parameter so link opens in native Spotify app after clicking
|
|
1884
1979
|
si = "?si=1"
|
|
@@ -1903,16 +1998,14 @@ def spotify_get_track_info(access_token, track_uri):
|
|
|
1903
1998
|
def spotify_get_playlist_info(access_token, playlist_uri):
|
|
1904
1999
|
playlist_id = playlist_uri.split(':', 2)[2]
|
|
1905
2000
|
url = f"https://api.spotify.com/v1/playlists/{playlist_id}?fields=name,owner,followers,external_urls"
|
|
1906
|
-
headers = {
|
|
2001
|
+
headers = {
|
|
2002
|
+
"Authorization": f"Bearer {access_token}",
|
|
2003
|
+
"User-Agent": USER_AGENT
|
|
2004
|
+
}
|
|
1907
2005
|
|
|
1908
2006
|
if TOKEN_SOURCE == "cookie":
|
|
1909
2007
|
headers.update({
|
|
1910
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1911
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1912
|
-
})
|
|
1913
|
-
elif TOKEN_SOURCE == "client":
|
|
1914
|
-
headers.update({
|
|
1915
|
-
"User-Agent": USER_AGENT
|
|
2008
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1916
2009
|
})
|
|
1917
2010
|
# add si parameter so link opens in native Spotify app after clicking
|
|
1918
2011
|
si = "?si=1"
|
|
@@ -1934,16 +2027,14 @@ def spotify_get_playlist_info(access_token, playlist_uri):
|
|
|
1934
2027
|
# Gets basic information about access token owner
|
|
1935
2028
|
def spotify_get_current_user(access_token) -> dict | None:
|
|
1936
2029
|
url = "https://api.spotify.com/v1/me"
|
|
1937
|
-
headers = {
|
|
2030
|
+
headers = {
|
|
2031
|
+
"Authorization": f"Bearer {access_token}",
|
|
2032
|
+
"User-Agent": USER_AGENT
|
|
2033
|
+
}
|
|
1938
2034
|
|
|
1939
2035
|
if TOKEN_SOURCE == "cookie":
|
|
1940
2036
|
headers.update({
|
|
1941
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1942
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1943
|
-
})
|
|
1944
|
-
elif TOKEN_SOURCE == "client":
|
|
1945
|
-
headers.update({
|
|
1946
|
-
"User-Agent": USER_AGENT
|
|
2037
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1947
2038
|
})
|
|
1948
2039
|
|
|
1949
2040
|
if platform.system() != 'Windows':
|
|
@@ -1972,6 +2063,29 @@ def spotify_get_current_user(access_token) -> dict | None:
|
|
|
1972
2063
|
signal.alarm(0)
|
|
1973
2064
|
|
|
1974
2065
|
|
|
2066
|
+
# Checks if a Spotify user URI ID has been deleted
|
|
2067
|
+
def is_user_removed(access_token, user_uri_id):
|
|
2068
|
+
url = f"https://api.spotify.com/v1/users/{user_uri_id}"
|
|
2069
|
+
|
|
2070
|
+
headers = {
|
|
2071
|
+
"Authorization": f"Bearer {access_token}",
|
|
2072
|
+
"User-Agent": USER_AGENT
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if TOKEN_SOURCE == "cookie":
|
|
2076
|
+
headers.update({
|
|
2077
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
try:
|
|
2081
|
+
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
2082
|
+
if response.status_code == 404:
|
|
2083
|
+
return True
|
|
2084
|
+
return False
|
|
2085
|
+
except Exception:
|
|
2086
|
+
return False
|
|
2087
|
+
|
|
2088
|
+
|
|
1975
2089
|
def spotify_macos_play_song(sp_track_uri_id, method=SPOTIFY_MACOS_PLAYING_METHOD):
|
|
1976
2090
|
if method == "apple-script": # apple-script
|
|
1977
2091
|
script = f'tell app "Spotify" to play track "spotify:track:{sp_track_uri_id}"'
|
|
@@ -2064,7 +2178,7 @@ def resolve_executable(path):
|
|
|
2064
2178
|
raise FileNotFoundError(f"Could not find executable '{path}'")
|
|
2065
2179
|
|
|
2066
2180
|
|
|
2067
|
-
#
|
|
2181
|
+
# Monitors music activity of the specified Spotify friend's user URI ID
|
|
2068
2182
|
def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
2069
2183
|
global SP_CACHED_ACCESS_TOKEN
|
|
2070
2184
|
sp_active_ts_start = 0
|
|
@@ -2084,6 +2198,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2084
2198
|
error_500_start_ts = 0
|
|
2085
2199
|
error_network_issue_counter = 0
|
|
2086
2200
|
error_network_issue_start_ts = 0
|
|
2201
|
+
sp_accessToken = ""
|
|
2087
2202
|
|
|
2088
2203
|
try:
|
|
2089
2204
|
if csv_file_name:
|
|
@@ -2135,25 +2250,25 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2135
2250
|
if TOKEN_SOURCE == 'cookie' and '401' in err:
|
|
2136
2251
|
SP_CACHED_ACCESS_TOKEN = None
|
|
2137
2252
|
|
|
2138
|
-
client_errs = ['access token', 'invalid client token', 'expired client token']
|
|
2139
|
-
cookie_errs = ['access token', '
|
|
2253
|
+
client_errs = ['access token', 'invalid client token', 'expired client token', 'refresh token has been revoked', 'refresh token has expired', 'refresh token is invalid', 'invalid grant during refresh']
|
|
2254
|
+
cookie_errs = ['access token', 'unauthorized']
|
|
2140
2255
|
|
|
2141
2256
|
if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
|
|
2142
|
-
print(f"* Error: client
|
|
2257
|
+
print(f"* Error: client or refresh token may be invalid or expired!")
|
|
2143
2258
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2144
|
-
m_subject = f"spotify_monitor: client
|
|
2145
|
-
m_body = f"Client
|
|
2146
|
-
m_body_html = f"<html><head></head><body>Client
|
|
2259
|
+
m_subject = f"spotify_monitor: client or refresh token may be invalid or expired! (uri: {user_uri_id})"
|
|
2260
|
+
m_body = f"Client or refresh token may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2261
|
+
m_body_html = f"<html><head></head><body>Client or refresh token may be invalid or expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2147
2262
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2148
2263
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2149
2264
|
email_sent = True
|
|
2150
2265
|
|
|
2151
2266
|
elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
|
|
2152
|
-
print(f"* Error: sp_dc
|
|
2267
|
+
print(f"* Error: sp_dc may be invalid or expired!")
|
|
2153
2268
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2154
|
-
m_subject = f"spotify_monitor: sp_dc
|
|
2155
|
-
m_body = f"sp_dc
|
|
2156
|
-
m_body_html = f"<html><head></head><body>sp_dc
|
|
2269
|
+
m_subject = f"spotify_monitor: sp_dc may be invalid or expired! (uri: {user_uri_id})"
|
|
2270
|
+
m_body = f"sp_dc may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2271
|
+
m_body_html = f"<html><head></head><body>sp_dc may be invalid or expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2157
2272
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2158
2273
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2159
2274
|
email_sent = True
|
|
@@ -2262,8 +2377,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2262
2377
|
|
|
2263
2378
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
2264
2379
|
|
|
2265
|
-
print(f"Apple
|
|
2266
|
-
print(f"YouTube Music
|
|
2380
|
+
print(f"Apple Music URL:\t\t{apple_search_url}")
|
|
2381
|
+
print(f"YouTube Music URL:\t\t{youtube_music_search_url}")
|
|
2267
2382
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
2268
2383
|
|
|
2269
2384
|
if not is_playlist:
|
|
@@ -2290,8 +2405,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2290
2405
|
|
|
2291
2406
|
if ACTIVE_NOTIFICATION:
|
|
2292
2407
|
m_subject = f"Spotify user {sp_username} is active: '{sp_artist} - {sp_track}'"
|
|
2293
|
-
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple
|
|
2294
|
-
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple
|
|
2408
|
+
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2409
|
+
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
2295
2410
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2296
2411
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2297
2412
|
|
|
@@ -2316,7 +2431,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2316
2431
|
|
|
2317
2432
|
email_sent = False
|
|
2318
2433
|
|
|
2319
|
-
#
|
|
2434
|
+
# Primary loop
|
|
2320
2435
|
while True:
|
|
2321
2436
|
|
|
2322
2437
|
while True:
|
|
@@ -2382,25 +2497,25 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2382
2497
|
elif not error_500_start_ts and not error_network_issue_start_ts:
|
|
2383
2498
|
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: '{e}'")
|
|
2384
2499
|
|
|
2385
|
-
client_errs = ['access token', 'invalid client token', 'expired client token']
|
|
2386
|
-
cookie_errs = ['access token', '
|
|
2500
|
+
client_errs = ['access token', 'invalid client token', 'expired client token', 'refresh token has been revoked', 'refresh token has expired', 'refresh token is invalid', 'invalid grant during refresh']
|
|
2501
|
+
cookie_errs = ['access token', 'unauthorized']
|
|
2387
2502
|
|
|
2388
2503
|
if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
|
|
2389
|
-
print(f"* Error: client
|
|
2504
|
+
print(f"* Error: client or refresh token may be invalid or expired!")
|
|
2390
2505
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2391
|
-
m_subject = f"spotify_monitor: client
|
|
2392
|
-
m_body = f"Client
|
|
2393
|
-
m_body_html = f"<html><head></head><body>Client
|
|
2506
|
+
m_subject = f"spotify_monitor: client or refresh token may be invalid or expired! (uri: {user_uri_id})"
|
|
2507
|
+
m_body = f"Client or refresh token may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2508
|
+
m_body_html = f"<html><head></head><body>Client or refresh token may be invalid or expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2394
2509
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2395
2510
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2396
2511
|
email_sent = True
|
|
2397
2512
|
|
|
2398
2513
|
elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
|
|
2399
|
-
print(f"* Error: sp_dc
|
|
2514
|
+
print(f"* Error: sp_dc may be invalid or expired!")
|
|
2400
2515
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2401
|
-
m_subject = f"spotify_monitor: sp_dc
|
|
2402
|
-
m_body = f"sp_dc
|
|
2403
|
-
m_body_html = f"<html><head></head><body>sp_dc
|
|
2516
|
+
m_subject = f"spotify_monitor: sp_dc may be invalid or expired! (uri: {user_uri_id})"
|
|
2517
|
+
m_body = f"sp_dc may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2518
|
+
m_body_html = f"<html><head></head><body>sp_dc may be invalid or expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2404
2519
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2405
2520
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2406
2521
|
email_sent = True
|
|
@@ -2409,15 +2524,24 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2409
2524
|
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
2410
2525
|
|
|
2411
2526
|
if sp_found is False:
|
|
2412
|
-
# User disappeared from the Spotify's friend list
|
|
2527
|
+
# User has disappeared from the Spotify's friend list or account has been removed
|
|
2413
2528
|
if user_not_found is False:
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2529
|
+
if is_user_removed(sp_accessToken, user_uri_id):
|
|
2530
|
+
print(f"Spotify user '{user_uri_id}' ({sp_username}) was probably removed! Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
2531
|
+
if ERROR_NOTIFICATION:
|
|
2532
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) was probably removed!"
|
|
2533
|
+
m_body = f"Spotify user {user_uri_id} ({sp_username}) was probably removed\nRetrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2534
|
+
m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) was probably removed<br>Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2535
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2536
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2537
|
+
else:
|
|
2538
|
+
print(f"Spotify user '{user_uri_id}' ({sp_username}) has disappeared - make sure your friend is followed and has activity sharing enabled. Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
2539
|
+
if ERROR_NOTIFICATION:
|
|
2540
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) has disappeared!"
|
|
2541
|
+
m_body = f"Spotify user {user_uri_id} ({sp_username}) has disappeared - make sure your friend is followed and has activity sharing enabled\nRetrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2542
|
+
m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) has disappeared - make sure your friend is followed and has activity sharing enabled<br>Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2543
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2544
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2421
2545
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2422
2546
|
user_not_found = True
|
|
2423
2547
|
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
@@ -2425,11 +2549,11 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2425
2549
|
else:
|
|
2426
2550
|
# User reappeared in the Spotify's friend list
|
|
2427
2551
|
if user_not_found is True:
|
|
2428
|
-
print(f"Spotify user {user_uri_id} ({sp_username})
|
|
2552
|
+
print(f"Spotify user {user_uri_id} ({sp_username}) has reappeared!")
|
|
2429
2553
|
if ERROR_NOTIFICATION:
|
|
2430
|
-
m_subject = f"Spotify user {user_uri_id} ({sp_username})
|
|
2431
|
-
m_body = f"Spotify user {user_uri_id} ({sp_username})
|
|
2432
|
-
m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username})
|
|
2554
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) has reappeared!"
|
|
2555
|
+
m_body = f"Spotify user {user_uri_id} ({sp_username}) has reappeared!{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2556
|
+
m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) has reappeared!{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2433
2557
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2434
2558
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2435
2559
|
print_cur_ts("Timestamp:\t\t\t")
|
|
@@ -2561,8 +2685,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2561
2685
|
|
|
2562
2686
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
2563
2687
|
|
|
2564
|
-
print(f"Apple
|
|
2565
|
-
print(f"YouTube Music
|
|
2688
|
+
print(f"Apple Music URL:\t\t{apple_search_url}")
|
|
2689
|
+
print(f"YouTube Music URL:\t\t{youtube_music_search_url}")
|
|
2566
2690
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
2567
2691
|
|
|
2568
2692
|
if not is_playlist:
|
|
@@ -2581,6 +2705,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2581
2705
|
listened_songs = 1
|
|
2582
2706
|
skipped_songs = 0
|
|
2583
2707
|
looped_songs = 0
|
|
2708
|
+
song_on_loop = 1
|
|
2584
2709
|
|
|
2585
2710
|
print(f"\n*** Friend got ACTIVE after being offline for {calculate_timespan(int(sp_active_ts_start), int(sp_active_ts_stop))} ({get_date_from_ts(sp_active_ts_stop)})")
|
|
2586
2711
|
m_subject = f"Spotify user {sp_username} is active: '{sp_artist} - {sp_track}' (after {calculate_timespan(int(sp_active_ts_start), int(sp_active_ts_stop), show_seconds=False)} - {get_short_date_from_ts(sp_active_ts_stop)})"
|
|
@@ -2597,8 +2722,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2597
2722
|
sp_active_ts_start = sp_active_ts_start_old
|
|
2598
2723
|
sp_active_ts_stop = 0
|
|
2599
2724
|
|
|
2600
|
-
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple
|
|
2601
|
-
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple
|
|
2725
|
+
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}{friend_active_m_body}\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2726
|
+
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a>{friend_active_m_body_html}<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
2602
2727
|
|
|
2603
2728
|
if ACTIVE_NOTIFICATION:
|
|
2604
2729
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2612,16 +2737,16 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2612
2737
|
|
|
2613
2738
|
if (TRACK_NOTIFICATION and on_the_list and not email_sent) or (SONG_NOTIFICATION and not email_sent):
|
|
2614
2739
|
m_subject = f"Spotify user {sp_username}: '{sp_artist} - {sp_track}'"
|
|
2615
|
-
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple
|
|
2616
|
-
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple
|
|
2740
|
+
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2741
|
+
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
2617
2742
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2618
2743
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2619
2744
|
email_sent = True
|
|
2620
2745
|
|
|
2621
2746
|
if song_on_loop == SONG_ON_LOOP_VALUE and SONG_ON_LOOP_NOTIFICATION:
|
|
2622
2747
|
m_subject = f"Spotify user {sp_username} plays song on loop: '{sp_artist} - {sp_track}'"
|
|
2623
|
-
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple
|
|
2624
|
-
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple
|
|
2748
|
+
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nUser plays song on LOOP ({song_on_loop} times)\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2749
|
+
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>User plays song on LOOP (<b>{song_on_loop}</b> times)<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
2625
2750
|
if not email_sent:
|
|
2626
2751
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2627
2752
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
@@ -2688,8 +2813,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2688
2813
|
spotify_linux_play_pause("pause")
|
|
2689
2814
|
if INACTIVE_NOTIFICATION:
|
|
2690
2815
|
m_subject = f"Spotify user {sp_username} is inactive: '{sp_artist} - {sp_track}' (after {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start), show_seconds=False)}: {get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True)})"
|
|
2691
|
-
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple
|
|
2692
|
-
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple
|
|
2816
|
+
m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nFriend got inactive after listening to music for {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}\nFriend played music from {get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True, between_sep=' to ')}{listened_songs_mbody}\n\nLast activity: {get_date_from_ts(sp_active_ts_stop)}\nInactivity timer: {display_time(SPOTIFY_INACTIVITY_CHECK)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2817
|
+
m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>Friend got inactive after listening to music for <b>{calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}</b><br>Friend played music from <b>{get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True, between_sep='</b> to <b>')}</b>{listened_songs_mbody_html}<br><br>Last activity: <b>{get_date_from_ts(sp_active_ts_stop)}</b><br>Inactivity timer: {display_time(SPOTIFY_INACTIVITY_CHECK)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
2693
2818
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2694
2819
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2695
2820
|
email_sent = True
|
|
@@ -2701,6 +2826,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2701
2826
|
listened_songs = 0
|
|
2702
2827
|
looped_songs = 0
|
|
2703
2828
|
skipped_songs = 0
|
|
2829
|
+
song_on_loop = 0
|
|
2704
2830
|
print_cur_ts("\nTimestamp:\t\t\t")
|
|
2705
2831
|
|
|
2706
2832
|
if LIVENESS_CHECK_COUNTER and alive_counter >= LIVENESS_CHECK_COUNTER:
|
|
@@ -2728,7 +2854,10 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2728
2854
|
# User is not found in the Spotify's friend list just after starting the tool
|
|
2729
2855
|
else:
|
|
2730
2856
|
if user_not_found is False:
|
|
2731
|
-
|
|
2857
|
+
if is_user_removed(sp_accessToken, user_uri_id):
|
|
2858
|
+
print(f"User '{user_uri_id}' does not exist! Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
2859
|
+
else:
|
|
2860
|
+
print(f"User '{user_uri_id}' not found - make sure your friend is followed and has activity sharing enabled. Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
2732
2861
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2733
2862
|
user_not_found = True
|
|
2734
2863
|
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
@@ -2736,7 +2865,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2736
2865
|
|
|
2737
2866
|
|
|
2738
2867
|
def main():
|
|
2739
|
-
global CLI_CONFIG_PATH, DOTENV_FILE, LIVENESS_CHECK_COUNTER, LOGIN_REQUEST_BODY_FILE, CLIENTTOKEN_REQUEST_BODY_FILE, REFRESH_TOKEN, LOGIN_URL, USER_AGENT, DEVICE_ID, SYSTEM_ID, USER_URI_ID, SP_DC_COOKIE, CSV_FILE, MONITOR_LIST_FILE, FILE_SUFFIX, DISABLE_LOGGING, SP_LOGFILE, ACTIVE_NOTIFICATION, INACTIVE_NOTIFICATION, TRACK_NOTIFICATION, SONG_NOTIFICATION, SONG_ON_LOOP_NOTIFICATION, ERROR_NOTIFICATION, SPOTIFY_CHECK_INTERVAL, SPOTIFY_INACTIVITY_CHECK, SPOTIFY_ERROR_INTERVAL, SPOTIFY_DISAPPEARED_CHECK_INTERVAL, TRACK_SONGS, SMTP_PASSWORD, stdout_bck, APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL, TOKEN_SOURCE, ALARM_TIMEOUT, pyotp
|
|
2868
|
+
global CLI_CONFIG_PATH, DOTENV_FILE, LIVENESS_CHECK_COUNTER, LOGIN_REQUEST_BODY_FILE, CLIENTTOKEN_REQUEST_BODY_FILE, REFRESH_TOKEN, LOGIN_URL, USER_AGENT, DEVICE_ID, SYSTEM_ID, USER_URI_ID, SP_DC_COOKIE, CSV_FILE, MONITOR_LIST_FILE, FILE_SUFFIX, DISABLE_LOGGING, SP_LOGFILE, ACTIVE_NOTIFICATION, INACTIVE_NOTIFICATION, TRACK_NOTIFICATION, SONG_NOTIFICATION, SONG_ON_LOOP_NOTIFICATION, ERROR_NOTIFICATION, SPOTIFY_CHECK_INTERVAL, SPOTIFY_INACTIVITY_CHECK, SPOTIFY_ERROR_INTERVAL, SPOTIFY_DISAPPEARED_CHECK_INTERVAL, TRACK_SONGS, SMTP_PASSWORD, stdout_bck, APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL, TOKEN_SOURCE, ALARM_TIMEOUT, pyotp, USER_AGENT
|
|
2740
2869
|
|
|
2741
2870
|
if "--generate-config" in sys.argv:
|
|
2742
2871
|
print(CONFIG_BLOCK.strip("\n"))
|
|
@@ -2804,9 +2933,9 @@ def main():
|
|
|
2804
2933
|
help="Method to obtain Spotify access token: 'cookie' (via sp_dc cookie) or 'client' (via desktop client login protobuf)"
|
|
2805
2934
|
)
|
|
2806
2935
|
|
|
2807
|
-
#
|
|
2808
|
-
|
|
2809
|
-
|
|
2936
|
+
# Auth details used when token source is set to cookie
|
|
2937
|
+
cookie_auth = parser.add_argument_group("Auth details for 'cookie' token source")
|
|
2938
|
+
cookie_auth.add_argument(
|
|
2810
2939
|
"-u", "--spotify-dc-cookie",
|
|
2811
2940
|
dest="spotify_dc_cookie",
|
|
2812
2941
|
metavar="SP_DC_COOKIE",
|
|
@@ -2814,20 +2943,21 @@ def main():
|
|
|
2814
2943
|
help="Spotify sp_dc cookie"
|
|
2815
2944
|
)
|
|
2816
2945
|
|
|
2817
|
-
#
|
|
2818
|
-
|
|
2819
|
-
|
|
2946
|
+
# Auth details used when token source is set to client
|
|
2947
|
+
client_auth = parser.add_argument_group("Auth details for 'client' token source")
|
|
2948
|
+
client_auth.add_argument(
|
|
2820
2949
|
"-w", "--login-request-body-file",
|
|
2821
2950
|
dest="login_request_body_file",
|
|
2822
2951
|
metavar="PROTOBUF_FILENAME",
|
|
2823
2952
|
help="Read device_id, system_id, user_uri_id and refresh_token from binary Protobuf login file"
|
|
2824
2953
|
)
|
|
2825
2954
|
|
|
2826
|
-
|
|
2955
|
+
client_auth.add_argument(
|
|
2827
2956
|
"-z", "--clienttoken-request-body-file",
|
|
2828
2957
|
dest="clienttoken_request_body_file",
|
|
2829
2958
|
metavar="PROTOBUF_FILENAME",
|
|
2830
|
-
help="Read app_version, cpu_arch, os_build, platform, os_major, os_minor and client_model from binary Protobuf client token file"
|
|
2959
|
+
# help="Read app_version, cpu_arch, os_build, platform, os_major, os_minor and client_model from binary Protobuf client token file"
|
|
2960
|
+
help=argparse.SUPPRESS
|
|
2831
2961
|
)
|
|
2832
2962
|
|
|
2833
2963
|
# Notifications
|
|
@@ -2943,6 +3073,13 @@ def main():
|
|
|
2943
3073
|
type=str,
|
|
2944
3074
|
help="Filename with Spotify tracks/playlists/albums to alert on"
|
|
2945
3075
|
)
|
|
3076
|
+
opts.add_argument(
|
|
3077
|
+
"--user-agent",
|
|
3078
|
+
dest="user_agent",
|
|
3079
|
+
metavar="USER_AGENT",
|
|
3080
|
+
type=str,
|
|
3081
|
+
help="Specify a custom user agent for Spotify API requests; leave empty to auto-generate it"
|
|
3082
|
+
)
|
|
2946
3083
|
opts.add_argument(
|
|
2947
3084
|
"-y", "--file-suffix",
|
|
2948
3085
|
dest="file_suffix",
|
|
@@ -3006,7 +3143,7 @@ def main():
|
|
|
3006
3143
|
except ImportError:
|
|
3007
3144
|
env_path = DOTENV_FILE if DOTENV_FILE else None
|
|
3008
3145
|
if env_path:
|
|
3009
|
-
print(f"* Warning: Cannot load dotenv file '{env_path}' because 'python-dotenv' is not installed\n\nTo install it, run:\n
|
|
3146
|
+
print(f"* Warning: Cannot load dotenv file '{env_path}' because 'python-dotenv' is not installed\n\nTo install it, run:\n pip install python-dotenv\n\nOnce installed, re-run this tool\n")
|
|
3010
3147
|
|
|
3011
3148
|
if env_path:
|
|
3012
3149
|
for secret in SECRET_KEYS:
|
|
@@ -3025,7 +3162,16 @@ def main():
|
|
|
3025
3162
|
try:
|
|
3026
3163
|
import pyotp
|
|
3027
3164
|
except ModuleNotFoundError:
|
|
3028
|
-
raise SystemExit("Error: Couldn't find the pyotp library !\n\nTo install it, run:\n
|
|
3165
|
+
raise SystemExit("Error: Couldn't find the pyotp library !\n\nTo install it, run:\n pip install pyotp\n\nOnce installed, re-run this tool")
|
|
3166
|
+
|
|
3167
|
+
if args.user_agent:
|
|
3168
|
+
USER_AGENT = args.user_agent
|
|
3169
|
+
|
|
3170
|
+
if not USER_AGENT:
|
|
3171
|
+
if TOKEN_SOURCE == "client":
|
|
3172
|
+
USER_AGENT = get_random_spotify_user_agent()
|
|
3173
|
+
else:
|
|
3174
|
+
USER_AGENT = get_random_user_agent()
|
|
3029
3175
|
|
|
3030
3176
|
if not check_internet():
|
|
3031
3177
|
sys.exit(1)
|
|
@@ -3060,7 +3206,7 @@ def main():
|
|
|
3060
3206
|
if LOGIN_REQUEST_BODY_FILE:
|
|
3061
3207
|
if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
|
|
3062
3208
|
try:
|
|
3063
|
-
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN =
|
|
3209
|
+
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN = parse_login_request_body_file(LOGIN_REQUEST_BODY_FILE)
|
|
3064
3210
|
except Exception as e:
|
|
3065
3211
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
|
|
3066
3212
|
sys.exit(1)
|
|
@@ -3076,22 +3222,28 @@ def main():
|
|
|
3076
3222
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) does not exist")
|
|
3077
3223
|
sys.exit(1)
|
|
3078
3224
|
|
|
3079
|
-
|
|
3080
|
-
|
|
3225
|
+
vals = {
|
|
3226
|
+
"LOGIN_URL": LOGIN_URL,
|
|
3227
|
+
"USER_AGENT": USER_AGENT,
|
|
3228
|
+
"DEVICE_ID": DEVICE_ID,
|
|
3229
|
+
"SYSTEM_ID": SYSTEM_ID,
|
|
3230
|
+
"USER_URI_ID": USER_URI_ID,
|
|
3231
|
+
"REFRESH_TOKEN": REFRESH_TOKEN,
|
|
3232
|
+
}
|
|
3233
|
+
placeholders = {
|
|
3234
|
+
"DEVICE_ID": "your_spotify_app_device_id",
|
|
3235
|
+
"SYSTEM_ID": "your_spotify_app_system_id",
|
|
3236
|
+
"USER_URI_ID": "your_spotify_user_uri_id",
|
|
3237
|
+
"REFRESH_TOKEN": "your_spotify_app_refresh_token",
|
|
3238
|
+
}
|
|
3081
3239
|
|
|
3082
|
-
|
|
3083
|
-
not
|
|
3084
|
-
|
|
3085
|
-
not
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
not USER_URI_ID,
|
|
3090
|
-
USER_URI_ID == "your_spotify_user_uri_id",
|
|
3091
|
-
not REFRESH_TOKEN,
|
|
3092
|
-
REFRESH_TOKEN == "your_spotify_app_refresh_token",
|
|
3093
|
-
]):
|
|
3094
|
-
print("* Error: Some login values are empty or incorrect")
|
|
3240
|
+
bad = [
|
|
3241
|
+
f"{k} {'missing' if not v else 'is placeholder'}"
|
|
3242
|
+
for k, v in vals.items()
|
|
3243
|
+
if not v or placeholders.get(k) == v
|
|
3244
|
+
]
|
|
3245
|
+
if bad:
|
|
3246
|
+
print("* Error:", "; ".join(bad))
|
|
3095
3247
|
sys.exit(1)
|
|
3096
3248
|
|
|
3097
3249
|
clienttoken_request_body_file_param = False
|
|
@@ -3130,7 +3282,7 @@ def main():
|
|
|
3130
3282
|
try:
|
|
3131
3283
|
APP_VERSION = ua_to_app_version(USER_AGENT)
|
|
3132
3284
|
except Exception as e:
|
|
3133
|
-
print(f"Warning: wrong USER_AGENT defined, reverting to the default one: {e}")
|
|
3285
|
+
print(f"Warning: wrong USER_AGENT defined, reverting to the default one for APP_VERSION: {e}")
|
|
3134
3286
|
APP_VERSION = app_version_default
|
|
3135
3287
|
else:
|
|
3136
3288
|
APP_VERSION = app_version_default
|
|
@@ -3288,6 +3440,7 @@ def main():
|
|
|
3288
3440
|
print(f"* Email notifications:\t\t[active = {ACTIVE_NOTIFICATION}] [inactive = {INACTIVE_NOTIFICATION}] [tracked = {TRACK_NOTIFICATION}]\n*\t\t\t\t[songs on loop = {SONG_ON_LOOP_NOTIFICATION}] [every song = {SONG_NOTIFICATION}] [errors = {ERROR_NOTIFICATION}]")
|
|
3289
3441
|
print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
|
|
3290
3442
|
print(f"* Track listened songs:\t\t{TRACK_SONGS}")
|
|
3443
|
+
# print(f"* User agent:\t\t\t{USER_AGENT}")
|
|
3291
3444
|
print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
|
|
3292
3445
|
print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
|
|
3293
3446
|
print(f"* Alert on monitored tracks:\t{bool(MONITOR_LIST_FILE)}" + (f" ({MONITOR_LIST_FILE})" if MONITOR_LIST_FILE else ""))
|