spotify-monitor 2.1.2__py3-none-any.whl → 2.2.1__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.1.dist-info}/METADATA +42 -36
- spotify_monitor-2.2.1.dist-info/RECORD +7 -0
- spotify_monitor.py +378 -207
- spotify_monitor-2.1.2.dist-info/RECORD +0 -7
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.1.dist-info}/WHEEL +0 -0
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.1.dist-info}/entry_points.txt +0 -0
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.1.dist-info}/licenses/LICENSE +0 -0
- {spotify_monitor-2.1.2.dist-info → spotify_monitor-2.2.1.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.1
|
|
4
|
+
v2.2.1
|
|
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.1
|
|
18
|
+
VERSION = "2.2.1"
|
|
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,23 @@ 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
|
+
# Occasionally, the Spotify API glitches and reports that the user has disappeared from the list of friends
|
|
153
|
+
# To avoid false alarms, we delay alerts until this happens REMOVED_DISAPPEARED_COUNTER times in a row
|
|
154
|
+
REMOVED_DISAPPEARED_COUNTER = 4
|
|
155
|
+
|
|
156
|
+
# Optional: specify user agent manually
|
|
157
|
+
#
|
|
158
|
+
# When the token source is 'cookie' - set it to web browser user agent, some examples:
|
|
159
|
+
# Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
|
|
160
|
+
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0
|
|
161
|
+
#
|
|
162
|
+
# When the token source is 'client' - set it to Spotify desktop client user agent, some examples:
|
|
163
|
+
# Spotify/126200580 Win32_x86_64/0 (PC desktop)
|
|
164
|
+
# Spotify/126400408 OSX_ARM64/OS X 15.5.0 [arm 2]
|
|
165
|
+
#
|
|
166
|
+
# Leave empty to auto-generate it randomly for specific token source
|
|
167
|
+
USER_AGENT = ""
|
|
168
|
+
|
|
154
169
|
# How often to print a "liveness check" message to the output; in seconds
|
|
155
170
|
# Set to 0 to disable
|
|
156
171
|
LIVENESS_CHECK_INTERVAL = 43200 # 12 hours
|
|
@@ -219,44 +234,64 @@ TOKEN_MAX_RETRIES = 10
|
|
|
219
234
|
# Used only when the token source is set to 'cookie'
|
|
220
235
|
TOKEN_RETRY_TIMEOUT = 0.5 # 0.5 second
|
|
221
236
|
|
|
222
|
-
#
|
|
237
|
+
# ---------------------------------------------------------------------
|
|
238
|
+
|
|
223
239
|
# The section below is used when the token source is set to 'client'
|
|
224
240
|
#
|
|
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
|
|
241
|
+
# - Run an intercepting proxy of your choice (like Proxyman)
|
|
242
|
+
# - Launch the Spotify desktop client and look for requests to: https://login{n}.spotify.com/v3/login
|
|
243
|
+
# (the 'login' part is suffixed with one or more digits)
|
|
244
|
+
# - Export the login request body (a binary Protobuf payload) to a file
|
|
231
245
|
# (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <login-request-body-file>)
|
|
232
246
|
#
|
|
233
|
-
#
|
|
247
|
+
# To automatically extract DEVICE_ID, SYSTEM_ID, USER_URI_ID and REFRESH_TOKEN from the exported binary login
|
|
248
|
+
# request Protobuf file:
|
|
249
|
+
#
|
|
250
|
+
# - Run the tool with the -w flag to indicate an exported file or specify its file name below
|
|
234
251
|
LOGIN_REQUEST_BODY_FILE = ""
|
|
235
252
|
|
|
236
|
-
# Alternatively, set the
|
|
253
|
+
# Alternatively, you can manually set the DEVICE_ID, SYSTEM_ID, USER_URI_ID and REFRESH_TOKEN options
|
|
254
|
+
# (however, using the automated method described above is recommended)
|
|
255
|
+
#
|
|
256
|
+
# These values can be extracted using one of the following methods:
|
|
257
|
+
#
|
|
258
|
+
# - Run spotify_profile_monitor with the -w flag without specifying SPOTIFY_USER_URI_ID - it will decode the file and
|
|
259
|
+
# print the values to stdout, example:
|
|
260
|
+
# spotify_profile_monitor --token-source client -w <path-to-login-request-body-file>
|
|
237
261
|
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
# - USER_URI_ID
|
|
242
|
-
# - REFRESH_TOKEN
|
|
243
|
-
# and assign them to respective configuration options
|
|
262
|
+
# - Use the protoc tool (part of protobuf pip package):
|
|
263
|
+
# pip install protobuf
|
|
264
|
+
# protoc --decode_raw < <path-to-login-request-body-file>
|
|
244
265
|
#
|
|
245
|
-
#
|
|
246
|
-
# protoc --decode_raw < <path-to-login-request-body-file>
|
|
266
|
+
# - Use the built-in Protobuf decoder in your intercepting proxy (if supported)
|
|
247
267
|
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
268
|
+
# The Protobuf structure is as follows:
|
|
269
|
+
#
|
|
270
|
+
# {
|
|
271
|
+
# 1: {
|
|
272
|
+
# 1: "DEVICE_ID",
|
|
273
|
+
# 2: "SYSTEM_ID"
|
|
274
|
+
# },
|
|
275
|
+
# 100: {
|
|
276
|
+
# 1: "USER_URI_ID",
|
|
277
|
+
# 2: "REFRESH_TOKEN"
|
|
278
|
+
# }
|
|
279
|
+
# }
|
|
280
|
+
#
|
|
281
|
+
# Provide the extracted values below (DEVICE_ID, SYSTEM_ID, USER_URI_ID). The REFRESH_TOKEN secret can be
|
|
282
|
+
# supplied using one of the following methods:
|
|
250
283
|
# - Set it as an environment variable (e.g. export REFRESH_TOKEN=...)
|
|
251
284
|
# - Add it to ".env" file (REFRESH_TOKEN=...) for persistent use
|
|
252
|
-
# Fallback:
|
|
253
|
-
# - Hard-code it in the code or config file
|
|
285
|
+
# - Fallback: hard-code it in the code or config file
|
|
254
286
|
DEVICE_ID = "your_spotify_app_device_id"
|
|
255
287
|
SYSTEM_ID = "your_spotify_app_system_id"
|
|
256
288
|
USER_URI_ID = "your_spotify_user_uri_id"
|
|
257
289
|
REFRESH_TOKEN = "your_spotify_app_refresh_token"
|
|
258
290
|
|
|
259
|
-
#
|
|
291
|
+
# ----------------------------------------------
|
|
292
|
+
# Advanced options for 'client' token source
|
|
293
|
+
# Modifying the values below is NOT recommended!
|
|
294
|
+
# ----------------------------------------------
|
|
260
295
|
|
|
261
296
|
# Spotify login URL
|
|
262
297
|
LOGIN_URL = "https://login5.spotify.com/v3/login"
|
|
@@ -264,20 +299,58 @@ LOGIN_URL = "https://login5.spotify.com/v3/login"
|
|
|
264
299
|
# Spotify client token URL
|
|
265
300
|
CLIENTTOKEN_URL = "https://clienttoken.spotify.com/v1/clienttoken"
|
|
266
301
|
|
|
267
|
-
#
|
|
302
|
+
# Platform-specific values for token generation so the Spotify client token requests match your exact Spotify desktop
|
|
303
|
+
# client build (arch, OS build, app version etc.)
|
|
268
304
|
#
|
|
269
|
-
#
|
|
270
|
-
# Spotify
|
|
305
|
+
# - Run an intercepting proxy of your choice (like Proxyman)
|
|
306
|
+
# - Launch the Spotify desktop client and look for requests to: https://clienttoken.spotify.com/v1/clienttoken
|
|
307
|
+
# (these requests are sent every time client token expires, usually every 2 weeks)
|
|
308
|
+
# - Export the client token request body (a binary Protobuf payload) to a file
|
|
309
|
+
# (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <clienttoken-request-body-file>)
|
|
271
310
|
#
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
#
|
|
276
|
-
|
|
277
|
-
APP_VERSION = ""
|
|
311
|
+
# To automatically extract APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR and CLIENT_MODEL from the
|
|
312
|
+
# exported binary client token request Protobuf file:
|
|
313
|
+
#
|
|
314
|
+
# - Run the tool with the hidden -z flag to indicate an exported file or specify its file name below
|
|
315
|
+
CLIENTTOKEN_REQUEST_BODY_FILE = ""
|
|
278
316
|
|
|
279
|
-
#
|
|
280
|
-
#
|
|
317
|
+
# Alternatively, you can manually set the APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR and
|
|
318
|
+
# CLIENT_MODEL options
|
|
319
|
+
#
|
|
320
|
+
# These values can be extracted using one of the following methods:
|
|
321
|
+
#
|
|
322
|
+
# - run spotify_profile_monitor with the hidden -z flag without specifying SPOTIFY_USER_URI_ID - it will decode the file
|
|
323
|
+
# and print the values to stdout, example:
|
|
324
|
+
# spotify_profile_monitor --token-source client -z <path-to-clienttoken-request-body-file>
|
|
325
|
+
#
|
|
326
|
+
# - use the protoc tool (part of protobuf pip package):
|
|
327
|
+
# pip install protobuf
|
|
328
|
+
# protoc --decode_raw < <path-to-clienttoken-request-body-file>
|
|
329
|
+
#
|
|
330
|
+
# - use the built-in Protobuf decoder in your intercepting proxy (if supported)
|
|
331
|
+
#
|
|
332
|
+
# The Protobuf structure is as follows:
|
|
333
|
+
#
|
|
334
|
+
# 1: 1
|
|
335
|
+
# 2 {
|
|
336
|
+
# 1: "APP_VERSION"
|
|
337
|
+
# 2: "DEVICE_ID"
|
|
338
|
+
# 3 {
|
|
339
|
+
# 1 {
|
|
340
|
+
# 4 {
|
|
341
|
+
# 1: "CPU_ARCH"
|
|
342
|
+
# 3: "OS_BUILD"
|
|
343
|
+
# 4: "PLATFORM"
|
|
344
|
+
# 5: "OS_MAJOR"
|
|
345
|
+
# 6: "OS_MINOR"
|
|
346
|
+
# 8: "CLIENT_MODEL"
|
|
347
|
+
# }
|
|
348
|
+
# }
|
|
349
|
+
# 2: "SYSTEM_ID"
|
|
350
|
+
# }
|
|
351
|
+
# }
|
|
352
|
+
#
|
|
353
|
+
# Provide the extracted values below (except for DEVICE_ID and SYSTEM_ID as it was already provided via -w)
|
|
281
354
|
CPU_ARCH = 10
|
|
282
355
|
OS_BUILD = 19045
|
|
283
356
|
PLATFORM = 2
|
|
@@ -285,19 +358,11 @@ OS_MAJOR = 9
|
|
|
285
358
|
OS_MINOR = 9
|
|
286
359
|
CLIENT_MODEL = 34404
|
|
287
360
|
|
|
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 = ""
|
|
361
|
+
# App version (e.g. '1.2.62.580.g7e3d9a4f')
|
|
362
|
+
# Leave empty to auto-generate from USER_AGENT
|
|
363
|
+
APP_VERSION = ""
|
|
299
364
|
|
|
300
|
-
#
|
|
365
|
+
# ---------------------------------------------------------------------
|
|
301
366
|
"""
|
|
302
367
|
|
|
303
368
|
# -------------------------
|
|
@@ -311,7 +376,6 @@ SP_DC_COOKIE = ""
|
|
|
311
376
|
LOGIN_REQUEST_BODY_FILE = ""
|
|
312
377
|
CLIENTTOKEN_REQUEST_BODY_FILE = ""
|
|
313
378
|
LOGIN_URL = ""
|
|
314
|
-
USER_AGENT = ""
|
|
315
379
|
DEVICE_ID = ""
|
|
316
380
|
SYSTEM_ID = ""
|
|
317
381
|
USER_URI_ID = ""
|
|
@@ -349,6 +413,8 @@ SONG_ON_LOOP_VALUE = 0
|
|
|
349
413
|
SKIPPED_SONG_THRESHOLD = 0
|
|
350
414
|
SP_USER_GOT_OFFLINE_TRACK_ID = ""
|
|
351
415
|
SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 0
|
|
416
|
+
REMOVED_DISAPPEARED_COUNTER = 0
|
|
417
|
+
USER_AGENT = ""
|
|
352
418
|
LIVENESS_CHECK_INTERVAL = 0
|
|
353
419
|
CHECK_INTERNET_URL = ""
|
|
354
420
|
CHECK_INTERNET_TIMEOUT = 0
|
|
@@ -393,7 +459,6 @@ SP_CACHED_ACCESS_TOKEN = None
|
|
|
393
459
|
SP_CACHED_REFRESH_TOKEN = None
|
|
394
460
|
SP_ACCESS_TOKEN_EXPIRES_AT = 0
|
|
395
461
|
SP_CACHED_CLIENT_ID = ""
|
|
396
|
-
SP_CACHED_USER_AGENT = ""
|
|
397
462
|
|
|
398
463
|
# URL of the Spotify Web Player endpoint to get access token
|
|
399
464
|
TOKEN_URL = "https://open.spotify.com/api/token"
|
|
@@ -466,7 +531,8 @@ retry = Retry(
|
|
|
466
531
|
backoff_factor=1,
|
|
467
532
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
468
533
|
allowed_methods=["GET", "HEAD", "OPTIONS"],
|
|
469
|
-
raise_on_status=False
|
|
534
|
+
raise_on_status=False,
|
|
535
|
+
respect_retry_after_header=True
|
|
470
536
|
)
|
|
471
537
|
|
|
472
538
|
adapter = HTTPAdapter(max_retries=retry, pool_connections=100, pool_maxsize=100)
|
|
@@ -510,7 +576,7 @@ def signal_handler(sig, frame):
|
|
|
510
576
|
# Checks internet connectivity
|
|
511
577
|
def check_internet(url=CHECK_INTERNET_URL, timeout=CHECK_INTERNET_TIMEOUT, verify=VERIFY_SSL):
|
|
512
578
|
try:
|
|
513
|
-
_ = req.get(url, headers={'User-Agent':
|
|
579
|
+
_ = req.get(url, headers={'User-Agent': USER_AGENT}, timeout=timeout, verify=verify)
|
|
514
580
|
return True
|
|
515
581
|
except req.RequestException as e:
|
|
516
582
|
print(f"* No connectivity, please check your network:\n\n{e}")
|
|
@@ -928,7 +994,7 @@ def reload_secrets_signal_handler(sig, frame):
|
|
|
928
994
|
if LOGIN_REQUEST_BODY_FILE:
|
|
929
995
|
if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
|
|
930
996
|
try:
|
|
931
|
-
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN =
|
|
997
|
+
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN = parse_login_request_body_file(LOGIN_REQUEST_BODY_FILE)
|
|
932
998
|
except Exception as e:
|
|
933
999
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
|
|
934
1000
|
else:
|
|
@@ -1118,10 +1184,13 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
|
|
|
1118
1184
|
def generate_totp():
|
|
1119
1185
|
import pyotp
|
|
1120
1186
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1187
|
+
secret_cipher_dict = {
|
|
1188
|
+
"8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
|
|
1189
|
+
"7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
|
|
1190
|
+
"6": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
|
|
1191
|
+
"5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54]
|
|
1192
|
+
}
|
|
1193
|
+
secret_cipher_bytes = secret_cipher_dict["8"]
|
|
1125
1194
|
|
|
1126
1195
|
transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
|
|
1127
1196
|
joined = "".join(str(num) for num in transformed)
|
|
@@ -1131,8 +1200,8 @@ def generate_totp():
|
|
|
1131
1200
|
return pyotp.TOTP(secret, digits=6, interval=30)
|
|
1132
1201
|
|
|
1133
1202
|
|
|
1134
|
-
#
|
|
1135
|
-
def
|
|
1203
|
+
# Refreshes the Spotify access token using the sp_dc cookie, tries first with mode "transport" and if needed with "init"
|
|
1204
|
+
def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
|
|
1136
1205
|
transport = True
|
|
1137
1206
|
init = True
|
|
1138
1207
|
session = req.Session()
|
|
@@ -1140,8 +1209,7 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1140
1209
|
data: dict = {}
|
|
1141
1210
|
token = ""
|
|
1142
1211
|
|
|
1143
|
-
|
|
1144
|
-
server_time = fetch_server_time(session, ua)
|
|
1212
|
+
server_time = fetch_server_time(session, USER_AGENT)
|
|
1145
1213
|
totp_obj = generate_totp()
|
|
1146
1214
|
client_time = int(time_ns() / 1000 / 1000)
|
|
1147
1215
|
otp_value = totp_obj.at(server_time)
|
|
@@ -1151,7 +1219,7 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1151
1219
|
"productType": "web-player",
|
|
1152
1220
|
"totp": otp_value,
|
|
1153
1221
|
"totpServer": otp_value,
|
|
1154
|
-
"totpVer":
|
|
1222
|
+
"totpVer": 8,
|
|
1155
1223
|
"sTime": server_time,
|
|
1156
1224
|
"cTime": client_time,
|
|
1157
1225
|
"buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
|
|
@@ -1159,13 +1227,15 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1159
1227
|
}
|
|
1160
1228
|
|
|
1161
1229
|
headers = {
|
|
1162
|
-
"User-Agent":
|
|
1230
|
+
"User-Agent": USER_AGENT,
|
|
1163
1231
|
"Accept": "application/json",
|
|
1164
1232
|
"Referer": "https://open.spotify.com/",
|
|
1165
1233
|
"App-Platform": "WebPlayer",
|
|
1166
1234
|
"Cookie": f"sp_dc={sp_dc}",
|
|
1167
1235
|
}
|
|
1168
1236
|
|
|
1237
|
+
last_err = ""
|
|
1238
|
+
|
|
1169
1239
|
try:
|
|
1170
1240
|
if platform.system() != "Windows":
|
|
1171
1241
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
@@ -1176,13 +1246,14 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1176
1246
|
data = response.json()
|
|
1177
1247
|
token = data.get("accessToken", "")
|
|
1178
1248
|
|
|
1179
|
-
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1249
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError) as e:
|
|
1180
1250
|
transport = False
|
|
1251
|
+
last_err = str(e)
|
|
1181
1252
|
finally:
|
|
1182
1253
|
if platform.system() != "Windows":
|
|
1183
1254
|
signal.alarm(0)
|
|
1184
1255
|
|
|
1185
|
-
if not transport or (transport and not check_token_validity(token, data.get("clientId", ""),
|
|
1256
|
+
if not transport or (transport and not check_token_validity(token, data.get("clientId", ""), USER_AGENT)):
|
|
1186
1257
|
params["reason"] = "init"
|
|
1187
1258
|
|
|
1188
1259
|
try:
|
|
@@ -1195,58 +1266,52 @@ def refresh_token(sp_dc: str) -> dict:
|
|
|
1195
1266
|
data = response.json()
|
|
1196
1267
|
token = data.get("accessToken", "")
|
|
1197
1268
|
|
|
1198
|
-
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1269
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError) as e:
|
|
1199
1270
|
init = False
|
|
1271
|
+
last_err = str(e)
|
|
1200
1272
|
finally:
|
|
1201
1273
|
if platform.system() != "Windows":
|
|
1202
1274
|
signal.alarm(0)
|
|
1203
1275
|
|
|
1204
1276
|
if not init or not data or "accessToken" not in data:
|
|
1205
|
-
raise Exception("
|
|
1277
|
+
raise Exception(f"refresh_access_token_from_sp_dc(): Unsuccessful token request{': ' + last_err if last_err else ''}")
|
|
1206
1278
|
|
|
1207
1279
|
return {
|
|
1208
1280
|
"access_token": token,
|
|
1209
1281
|
"expires_at": data["accessTokenExpirationTimestampMs"] // 1000,
|
|
1210
1282
|
"client_id": data.get("clientId", ""),
|
|
1211
|
-
"user_agent": ua,
|
|
1212
1283
|
"length": len(token)
|
|
1213
1284
|
}
|
|
1214
1285
|
|
|
1215
1286
|
|
|
1216
1287
|
# Fetches Spotify access token based on provided SP_DC value
|
|
1217
1288
|
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
|
|
1289
|
+
global SP_CACHED_ACCESS_TOKEN, SP_ACCESS_TOKEN_EXPIRES_AT, SP_CACHED_CLIENT_ID
|
|
1219
1290
|
|
|
1220
1291
|
now = time.time()
|
|
1221
1292
|
|
|
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,
|
|
1293
|
+
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
1294
|
return SP_CACHED_ACCESS_TOKEN
|
|
1224
1295
|
|
|
1225
1296
|
max_retries = TOKEN_MAX_RETRIES
|
|
1226
1297
|
retry = 0
|
|
1227
1298
|
|
|
1228
1299
|
while retry < max_retries:
|
|
1229
|
-
token_data =
|
|
1300
|
+
token_data = refresh_access_token_from_sp_dc(sp_dc)
|
|
1230
1301
|
token = token_data["access_token"]
|
|
1231
1302
|
client_id = token_data.get("client_id", "")
|
|
1232
|
-
user_agent = token_data.get("user_agent", get_random_user_agent())
|
|
1233
1303
|
length = token_data["length"]
|
|
1234
1304
|
|
|
1235
1305
|
SP_CACHED_ACCESS_TOKEN = token
|
|
1236
1306
|
SP_ACCESS_TOKEN_EXPIRES_AT = token_data["expires_at"]
|
|
1237
1307
|
SP_CACHED_CLIENT_ID = client_id
|
|
1238
|
-
SP_CACHED_USER_AGENT = user_agent
|
|
1239
1308
|
|
|
1240
|
-
if SP_CACHED_ACCESS_TOKEN is None or not check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID,
|
|
1309
|
+
if SP_CACHED_ACCESS_TOKEN is None or not check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, USER_AGENT):
|
|
1241
1310
|
retry += 1
|
|
1242
1311
|
time.sleep(TOKEN_RETRY_TIMEOUT)
|
|
1243
1312
|
else:
|
|
1244
|
-
# print("* Token is valid")
|
|
1245
1313
|
break
|
|
1246
1314
|
|
|
1247
|
-
# print("Spotify Access Token:", SP_CACHED_ACCESS_TOKEN)
|
|
1248
|
-
# print("Token expires at:", time.ctime(SP_TOKEN_EXPIRES_AT))
|
|
1249
|
-
|
|
1250
1315
|
if retry == max_retries:
|
|
1251
1316
|
if SP_CACHED_ACCESS_TOKEN is not None:
|
|
1252
1317
|
print(f"* Token appears to be still invalid after {max_retries} attempts, returning token anyway")
|
|
@@ -1319,13 +1384,15 @@ def encode_nested_field(tag, nested_bytes):
|
|
|
1319
1384
|
# Builds the Spotify Protobuf login request body
|
|
1320
1385
|
def build_spotify_auth_protobuf(device_id, system_id, user_uri_id, refresh_token):
|
|
1321
1386
|
"""
|
|
1322
|
-
|
|
1323
|
-
1:
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1387
|
+
{
|
|
1388
|
+
1: {
|
|
1389
|
+
1: "device_id",
|
|
1390
|
+
2: "system_id"
|
|
1391
|
+
},
|
|
1392
|
+
100: {
|
|
1393
|
+
1: "user_uri_id",
|
|
1394
|
+
2: "refresh_token"
|
|
1395
|
+
}
|
|
1329
1396
|
}
|
|
1330
1397
|
"""
|
|
1331
1398
|
device_info_msg = encode_string_field(1, device_id) + encode_string_field(2, system_id)
|
|
@@ -1390,7 +1457,7 @@ def parse_protobuf_message(data):
|
|
|
1390
1457
|
|
|
1391
1458
|
# Parses the Protobuf-encoded login request body file (as dumped for example by Proxyman) and returns a tuple:
|
|
1392
1459
|
# (device_id, system_id, user_uri_id, refresh_token)
|
|
1393
|
-
def
|
|
1460
|
+
def parse_login_request_body_file(file_path):
|
|
1394
1461
|
"""
|
|
1395
1462
|
{
|
|
1396
1463
|
1: {
|
|
@@ -1467,6 +1534,26 @@ def ensure_dict(value):
|
|
|
1467
1534
|
# Parses the Protobuf-encoded client token request body file (as dumped for example by Proxyman) and returns a tuple:
|
|
1468
1535
|
# (app_version, device_id, system_id, cpu_arch, os_build, platform, os_major, os_minor, client_model)
|
|
1469
1536
|
def parse_clienttoken_request_body_file(file_path):
|
|
1537
|
+
"""
|
|
1538
|
+
1: 1 (const)
|
|
1539
|
+
2: {
|
|
1540
|
+
1: "app_version"
|
|
1541
|
+
2: "device_id"
|
|
1542
|
+
3: {
|
|
1543
|
+
1: {
|
|
1544
|
+
4: {
|
|
1545
|
+
1: "cpu_arch"
|
|
1546
|
+
3: "os_build"
|
|
1547
|
+
4: "platform"
|
|
1548
|
+
5: "os_major"
|
|
1549
|
+
6: "os_minor"
|
|
1550
|
+
8: "client_model"
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
2: "system_id"
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
"""
|
|
1470
1557
|
|
|
1471
1558
|
with open(file_path, "rb") as f:
|
|
1472
1559
|
data = f.read()
|
|
@@ -1527,13 +1614,20 @@ def build_clienttoken_request_protobuf(app_version, device_id, system_id, cpu_ar
|
|
|
1527
1614
|
"""
|
|
1528
1615
|
1: 1 (const)
|
|
1529
1616
|
2: {
|
|
1530
|
-
1: app_version
|
|
1531
|
-
2: device_id
|
|
1617
|
+
1: "app_version"
|
|
1618
|
+
2: "device_id"
|
|
1532
1619
|
3: {
|
|
1533
1620
|
1: {
|
|
1534
|
-
4:
|
|
1621
|
+
4: {
|
|
1622
|
+
1: "cpu_arch"
|
|
1623
|
+
3: "os_build"
|
|
1624
|
+
4: "platform"
|
|
1625
|
+
5: "os_major"
|
|
1626
|
+
6: "os_minor"
|
|
1627
|
+
8: "client_model"
|
|
1628
|
+
}
|
|
1535
1629
|
}
|
|
1536
|
-
2: system_id
|
|
1630
|
+
2: "system_id"
|
|
1537
1631
|
}
|
|
1538
1632
|
}
|
|
1539
1633
|
"""
|
|
@@ -1605,6 +1699,22 @@ def spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refr
|
|
|
1605
1699
|
elif response.headers.get("client-token-error") == "EXPIRED_CLIENTTOKEN":
|
|
1606
1700
|
raise Exception(f"Request failed with status {response.status_code}: expired client token")
|
|
1607
1701
|
|
|
1702
|
+
try:
|
|
1703
|
+
error_json = response.json()
|
|
1704
|
+
except ValueError:
|
|
1705
|
+
error_json = {}
|
|
1706
|
+
|
|
1707
|
+
if error_json.get("error") == "invalid_grant":
|
|
1708
|
+
desc = error_json.get("error_description", "")
|
|
1709
|
+
if "refresh token" in desc.lower() and "revoked" in desc.lower():
|
|
1710
|
+
raise Exception(f"Request failed with status {response.status_code}: refresh token has been revoked")
|
|
1711
|
+
elif "refresh token" in desc.lower() and "expired" in desc.lower():
|
|
1712
|
+
raise Exception(f"Request failed with status {response.status_code}: refresh token has expired")
|
|
1713
|
+
elif "invalid refresh token" in desc.lower():
|
|
1714
|
+
raise Exception(f"Request failed with status {response.status_code}: refresh token is invalid")
|
|
1715
|
+
else:
|
|
1716
|
+
raise Exception(f"Request failed with status {response.status_code}: invalid grant during refresh ({desc})")
|
|
1717
|
+
|
|
1608
1718
|
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
1719
|
|
|
1610
1720
|
parsed = parse_protobuf_message(response.content)
|
|
@@ -1735,16 +1845,14 @@ def spotify_get_access_token_from_client_auto(device_id, system_id, user_uri_id,
|
|
|
1735
1845
|
# Fetches list of Spotify friends
|
|
1736
1846
|
def spotify_get_friends_json(access_token):
|
|
1737
1847
|
url = "https://guc-spclient.spotify.com/presence-view/v1/buddylist"
|
|
1738
|
-
headers = {
|
|
1848
|
+
headers = {
|
|
1849
|
+
"Authorization": f"Bearer {access_token}",
|
|
1850
|
+
"User-Agent": USER_AGENT
|
|
1851
|
+
}
|
|
1739
1852
|
|
|
1740
1853
|
if TOKEN_SOURCE == "cookie":
|
|
1741
1854
|
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
|
|
1855
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1748
1856
|
})
|
|
1749
1857
|
|
|
1750
1858
|
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
@@ -1833,8 +1941,8 @@ def spotify_list_friends(friend_activity):
|
|
|
1833
1941
|
|
|
1834
1942
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
1835
1943
|
|
|
1836
|
-
print(f"Apple
|
|
1837
|
-
print(f"YouTube Music
|
|
1944
|
+
print(f"Apple Music URL:\t\t{apple_search_url}")
|
|
1945
|
+
print(f"YouTube Music URL:\t\t{youtube_music_search_url}")
|
|
1838
1946
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
1839
1947
|
|
|
1840
1948
|
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 +1974,14 @@ def spotify_get_friend_info(friend_activity, uri):
|
|
|
1866
1974
|
def spotify_get_track_info(access_token, track_uri):
|
|
1867
1975
|
track_id = track_uri.split(':', 2)[2]
|
|
1868
1976
|
url = "https://api.spotify.com/v1/tracks/" + track_id
|
|
1869
|
-
headers = {
|
|
1977
|
+
headers = {
|
|
1978
|
+
"Authorization": f"Bearer {access_token}",
|
|
1979
|
+
"User-Agent": USER_AGENT
|
|
1980
|
+
}
|
|
1870
1981
|
|
|
1871
1982
|
if TOKEN_SOURCE == "cookie":
|
|
1872
1983
|
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
|
|
1984
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1879
1985
|
})
|
|
1880
1986
|
# add si parameter so link opens in native Spotify app after clicking
|
|
1881
1987
|
si = "?si=1"
|
|
@@ -1900,16 +2006,14 @@ def spotify_get_track_info(access_token, track_uri):
|
|
|
1900
2006
|
def spotify_get_playlist_info(access_token, playlist_uri):
|
|
1901
2007
|
playlist_id = playlist_uri.split(':', 2)[2]
|
|
1902
2008
|
url = f"https://api.spotify.com/v1/playlists/{playlist_id}?fields=name,owner,followers,external_urls"
|
|
1903
|
-
headers = {
|
|
2009
|
+
headers = {
|
|
2010
|
+
"Authorization": f"Bearer {access_token}",
|
|
2011
|
+
"User-Agent": USER_AGENT
|
|
2012
|
+
}
|
|
1904
2013
|
|
|
1905
2014
|
if TOKEN_SOURCE == "cookie":
|
|
1906
2015
|
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
|
|
2016
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1913
2017
|
})
|
|
1914
2018
|
# add si parameter so link opens in native Spotify app after clicking
|
|
1915
2019
|
si = "?si=1"
|
|
@@ -1931,16 +2035,14 @@ def spotify_get_playlist_info(access_token, playlist_uri):
|
|
|
1931
2035
|
# Gets basic information about access token owner
|
|
1932
2036
|
def spotify_get_current_user(access_token) -> dict | None:
|
|
1933
2037
|
url = "https://api.spotify.com/v1/me"
|
|
1934
|
-
headers = {
|
|
2038
|
+
headers = {
|
|
2039
|
+
"Authorization": f"Bearer {access_token}",
|
|
2040
|
+
"User-Agent": USER_AGENT
|
|
2041
|
+
}
|
|
1935
2042
|
|
|
1936
2043
|
if TOKEN_SOURCE == "cookie":
|
|
1937
2044
|
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
|
|
2045
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
1944
2046
|
})
|
|
1945
2047
|
|
|
1946
2048
|
if platform.system() != 'Windows':
|
|
@@ -1969,6 +2071,29 @@ def spotify_get_current_user(access_token) -> dict | None:
|
|
|
1969
2071
|
signal.alarm(0)
|
|
1970
2072
|
|
|
1971
2073
|
|
|
2074
|
+
# Checks if a Spotify user URI ID has been deleted
|
|
2075
|
+
def is_user_removed(access_token, user_uri_id):
|
|
2076
|
+
url = f"https://api.spotify.com/v1/users/{user_uri_id}"
|
|
2077
|
+
|
|
2078
|
+
headers = {
|
|
2079
|
+
"Authorization": f"Bearer {access_token}",
|
|
2080
|
+
"User-Agent": USER_AGENT
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if TOKEN_SOURCE == "cookie":
|
|
2084
|
+
headers.update({
|
|
2085
|
+
"Client-Id": SP_CACHED_CLIENT_ID
|
|
2086
|
+
})
|
|
2087
|
+
|
|
2088
|
+
try:
|
|
2089
|
+
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
2090
|
+
if response.status_code == 404:
|
|
2091
|
+
return True
|
|
2092
|
+
return False
|
|
2093
|
+
except Exception:
|
|
2094
|
+
return False
|
|
2095
|
+
|
|
2096
|
+
|
|
1972
2097
|
def spotify_macos_play_song(sp_track_uri_id, method=SPOTIFY_MACOS_PLAYING_METHOD):
|
|
1973
2098
|
if method == "apple-script": # apple-script
|
|
1974
2099
|
script = f'tell app "Spotify" to play track "spotify:track:{sp_track_uri_id}"'
|
|
@@ -2061,7 +2186,7 @@ def resolve_executable(path):
|
|
|
2061
2186
|
raise FileNotFoundError(f"Could not find executable '{path}'")
|
|
2062
2187
|
|
|
2063
2188
|
|
|
2064
|
-
#
|
|
2189
|
+
# Monitors music activity of the specified Spotify friend's user URI ID
|
|
2065
2190
|
def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
2066
2191
|
global SP_CACHED_ACCESS_TOKEN
|
|
2067
2192
|
sp_active_ts_start = 0
|
|
@@ -2081,6 +2206,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2081
2206
|
error_500_start_ts = 0
|
|
2082
2207
|
error_network_issue_counter = 0
|
|
2083
2208
|
error_network_issue_start_ts = 0
|
|
2209
|
+
sp_accessToken = ""
|
|
2084
2210
|
|
|
2085
2211
|
try:
|
|
2086
2212
|
if csv_file_name:
|
|
@@ -2132,25 +2258,25 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2132
2258
|
if TOKEN_SOURCE == 'cookie' and '401' in err:
|
|
2133
2259
|
SP_CACHED_ACCESS_TOKEN = None
|
|
2134
2260
|
|
|
2135
|
-
client_errs = ['access token', 'invalid client token', 'expired client token']
|
|
2136
|
-
cookie_errs = ['access token', 'unsuccessful token request']
|
|
2261
|
+
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']
|
|
2262
|
+
cookie_errs = ['access token', 'unauthorized', 'unsuccessful token request']
|
|
2137
2263
|
|
|
2138
2264
|
if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
|
|
2139
|
-
print(f"* Error: client
|
|
2265
|
+
print(f"* Error: client or refresh token may be invalid or expired!")
|
|
2140
2266
|
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
|
|
2267
|
+
m_subject = f"spotify_monitor: client or refresh token may be invalid or expired! (uri: {user_uri_id})"
|
|
2268
|
+
m_body = f"Client or refresh token may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2269
|
+
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
2270
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2145
2271
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2146
2272
|
email_sent = True
|
|
2147
2273
|
|
|
2148
2274
|
elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
|
|
2149
|
-
print(f"* Error: sp_dc
|
|
2275
|
+
print(f"* Error: sp_dc may be invalid/expired or Spotify has broken sth again!")
|
|
2150
2276
|
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
|
|
2277
|
+
m_subject = f"spotify_monitor: sp_dc may be invalid/expired or Spotify has broken sth again! (uri: {user_uri_id})"
|
|
2278
|
+
m_body = f"sp_dc may be invalid/expired or Spotify has broken sth again!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2279
|
+
m_body_html = f"<html><head></head><body>sp_dc may be invalid/expired or Spotify has broken sth again!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2154
2280
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2155
2281
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2156
2282
|
email_sent = True
|
|
@@ -2259,8 +2385,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2259
2385
|
|
|
2260
2386
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
2261
2387
|
|
|
2262
|
-
print(f"Apple
|
|
2263
|
-
print(f"YouTube Music
|
|
2388
|
+
print(f"Apple Music URL:\t\t{apple_search_url}")
|
|
2389
|
+
print(f"YouTube Music URL:\t\t{youtube_music_search_url}")
|
|
2264
2390
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
2265
2391
|
|
|
2266
2392
|
if not is_playlist:
|
|
@@ -2287,8 +2413,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2287
2413
|
|
|
2288
2414
|
if ACTIVE_NOTIFICATION:
|
|
2289
2415
|
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
|
|
2416
|
+
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: ')}"
|
|
2417
|
+
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
2418
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2293
2419
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2294
2420
|
|
|
@@ -2313,7 +2439,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2313
2439
|
|
|
2314
2440
|
email_sent = False
|
|
2315
2441
|
|
|
2316
|
-
|
|
2442
|
+
disappeared_counter = 0
|
|
2443
|
+
|
|
2444
|
+
# Primary loop
|
|
2317
2445
|
while True:
|
|
2318
2446
|
|
|
2319
2447
|
while True:
|
|
@@ -2379,25 +2507,25 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2379
2507
|
elif not error_500_start_ts and not error_network_issue_start_ts:
|
|
2380
2508
|
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: '{e}'")
|
|
2381
2509
|
|
|
2382
|
-
client_errs = ['access token', 'invalid client token', 'expired client token']
|
|
2383
|
-
cookie_errs = ['access token', 'unsuccessful token request']
|
|
2510
|
+
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']
|
|
2511
|
+
cookie_errs = ['access token', 'unauthorized', 'unsuccessful token request']
|
|
2384
2512
|
|
|
2385
2513
|
if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
|
|
2386
|
-
print(f"* Error: client
|
|
2514
|
+
print(f"* Error: client or refresh token may be invalid or expired!")
|
|
2387
2515
|
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
|
|
2516
|
+
m_subject = f"spotify_monitor: client or refresh token may be invalid or expired! (uri: {user_uri_id})"
|
|
2517
|
+
m_body = f"Client or refresh token may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2518
|
+
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
2519
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2392
2520
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2393
2521
|
email_sent = True
|
|
2394
2522
|
|
|
2395
2523
|
elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
|
|
2396
|
-
print(f"* Error: sp_dc
|
|
2524
|
+
print(f"* Error: sp_dc may be invalid/expired or Spotify has broken sth again!")
|
|
2397
2525
|
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
|
|
2526
|
+
m_subject = f"spotify_monitor: sp_dc may be invalid/expired or Spotify has broken sth again! (uri: {user_uri_id})"
|
|
2527
|
+
m_body = f"sp_dc may be invalid/expired or Spotify has broken sth again!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2528
|
+
m_body_html = f"<html><head></head><body>sp_dc may be invalid/expired or Spotify has broken sth again!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
2401
2529
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2402
2530
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2403
2531
|
email_sent = True
|
|
@@ -2406,27 +2534,41 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2406
2534
|
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
2407
2535
|
|
|
2408
2536
|
if sp_found is False:
|
|
2409
|
-
# User disappeared from the Spotify's friend list
|
|
2537
|
+
# User has disappeared from the Spotify's friend list or account has been removed
|
|
2538
|
+
disappeared_counter += 1
|
|
2539
|
+
if disappeared_counter < REMOVED_DISAPPEARED_COUNTER:
|
|
2540
|
+
time.sleep(SPOTIFY_CHECK_INTERVAL)
|
|
2541
|
+
continue
|
|
2410
2542
|
if user_not_found is False:
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2543
|
+
if is_user_removed(sp_accessToken, user_uri_id):
|
|
2544
|
+
print(f"Spotify user '{user_uri_id}' ({sp_username}) was probably removed! Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
2545
|
+
if ERROR_NOTIFICATION:
|
|
2546
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) was probably removed!"
|
|
2547
|
+
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: ')}"
|
|
2548
|
+
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>"
|
|
2549
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2550
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2551
|
+
else:
|
|
2552
|
+
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")
|
|
2553
|
+
if ERROR_NOTIFICATION:
|
|
2554
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) has disappeared!"
|
|
2555
|
+
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: ')}"
|
|
2556
|
+
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>"
|
|
2557
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2558
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2418
2559
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2419
2560
|
user_not_found = True
|
|
2420
2561
|
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
2421
2562
|
continue
|
|
2422
2563
|
else:
|
|
2423
2564
|
# User reappeared in the Spotify's friend list
|
|
2565
|
+
disappeared_counter = 0
|
|
2424
2566
|
if user_not_found is True:
|
|
2425
|
-
print(f"Spotify user {user_uri_id} ({sp_username})
|
|
2567
|
+
print(f"Spotify user {user_uri_id} ({sp_username}) has reappeared!")
|
|
2426
2568
|
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})
|
|
2569
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) has reappeared!"
|
|
2570
|
+
m_body = f"Spotify user {user_uri_id} ({sp_username}) has reappeared!{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
2571
|
+
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
2572
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2431
2573
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2432
2574
|
print_cur_ts("Timestamp:\t\t\t")
|
|
@@ -2558,8 +2700,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2558
2700
|
|
|
2559
2701
|
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
2560
2702
|
|
|
2561
|
-
print(f"Apple
|
|
2562
|
-
print(f"YouTube Music
|
|
2703
|
+
print(f"Apple Music URL:\t\t{apple_search_url}")
|
|
2704
|
+
print(f"YouTube Music URL:\t\t{youtube_music_search_url}")
|
|
2563
2705
|
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
2564
2706
|
|
|
2565
2707
|
if not is_playlist:
|
|
@@ -2578,6 +2720,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2578
2720
|
listened_songs = 1
|
|
2579
2721
|
skipped_songs = 0
|
|
2580
2722
|
looped_songs = 0
|
|
2723
|
+
song_on_loop = 1
|
|
2581
2724
|
|
|
2582
2725
|
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
2726
|
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 +2737,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2594
2737
|
sp_active_ts_start = sp_active_ts_start_old
|
|
2595
2738
|
sp_active_ts_stop = 0
|
|
2596
2739
|
|
|
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
|
|
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}{friend_active_m_body}\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>{friend_active_m_body_html}<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
2599
2742
|
|
|
2600
2743
|
if ACTIVE_NOTIFICATION:
|
|
2601
2744
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2609,16 +2752,16 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2609
2752
|
|
|
2610
2753
|
if (TRACK_NOTIFICATION and on_the_list and not email_sent) or (SONG_NOTIFICATION and not email_sent):
|
|
2611
2754
|
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
|
|
2755
|
+
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: ')}"
|
|
2756
|
+
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
2757
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2615
2758
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2616
2759
|
email_sent = True
|
|
2617
2760
|
|
|
2618
2761
|
if song_on_loop == SONG_ON_LOOP_VALUE and SONG_ON_LOOP_NOTIFICATION:
|
|
2619
2762
|
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
|
|
2763
|
+
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: ')}"
|
|
2764
|
+
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
2765
|
if not email_sent:
|
|
2623
2766
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2624
2767
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
@@ -2685,8 +2828,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2685
2828
|
spotify_linux_play_pause("pause")
|
|
2686
2829
|
if INACTIVE_NOTIFICATION:
|
|
2687
2830
|
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
|
|
2831
|
+
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: ')}"
|
|
2832
|
+
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
2833
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2691
2834
|
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
2692
2835
|
email_sent = True
|
|
@@ -2698,6 +2841,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2698
2841
|
listened_songs = 0
|
|
2699
2842
|
looped_songs = 0
|
|
2700
2843
|
skipped_songs = 0
|
|
2844
|
+
song_on_loop = 0
|
|
2701
2845
|
print_cur_ts("\nTimestamp:\t\t\t")
|
|
2702
2846
|
|
|
2703
2847
|
if LIVENESS_CHECK_COUNTER and alive_counter >= LIVENESS_CHECK_COUNTER:
|
|
@@ -2725,7 +2869,10 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2725
2869
|
# User is not found in the Spotify's friend list just after starting the tool
|
|
2726
2870
|
else:
|
|
2727
2871
|
if user_not_found is False:
|
|
2728
|
-
|
|
2872
|
+
if is_user_removed(sp_accessToken, user_uri_id):
|
|
2873
|
+
print(f"User '{user_uri_id}' does not exist! Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
2874
|
+
else:
|
|
2875
|
+
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
2876
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2730
2877
|
user_not_found = True
|
|
2731
2878
|
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
@@ -2733,7 +2880,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
|
2733
2880
|
|
|
2734
2881
|
|
|
2735
2882
|
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
|
|
2883
|
+
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
2884
|
|
|
2738
2885
|
if "--generate-config" in sys.argv:
|
|
2739
2886
|
print(CONFIG_BLOCK.strip("\n"))
|
|
@@ -2801,9 +2948,9 @@ def main():
|
|
|
2801
2948
|
help="Method to obtain Spotify access token: 'cookie' (via sp_dc cookie) or 'client' (via desktop client login protobuf)"
|
|
2802
2949
|
)
|
|
2803
2950
|
|
|
2804
|
-
#
|
|
2805
|
-
|
|
2806
|
-
|
|
2951
|
+
# Auth details used when token source is set to cookie
|
|
2952
|
+
cookie_auth = parser.add_argument_group("Auth details for 'cookie' token source")
|
|
2953
|
+
cookie_auth.add_argument(
|
|
2807
2954
|
"-u", "--spotify-dc-cookie",
|
|
2808
2955
|
dest="spotify_dc_cookie",
|
|
2809
2956
|
metavar="SP_DC_COOKIE",
|
|
@@ -2811,20 +2958,21 @@ def main():
|
|
|
2811
2958
|
help="Spotify sp_dc cookie"
|
|
2812
2959
|
)
|
|
2813
2960
|
|
|
2814
|
-
#
|
|
2815
|
-
|
|
2816
|
-
|
|
2961
|
+
# Auth details used when token source is set to client
|
|
2962
|
+
client_auth = parser.add_argument_group("Auth details for 'client' token source")
|
|
2963
|
+
client_auth.add_argument(
|
|
2817
2964
|
"-w", "--login-request-body-file",
|
|
2818
2965
|
dest="login_request_body_file",
|
|
2819
2966
|
metavar="PROTOBUF_FILENAME",
|
|
2820
2967
|
help="Read device_id, system_id, user_uri_id and refresh_token from binary Protobuf login file"
|
|
2821
2968
|
)
|
|
2822
2969
|
|
|
2823
|
-
|
|
2970
|
+
client_auth.add_argument(
|
|
2824
2971
|
"-z", "--clienttoken-request-body-file",
|
|
2825
2972
|
dest="clienttoken_request_body_file",
|
|
2826
2973
|
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"
|
|
2974
|
+
# help="Read app_version, cpu_arch, os_build, platform, os_major, os_minor and client_model from binary Protobuf client token file"
|
|
2975
|
+
help=argparse.SUPPRESS
|
|
2828
2976
|
)
|
|
2829
2977
|
|
|
2830
2978
|
# Notifications
|
|
@@ -2940,6 +3088,13 @@ def main():
|
|
|
2940
3088
|
type=str,
|
|
2941
3089
|
help="Filename with Spotify tracks/playlists/albums to alert on"
|
|
2942
3090
|
)
|
|
3091
|
+
opts.add_argument(
|
|
3092
|
+
"--user-agent",
|
|
3093
|
+
dest="user_agent",
|
|
3094
|
+
metavar="USER_AGENT",
|
|
3095
|
+
type=str,
|
|
3096
|
+
help="Specify a custom user agent for Spotify API requests; leave empty to auto-generate it"
|
|
3097
|
+
)
|
|
2943
3098
|
opts.add_argument(
|
|
2944
3099
|
"-y", "--file-suffix",
|
|
2945
3100
|
dest="file_suffix",
|
|
@@ -3003,7 +3158,7 @@ def main():
|
|
|
3003
3158
|
except ImportError:
|
|
3004
3159
|
env_path = DOTENV_FILE if DOTENV_FILE else None
|
|
3005
3160
|
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
|
|
3161
|
+
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
3162
|
|
|
3008
3163
|
if env_path:
|
|
3009
3164
|
for secret in SECRET_KEYS:
|
|
@@ -3022,7 +3177,16 @@ def main():
|
|
|
3022
3177
|
try:
|
|
3023
3178
|
import pyotp
|
|
3024
3179
|
except ModuleNotFoundError:
|
|
3025
|
-
raise SystemExit("Error: Couldn't find the pyotp library !\n\nTo install it, run:\n
|
|
3180
|
+
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")
|
|
3181
|
+
|
|
3182
|
+
if args.user_agent:
|
|
3183
|
+
USER_AGENT = args.user_agent
|
|
3184
|
+
|
|
3185
|
+
if not USER_AGENT:
|
|
3186
|
+
if TOKEN_SOURCE == "client":
|
|
3187
|
+
USER_AGENT = get_random_spotify_user_agent()
|
|
3188
|
+
else:
|
|
3189
|
+
USER_AGENT = get_random_user_agent()
|
|
3026
3190
|
|
|
3027
3191
|
if not check_internet():
|
|
3028
3192
|
sys.exit(1)
|
|
@@ -3057,7 +3221,7 @@ def main():
|
|
|
3057
3221
|
if LOGIN_REQUEST_BODY_FILE:
|
|
3058
3222
|
if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
|
|
3059
3223
|
try:
|
|
3060
|
-
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN =
|
|
3224
|
+
DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN = parse_login_request_body_file(LOGIN_REQUEST_BODY_FILE)
|
|
3061
3225
|
except Exception as e:
|
|
3062
3226
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
|
|
3063
3227
|
sys.exit(1)
|
|
@@ -3073,22 +3237,28 @@ def main():
|
|
|
3073
3237
|
print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) does not exist")
|
|
3074
3238
|
sys.exit(1)
|
|
3075
3239
|
|
|
3076
|
-
|
|
3077
|
-
|
|
3240
|
+
vals = {
|
|
3241
|
+
"LOGIN_URL": LOGIN_URL,
|
|
3242
|
+
"USER_AGENT": USER_AGENT,
|
|
3243
|
+
"DEVICE_ID": DEVICE_ID,
|
|
3244
|
+
"SYSTEM_ID": SYSTEM_ID,
|
|
3245
|
+
"USER_URI_ID": USER_URI_ID,
|
|
3246
|
+
"REFRESH_TOKEN": REFRESH_TOKEN,
|
|
3247
|
+
}
|
|
3248
|
+
placeholders = {
|
|
3249
|
+
"DEVICE_ID": "your_spotify_app_device_id",
|
|
3250
|
+
"SYSTEM_ID": "your_spotify_app_system_id",
|
|
3251
|
+
"USER_URI_ID": "your_spotify_user_uri_id",
|
|
3252
|
+
"REFRESH_TOKEN": "your_spotify_app_refresh_token",
|
|
3253
|
+
}
|
|
3078
3254
|
|
|
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")
|
|
3255
|
+
bad = [
|
|
3256
|
+
f"{k} {'missing' if not v else 'is placeholder'}"
|
|
3257
|
+
for k, v in vals.items()
|
|
3258
|
+
if not v or placeholders.get(k) == v
|
|
3259
|
+
]
|
|
3260
|
+
if bad:
|
|
3261
|
+
print("* Error:", "; ".join(bad))
|
|
3092
3262
|
sys.exit(1)
|
|
3093
3263
|
|
|
3094
3264
|
clienttoken_request_body_file_param = False
|
|
@@ -3127,7 +3297,7 @@ def main():
|
|
|
3127
3297
|
try:
|
|
3128
3298
|
APP_VERSION = ua_to_app_version(USER_AGENT)
|
|
3129
3299
|
except Exception as e:
|
|
3130
|
-
print(f"Warning: wrong USER_AGENT defined, reverting to the default one: {e}")
|
|
3300
|
+
print(f"Warning: wrong USER_AGENT defined, reverting to the default one for APP_VERSION: {e}")
|
|
3131
3301
|
APP_VERSION = app_version_default
|
|
3132
3302
|
else:
|
|
3133
3303
|
APP_VERSION = app_version_default
|
|
@@ -3285,6 +3455,7 @@ def main():
|
|
|
3285
3455
|
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
3456
|
print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
|
|
3287
3457
|
print(f"* Track listened songs:\t\t{TRACK_SONGS}")
|
|
3458
|
+
# print(f"* User agent:\t\t\t{USER_AGENT}")
|
|
3288
3459
|
print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
|
|
3289
3460
|
print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
|
|
3290
3461
|
print(f"* Alert on monitored tracks:\t{bool(MONITOR_LIST_FILE)}" + (f" ({MONITOR_LIST_FILE})" if MONITOR_LIST_FILE else ""))
|