spotify-monitor 2.1.2__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.2.dist-info → spotify_monitor-2.2.dist-info}/METADATA +40 -36
- spotify_monitor-2.2.dist-info/RECORD +7 -0
- spotify_monitor.py +358 -202
- spotify_monitor-2.1.2.dist-info/RECORD +0 -7
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.dist-info}/WHEEL +0 -0
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.dist-info}/entry_points.txt +0 -0
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.dist-info}/licenses/LICENSE +0 -0
- {spotify_monitor-2.1.2.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.2"
|
|
|
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>
|
|
257
|
+
#
|
|
258
|
+
# - Use the protoc tool (part of protobuf pip package):
|
|
259
|
+
# pip install protobuf
|
|
260
|
+
# protoc --decode_raw < <path-to-login-request-body-file>
|
|
237
261
|
#
|
|
238
|
-
#
|
|
239
|
-
# - DEVICE_ID
|
|
240
|
-
# - SYSTEM_ID
|
|
241
|
-
# - USER_URI_ID
|
|
242
|
-
# - REFRESH_TOKEN
|
|
243
|
-
# and assign them to respective configuration options
|
|
262
|
+
# - Use the built-in Protobuf decoder in your intercepting proxy (if supported)
|
|
244
263
|
#
|
|
245
|
-
#
|
|
246
|
-
# protoc --decode_raw < <path-to-login-request-body-file>
|
|
264
|
+
# The Protobuf structure is as follows:
|
|
247
265
|
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
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,7 +454,6 @@ 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
459
|
TOKEN_URL = "https://open.spotify.com/api/token"
|
|
@@ -466,7 +526,8 @@ retry = Retry(
|
|
|
466
526
|
backoff_factor=1,
|
|
467
527
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
468
528
|
allowed_methods=["GET", "HEAD", "OPTIONS"],
|
|
469
|
-
raise_on_status=False
|
|
529
|
+
raise_on_status=False,
|
|
530
|
+
respect_retry_after_header=True
|
|
470
531
|
)
|
|
471
532
|
|
|
472
533
|
adapter = HTTPAdapter(max_retries=retry, pool_connections=100, pool_maxsize=100)
|
|
@@ -510,7 +571,7 @@ def signal_handler(sig, frame):
|
|
|
510
571
|
# Checks internet connectivity
|
|
511
572
|
def check_internet(url=CHECK_INTERNET_URL, timeout=CHECK_INTERNET_TIMEOUT, verify=VERIFY_SSL):
|
|
512
573
|
try:
|
|
513
|
-
_ = req.get(url, headers={'User-Agent':
|
|
574
|
+
_ = req.get(url, headers={'User-Agent': USER_AGENT}, timeout=timeout, verify=verify)
|
|
514
575
|
return True
|
|
515
576
|
except req.RequestException as e:
|
|
516
577
|
print(f"* No connectivity, please check your network:\n\n{e}")
|
|
@@ -928,7 +989,7 @@ def reload_secrets_signal_handler(sig, frame):
|
|
|
928
989
|
if LOGIN_REQUEST_BODY_FILE:
|
|
929
990
|
if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
|
|
930
991
|
try:
|
|
931
|
-
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)
|
|
932
993
|
except Exception as e:
|
|
933
994
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
|
|
934
995
|
else:
|
|
@@ -1131,8 +1192,8 @@ def generate_totp():
|
|
|
1131
1192
|
return pyotp.TOTP(secret, digits=6, interval=30)
|
|
1132
1193
|
|
|
1133
1194
|
|
|
1134
|
-
#
|
|
1135
|
-
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:
|
|
1136
1197
|
transport = True
|
|
1137
1198
|
init = True
|
|
1138
1199
|
session = req.Session()
|
|
@@ -1140,8 +1201,7 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1140
1201
|
data: dict = {}
|
|
1141
1202
|
token = ""
|
|
1142
1203
|
|
|
1143
|
-
|
|
1144
|
-
server_time = fetch_server_time(session, ua)
|
|
1204
|
+
server_time = fetch_server_time(session, USER_AGENT)
|
|
1145
1205
|
totp_obj = generate_totp()
|
|
1146
1206
|
client_time = int(time_ns() / 1000 / 1000)
|
|
1147
1207
|
otp_value = totp_obj.at(server_time)
|
|
@@ -1159,13 +1219,15 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1159
1219
|
}
|
|
1160
1220
|
|
|
1161
1221
|
headers = {
|
|
1162
|
-
"User-Agent":
|
|
1222
|
+
"User-Agent": USER_AGENT,
|
|
1163
1223
|
"Accept": "application/json",
|
|
1164
1224
|
"Referer": "https://open.spotify.com/",
|
|
1165
1225
|
"App-Platform": "WebPlayer",
|
|
1166
1226
|
"Cookie": f"sp_dc={sp_dc}",
|
|
1167
1227
|
}
|
|
1168
1228
|
|
|
1229
|
+
last_err = ""
|
|
1230
|
+
|
|
1169
1231
|
try:
|
|
1170
1232
|
if platform.system() != "Windows":
|
|
1171
1233
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
@@ -1176,13 +1238,14 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1176
1238
|
data = response.json()
|
|
1177
1239
|
token = data.get("accessToken", "")
|
|
1178
1240
|
|
|
1179
|
-
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1241
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError) as e:
|
|
1180
1242
|
transport = False
|
|
1243
|
+
last_err = str(e)
|
|
1181
1244
|
finally:
|
|
1182
1245
|
if platform.system() != "Windows":
|
|
1183
1246
|
signal.alarm(0)
|
|
1184
1247
|
|
|
1185
|
-
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)):
|
|
1186
1249
|
params["reason"] = "init"
|
|
1187
1250
|
|
|
1188
1251
|
try:
|
|
@@ -1195,58 +1258,52 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1195
1258
|
data = response.json()
|
|
1196
1259
|
token = data.get("accessToken", "")
|
|
1197
1260
|
|
|
1198
|
-
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1261
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError) as e:
|
|
1199
1262
|
init = False
|
|
1263
|
+
last_err = str(e)
|
|
1200
1264
|
finally:
|
|
1201
1265
|
if platform.system() != "Windows":
|
|
1202
1266
|
signal.alarm(0)
|
|
1203
1267
|
|
|
1204
1268
|
if not init or not data or "accessToken" not in data:
|
|
1205
|
-
raise Exception("
|
|
1269
|
+
raise Exception(f"refresh_access_token_from_sp_dc(): Unsuccessful token request{': ' + last_err if last_err else ''}")
|
|
1206
1270
|
|
|
1207
1271
|
return {
|
|
1208
1272
|
"access_token": token,
|
|
1209
1273
|
"expires_at": data["accessTokenExpirationTimestampMs"] // 1000,
|
|
1210
1274
|
"client_id": data.get("clientId", ""),
|
|
1211
|
-
"user_agent": ua,
|
|
1212
1275
|
"length": len(token)
|
|
1213
1276
|
}
|
|
1214
1277
|
|
|
1215
1278
|
|
|
1216
1279
|
# Fetches Spotify access token based on provided SP_DC value
|
|
1217
1280
|
def spotify_get_access_token_from_sp_dc(sp_dc: str):
|
|
1218
|
-
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
|
|
1219
1282
|
|
|
1220
1283
|
now = time.time()
|
|
1221
1284
|
|
|
1222
|
-
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):
|
|
1223
1286
|
return SP_CACHED_ACCESS_TOKEN
|
|
1224
1287
|
|
|
1225
1288
|
max_retries = TOKEN_MAX_RETRIES
|
|
1226
1289
|
retry = 0
|
|
1227
1290
|
|
|
1228
1291
|
while retry < max_retries:
|
|
1229
|
-
token_data =
|
|
1292
|
+
token_data = refresh_access_token_from_sp_dc(sp_dc)
|
|
1230
1293
|
token = token_data["access_token"]
|
|
1231
1294
|
client_id = token_data.get("client_id", "")
|
|
1232
|
-
user_agent = token_data.get("user_agent", get_random_user_agent())
|
|
1233
1295
|
length = token_data["length"]
|
|
1234
1296
|
|
|
1235
1297
|
SP_CACHED_ACCESS_TOKEN = token
|
|
1236
1298
|
SP_ACCESS_TOKEN_EXPIRES_AT = token_data["expires_at"]
|
|
1237
1299
|
SP_CACHED_CLIENT_ID = client_id
|
|
1238
|
-
SP_CACHED_USER_AGENT = user_agent
|
|
1239
1300
|
|
|
1240
|
-
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):
|
|
1241
1302
|
retry += 1
|
|
1242
1303
|
time.sleep(TOKEN_RETRY_TIMEOUT)
|
|
1243
1304
|
else:
|
|
1244
|
-
# print("* Token is valid")
|
|
1245
1305
|
break
|
|
1246
1306
|
|
|
1247
|
-
# print("Spotify Access Token:", SP_CACHED_ACCESS_TOKEN)
|
|
1248
|
-
# print("Token expires at:", time.ctime(SP_TOKEN_EXPIRES_AT))
|
|
1249
|
-
|
|
1250
1307
|
if retry == max_retries:
|
|
1251
1308
|
if SP_CACHED_ACCESS_TOKEN is not None:
|
|
1252
1309
|
print(f"* Token appears to be still invalid after {max_retries} attempts, returning token anyway")
|
|
@@ -1319,13 +1376,15 @@ def encode_nested_field(tag, nested_bytes):
|
|
|
1319
1376
|
# Builds the Spotify Protobuf login request body
|
|
1320
1377
|
def build_spotify_auth_protobuf(device_id, system_id, user_uri_id, refresh_token):
|
|
1321
1378
|
"""
|
|
1322
|
-
|
|
1323
|
-
1:
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1379
|
+
{
|
|
1380
|
+
1: {
|
|
1381
|
+
1: "device_id",
|
|
1382
|
+
2: "system_id"
|
|
1383
|
+
},
|
|
1384
|
+
100: {
|
|
1385
|
+
1: "user_uri_id",
|
|
1386
|
+
2: "refresh_token"
|
|
1387
|
+
}
|
|
1329
1388
|
}
|
|
1330
1389
|
"""
|
|
1331
1390
|
device_info_msg = encode_string_field(1, device_id) + encode_string_field(2, system_id)
|
|
@@ -1390,7 +1449,7 @@ def parse_protobuf_message(data):
|
|
|
1390
1449
|
|
|
1391
1450
|
# Parses the Protobuf-encoded login request body file (as dumped for example by Proxyman) and returns a tuple:
|
|
1392
1451
|
# (device_id, system_id, user_uri_id, refresh_token)
|
|
1393
|
-
def
|
|
1452
|
+
def parse_login_request_body_file(file_path):
|
|
1394
1453
|
"""
|
|
1395
1454
|
{
|
|
1396
1455
|
1: {
|
|
@@ -1467,6 +1526,26 @@ def ensure_dict(value):
|
|
|
1467
1526
|
# Parses the Protobuf-encoded client token request body file (as dumped for example by Proxyman) and returns a tuple:
|
|
1468
1527
|
# (app_version, device_id, system_id, cpu_arch, os_build, platform, os_major, os_minor, client_model)
|
|
1469
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
|
+
"""
|
|
1470
1549
|
|
|
1471
1550
|
with open(file_path, "rb") as f:
|
|
1472
1551
|
data = f.read()
|
|
@@ -1527,13 +1606,20 @@ def build_clienttoken_request_protobuf(app_version, device_id, system_id, cpu_ar
|
|
|
1527
1606
|
"""
|
|
1528
1607
|
1: 1 (const)
|
|
1529
1608
|
2: {
|
|
1530
|
-
1: app_version
|
|
1531
|
-
2: device_id
|
|
1609
|
+
1: "app_version"
|
|
1610
|
+
2: "device_id"
|
|
1532
1611
|
3: {
|
|
1533
1612
|
1: {
|
|
1534
|
-
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
|
+
}
|
|
1535
1621
|
}
|
|
1536
|
-
2: system_id
|
|
1622
|
+
2: "system_id"
|
|
1537
1623
|
}
|
|
1538
1624
|
}
|
|
1539
1625
|
"""
|
|
@@ -1605,6 +1691,22 @@ def spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refr
|
|
|
1605
1691
|
elif response.headers.get("client-token-error") == "EXPIRED_CLIENTTOKEN":
|
|
1606
1692
|
raise Exception(f"Request failed with status {response.status_code}: expired client token")
|
|
1607
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
|
+
|
|
1608
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}")
|
|
1609
1711
|
|
|
1610
1712
|
parsed = parse_protobuf_message(response.content)
|
|
@@ -1735,16 +1837,14 @@ def spotify_get_access_token_from_client_auto(device_id, system_id, user_uri_id,
|
|
|
1735
1837
|
# Fetches list of Spotify friends
|
|
1736
1838
|
def spotify_get_friends_json(access_token):
|
|
1737
1839
|
url = "https://guc-spclient.spotify.com/presence-view/v1/buddylist"
|
|
1738
|
-
headers = {
|
|
1840
|
+
headers = {
|
|
1841
|
+
"Authorization": f"Bearer {access_token}",
|
|
1842
|
+
"User-Agent": USER_AGENT
|
|
1843
|
+
}
|
|
1739
1844
|
|
|
1740
1845
|
if TOKEN_SOURCE == "cookie":
|
|
1741
1846
|
headers.update({
|
|
1742
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1743
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1744
|
-
})
|
|
1745
|
-
elif TOKEN_SOURCE == "client":
|
|
1746
|
-
headers.update({
|
|
1747
|
-
"User-Agent": USER_AGENT
|
|
1847
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1748
1848
|
})
|
|
1749
1849
|
|
|
1750
1850
|
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
@@ -1833,8 +1933,8 @@ def spotify_list_friends(friend_activity):
|
|
|
1833
1933
|
|
|
1834
1934
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
1835
1935
|
|
|
1836
|
-
print(f"Apple
|
|
1837
|
-
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}")
|
|
1838
1938
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
1839
1939
|
|
|
1840
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)")
|
|
@@ -1866,16 +1966,14 @@ def spotify_get_friend_info(friend_activity, uri):
|
|
|
1866
1966
|
def spotify_get_track_info(access_token, track_uri):
|
|
1867
1967
|
track_id = track_uri.split(':', 2)[2]
|
|
1868
1968
|
url = "https://api.spotify.com/v1/tracks/" + track_id
|
|
1869
|
-
headers = {
|
|
1969
|
+
headers = {
|
|
1970
|
+
"Authorization": f"Bearer {access_token}",
|
|
1971
|
+
"User-Agent": USER_AGENT
|
|
1972
|
+
}
|
|
1870
1973
|
|
|
1871
1974
|
if TOKEN_SOURCE == "cookie":
|
|
1872
1975
|
headers.update({
|
|
1873
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1874
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1875
|
-
})
|
|
1876
|
-
elif TOKEN_SOURCE == "client":
|
|
1877
|
-
headers.update({
|
|
1878
|
-
"User-Agent": USER_AGENT
|
|
1976
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1879
1977
|
})
|
|
1880
1978
|
# add si parameter so link opens in native Spotify app after clicking
|
|
1881
1979
|
si = "?si=1"
|
|
@@ -1900,16 +1998,14 @@ def spotify_get_track_info(access_token, track_uri):
|
|
|
1900
1998
|
def spotify_get_playlist_info(access_token, playlist_uri):
|
|
1901
1999
|
playlist_id = playlist_uri.split(':', 2)[2]
|
|
1902
2000
|
url = f"https://api.spotify.com/v1/playlists/{playlist_id}?fields=name,owner,followers,external_urls"
|
|
1903
|
-
headers = {
|
|
2001
|
+
headers = {
|
|
2002
|
+
"Authorization": f"Bearer {access_token}",
|
|
2003
|
+
"User-Agent": USER_AGENT
|
|
2004
|
+
}
|
|
1904
2005
|
|
|
1905
2006
|
if TOKEN_SOURCE == "cookie":
|
|
1906
2007
|
headers.update({
|
|
1907
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1908
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1909
|
-
})
|
|
1910
|
-
elif TOKEN_SOURCE == "client":
|
|
1911
|
-
headers.update({
|
|
1912
|
-
"User-Agent": USER_AGENT
|
|
2008
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1913
2009
|
})
|
|
1914
2010
|
# add si parameter so link opens in native Spotify app after clicking
|
|
1915
2011
|
si = "?si=1"
|
|
@@ -1931,16 +2027,14 @@ def spotify_get_playlist_info(access_token, playlist_uri):
|
|
|
1931
2027
|
# Gets basic information about access token owner
|
|
1932
2028
|
def spotify_get_current_user(access_token) -> dict | None:
|
|
1933
2029
|
url = "https://api.spotify.com/v1/me"
|
|
1934
|
-
headers = {
|
|
2030
|
+
headers = {
|
|
2031
|
+
"Authorization": f"Bearer {access_token}",
|
|
2032
|
+
"User-Agent": USER_AGENT
|
|
2033
|
+
}
|
|
1935
2034
|
|
|
1936
2035
|
if TOKEN_SOURCE == "cookie":
|
|
1937
2036
|
headers.update({
|
|
1938
|
-
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1939
|
-
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1940
|
-
})
|
|
1941
|
-
elif TOKEN_SOURCE == "client":
|
|
1942
|
-
headers.update({
|
|
1943
|
-
"User-Agent": USER_AGENT
|
|
2037
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1944
2038
|
})
|
|
1945
2039
|
|
|
1946
2040
|
if platform.system() != 'Windows':
|
|
@@ -1969,6 +2063,29 @@ def spotify_get_current_user(access_token) -> dict | None:
|
|
|
1969
2063
|
signal.alarm(0)
|
|
1970
2064
|
|
|
1971
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
|
+
|
|
1972
2089
|
def spotify_macos_play_song(sp_track_uri_id, method=SPOTIFY_MACOS_PLAYING_METHOD):
|
|
1973
2090
|
if method == "apple-script": # apple-script
|
|
1974
2091
|
script = f'tell app "Spotify" to play track "spotify:track:{sp_track_uri_id}"'
|
|
@@ -2061,7 +2178,7 @@ def resolve_executable(path):
|
|
|
2061
2178
|
raise FileNotFoundError(f"Could not find executable '{path}'")
|
|
2062
2179
|
|
|
2063
2180
|
|
|
2064
|
-
#
|
|
2181
|
+
# Monitors music activity of the specified Spotify friend's user URI ID
|
|
2065
2182
|
def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
2066
2183
|
global SP_CACHED_ACCESS_TOKEN
|
|
2067
2184
|
sp_active_ts_start = 0
|
|
@@ -2081,6 +2198,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2081
2198
|
error_500_start_ts = 0
|
|
2082
2199
|
error_network_issue_counter = 0
|
|
2083
2200
|
error_network_issue_start_ts = 0
|
|
2201
|
+
sp_accessToken = ""
|
|
2084
2202
|
|
|
2085
2203
|
try:
|
|
2086
2204
|
if csv_file_name:
|
|
@@ -2132,25 +2250,25 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2132
2250
|
if TOKEN_SOURCE == 'cookie' and '401' in err:
|
|
2133
2251
|
SP_CACHED_ACCESS_TOKEN = None
|
|
2134
2252
|
|
|
2135
|
-
client_errs = ['access token', 'invalid client token', 'expired client token']
|
|
2136
|
-
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']
|
|
2137
2255
|
|
|
2138
2256
|
if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
|
|
2139
|
-
print(f"* Error: client
|
|
2257
|
+
print(f"* Error: client or refresh token may be invalid or expired!")
|
|
2140
2258
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2141
|
-
m_subject = f"spotify_monitor: client
|
|
2142
|
-
m_body = f"Client
|
|
2143
|
-
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>"
|
|
2144
2262
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2145
2263
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2146
2264
|
email_sent = True
|
|
2147
2265
|
|
|
2148
2266
|
elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
|
|
2149
|
-
print(f"* Error: sp_dc
|
|
2267
|
+
print(f"* Error: sp_dc may be invalid or expired!")
|
|
2150
2268
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2151
|
-
m_subject = f"spotify_monitor: sp_dc
|
|
2152
|
-
m_body = f"sp_dc
|
|
2153
|
-
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>"
|
|
2154
2272
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2155
2273
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2156
2274
|
email_sent = True
|
|
@@ -2259,8 +2377,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2259
2377
|
|
|
2260
2378
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
2261
2379
|
|
|
2262
|
-
print(f"Apple
|
|
2263
|
-
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}")
|
|
2264
2382
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
2265
2383
|
|
|
2266
2384
|
if not is_playlist:
|
|
@@ -2287,8 +2405,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2287
2405
|
|
|
2288
2406
|
if ACTIVE_NOTIFICATION:
|
|
2289
2407
|
m_subject = f"Spotify user {sp_username} is active: '{sp_artist} - {sp_track}'"
|
|
2290
|
-
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
|
|
2291
|
-
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>"
|
|
2292
2410
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2293
2411
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2294
2412
|
|
|
@@ -2313,7 +2431,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2313
2431
|
|
|
2314
2432
|
email_sent = False
|
|
2315
2433
|
|
|
2316
|
-
#
|
|
2434
|
+
# Primary loop
|
|
2317
2435
|
while True:
|
|
2318
2436
|
|
|
2319
2437
|
while True:
|
|
@@ -2379,25 +2497,25 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2379
2497
|
elif not error_500_start_ts and not error_network_issue_start_ts:
|
|
2380
2498
|
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: '{e}'")
|
|
2381
2499
|
|
|
2382
|
-
client_errs = ['access token', 'invalid client token', 'expired client token']
|
|
2383
|
-
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']
|
|
2384
2502
|
|
|
2385
2503
|
if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
|
|
2386
|
-
print(f"* Error: client
|
|
2504
|
+
print(f"* Error: client or refresh token may be invalid or expired!")
|
|
2387
2505
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2388
|
-
m_subject = f"spotify_monitor: client
|
|
2389
|
-
m_body = f"Client
|
|
2390
|
-
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>"
|
|
2391
2509
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2392
2510
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2393
2511
|
email_sent = True
|
|
2394
2512
|
|
|
2395
2513
|
elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
|
|
2396
|
-
print(f"* Error: sp_dc
|
|
2514
|
+
print(f"* Error: sp_dc may be invalid or expired!")
|
|
2397
2515
|
if ERROR_NOTIFICATION and not email_sent:
|
|
2398
|
-
m_subject = f"spotify_monitor: sp_dc
|
|
2399
|
-
m_body = f"sp_dc
|
|
2400
|
-
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>"
|
|
2401
2519
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2402
2520
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2403
2521
|
email_sent = True
|
|
@@ -2406,15 +2524,24 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2406
2524
|
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
2407
2525
|
|
|
2408
2526
|
if sp_found is False:
|
|
2409
|
-
# User disappeared from the Spotify's friend list
|
|
2527
|
+
# User has disappeared from the Spotify's friend list or account has been removed
|
|
2410
2528
|
if user_not_found is False:
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
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)
|
|
2418
2545
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2419
2546
|
user_not_found = True
|
|
2420
2547
|
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
@@ -2422,11 +2549,11 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2422
2549
|
else:
|
|
2423
2550
|
# User reappeared in the Spotify's friend list
|
|
2424
2551
|
if user_not_found is True:
|
|
2425
|
-
print(f"Spotify user {user_uri_id} ({sp_username})
|
|
2552
|
+
print(f"Spotify user {user_uri_id} ({sp_username}) has reappeared!")
|
|
2426
2553
|
if ERROR_NOTIFICATION:
|
|
2427
|
-
m_subject = f"Spotify user {user_uri_id} ({sp_username})
|
|
2428
|
-
m_body = f"Spotify user {user_uri_id} ({sp_username})
|
|
2429
|
-
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>"
|
|
2430
2557
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2431
2558
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2432
2559
|
print_cur_ts("Timestamp:\t\t\t")
|
|
@@ -2558,8 +2685,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2558
2685
|
|
|
2559
2686
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
2560
2687
|
|
|
2561
|
-
print(f"Apple
|
|
2562
|
-
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}")
|
|
2563
2690
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
2564
2691
|
|
|
2565
2692
|
if not is_playlist:
|
|
@@ -2578,6 +2705,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2578
2705
|
listened_songs = 1
|
|
2579
2706
|
skipped_songs = 0
|
|
2580
2707
|
looped_songs = 0
|
|
2708
|
+
song_on_loop = 1
|
|
2581
2709
|
|
|
2582
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)})")
|
|
2583
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)})"
|
|
@@ -2594,8 +2722,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2594
2722
|
sp_active_ts_start = sp_active_ts_start_old
|
|
2595
2723
|
sp_active_ts_stop = 0
|
|
2596
2724
|
|
|
2597
|
-
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
|
|
2598
|
-
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>"
|
|
2599
2727
|
|
|
2600
2728
|
if ACTIVE_NOTIFICATION:
|
|
2601
2729
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2609,16 +2737,16 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2609
2737
|
|
|
2610
2738
|
if (TRACK_NOTIFICATION and on_the_list and not email_sent) or (SONG_NOTIFICATION and not email_sent):
|
|
2611
2739
|
m_subject = f"Spotify user {sp_username}: '{sp_artist} - {sp_track}'"
|
|
2612
|
-
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
|
|
2613
|
-
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>"
|
|
2614
2742
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2615
2743
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2616
2744
|
email_sent = True
|
|
2617
2745
|
|
|
2618
2746
|
if song_on_loop == SONG_ON_LOOP_VALUE and SONG_ON_LOOP_NOTIFICATION:
|
|
2619
2747
|
m_subject = f"Spotify user {sp_username} plays song on loop: '{sp_artist} - {sp_track}'"
|
|
2620
|
-
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
|
|
2621
|
-
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>"
|
|
2622
2750
|
if not email_sent:
|
|
2623
2751
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2624
2752
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
@@ -2685,8 +2813,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2685
2813
|
spotify_linux_play_pause("pause")
|
|
2686
2814
|
if INACTIVE_NOTIFICATION:
|
|
2687
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)})"
|
|
2688
|
-
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
|
|
2689
|
-
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>"
|
|
2690
2818
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2691
2819
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2692
2820
|
email_sent = True
|
|
@@ -2698,6 +2826,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2698
2826
|
listened_songs = 0
|
|
2699
2827
|
looped_songs = 0
|
|
2700
2828
|
skipped_songs = 0
|
|
2829
|
+
song_on_loop = 0
|
|
2701
2830
|
print_cur_ts("\nTimestamp:\t\t\t")
|
|
2702
2831
|
|
|
2703
2832
|
if LIVENESS_CHECK_COUNTER and alive_counter >= LIVENESS_CHECK_COUNTER:
|
|
@@ -2725,7 +2854,10 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2725
2854
|
# User is not found in the Spotify's friend list just after starting the tool
|
|
2726
2855
|
else:
|
|
2727
2856
|
if user_not_found is False:
|
|
2728
|
-
|
|
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")
|
|
2729
2861
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2730
2862
|
user_not_found = True
|
|
2731
2863
|
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
@@ -2733,7 +2865,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2733
2865
|
|
|
2734
2866
|
|
|
2735
2867
|
def main():
|
|
2736
|
-
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
|
|
2737
2869
|
|
|
2738
2870
|
if "--generate-config" in sys.argv:
|
|
2739
2871
|
print(CONFIG_BLOCK.strip("\n"))
|
|
@@ -2801,9 +2933,9 @@ def main():
|
|
|
2801
2933
|
help="Method to obtain Spotify access token: 'cookie' (via sp_dc cookie) or 'client' (via desktop client login protobuf)"
|
|
2802
2934
|
)
|
|
2803
2935
|
|
|
2804
|
-
#
|
|
2805
|
-
|
|
2806
|
-
|
|
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(
|
|
2807
2939
|
"-u", "--spotify-dc-cookie",
|
|
2808
2940
|
dest="spotify_dc_cookie",
|
|
2809
2941
|
metavar="SP_DC_COOKIE",
|
|
@@ -2811,20 +2943,21 @@ def main():
|
|
|
2811
2943
|
help="Spotify sp_dc cookie"
|
|
2812
2944
|
)
|
|
2813
2945
|
|
|
2814
|
-
#
|
|
2815
|
-
|
|
2816
|
-
|
|
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(
|
|
2817
2949
|
"-w", "--login-request-body-file",
|
|
2818
2950
|
dest="login_request_body_file",
|
|
2819
2951
|
metavar="PROTOBUF_FILENAME",
|
|
2820
2952
|
help="Read device_id, system_id, user_uri_id and refresh_token from binary Protobuf login file"
|
|
2821
2953
|
)
|
|
2822
2954
|
|
|
2823
|
-
|
|
2955
|
+
client_auth.add_argument(
|
|
2824
2956
|
"-z", "--clienttoken-request-body-file",
|
|
2825
2957
|
dest="clienttoken_request_body_file",
|
|
2826
2958
|
metavar="PROTOBUF_FILENAME",
|
|
2827
|
-
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
|
|
2828
2961
|
)
|
|
2829
2962
|
|
|
2830
2963
|
# Notifications
|
|
@@ -2940,6 +3073,13 @@ def main():
|
|
|
2940
3073
|
type=str,
|
|
2941
3074
|
help="Filename with Spotify tracks/playlists/albums to alert on"
|
|
2942
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
|
+
)
|
|
2943
3083
|
opts.add_argument(
|
|
2944
3084
|
"-y", "--file-suffix",
|
|
2945
3085
|
dest="file_suffix",
|
|
@@ -3003,7 +3143,7 @@ def main():
|
|
|
3003
3143
|
except ImportError:
|
|
3004
3144
|
env_path = DOTENV_FILE if DOTENV_FILE else None
|
|
3005
3145
|
if env_path:
|
|
3006
|
-
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")
|
|
3007
3147
|
|
|
3008
3148
|
if env_path:
|
|
3009
3149
|
for secret in SECRET_KEYS:
|
|
@@ -3022,7 +3162,16 @@ def main():
|
|
|
3022
3162
|
try:
|
|
3023
3163
|
import pyotp
|
|
3024
3164
|
except ModuleNotFoundError:
|
|
3025
|
-
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()
|
|
3026
3175
|
|
|
3027
3176
|
if not check_internet():
|
|
3028
3177
|
sys.exit(1)
|
|
@@ -3057,7 +3206,7 @@ def main():
|
|
|
3057
3206
|
if LOGIN_REQUEST_BODY_FILE:
|
|
3058
3207
|
if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
|
|
3059
3208
|
try:
|
|
3060
|
-
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)
|
|
3061
3210
|
except Exception as e:
|
|
3062
3211
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
|
|
3063
3212
|
sys.exit(1)
|
|
@@ -3073,22 +3222,28 @@ def main():
|
|
|
3073
3222
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) does not exist")
|
|
3074
3223
|
sys.exit(1)
|
|
3075
3224
|
|
|
3076
|
-
|
|
3077
|
-
|
|
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
|
+
}
|
|
3078
3239
|
|
|
3079
|
-
|
|
3080
|
-
not
|
|
3081
|
-
|
|
3082
|
-
not
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
not USER_URI_ID,
|
|
3087
|
-
USER_URI_ID == "your_spotify_user_uri_id",
|
|
3088
|
-
not REFRESH_TOKEN,
|
|
3089
|
-
REFRESH_TOKEN == "your_spotify_app_refresh_token",
|
|
3090
|
-
]):
|
|
3091
|
-
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))
|
|
3092
3247
|
sys.exit(1)
|
|
3093
3248
|
|
|
3094
3249
|
clienttoken_request_body_file_param = False
|
|
@@ -3127,7 +3282,7 @@ def main():
|
|
|
3127
3282
|
try:
|
|
3128
3283
|
APP_VERSION = ua_to_app_version(USER_AGENT)
|
|
3129
3284
|
except Exception as e:
|
|
3130
|
-
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}")
|
|
3131
3286
|
APP_VERSION = app_version_default
|
|
3132
3287
|
else:
|
|
3133
3288
|
APP_VERSION = app_version_default
|
|
@@ -3285,6 +3440,7 @@ def main():
|
|
|
3285
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}]")
|
|
3286
3441
|
print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
|
|
3287
3442
|
print(f"* Track listened songs:\t\t{TRACK_SONGS}")
|
|
3443
|
+
# print(f"* User agent:\t\t\t{USER_AGENT}")
|
|
3288
3444
|
print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
|
|
3289
3445
|
print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
|
|
3290
3446
|
print(f"* Alert on monitored tracks:\t{bool(MONITOR_LIST_FILE)}" + (f" ({MONITOR_LIST_FILE})" if MONITOR_LIST_FILE else ""))
|