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.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.2
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.1.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 - 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)
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
- # Use your web browser's dev console or "Cookie-Editor" by cgagnier to extract it easily: https://cookie-editor.com/
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
- # To extract device_id, system_id, user_uri_id and refresh_token from binary login request Protobuf file:
226
- #
227
- # - run an intercepting proxy of your choice (like Proxyman)
228
- # - launch the Spotify desktop client and look for requests to: https://login{n}.spotify.com/v3/login
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
- # Can also be set using the -w flag
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 configuration options below manually
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
- # For the binary login request Protobuf body, if your proxy supports Protobuf decoding, extract:
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
- # Alternatively, you can use protoc tool (part of Protobuf pip package) to decode the binary:
246
- # protoc --decode_raw < <path-to-login-request-body-file>
264
+ # The Protobuf structure is as follows:
247
265
  #
248
- # If you decided to set the configuration options manually, it is recommended to provide the REFRESH_TOKEN
249
- # secret using one of the following methods:
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
- # Default values below are typically fine - only modify if necessary
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
- # Optional: specify user agent manually, some examples:
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
- # Spotify/126200580 Win32_x86_64/0 (PC desktop)
270
- # Spotify/126400408 OSX_ARM64/OS X 15.5.0 [arm 2]
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
- # Leave empty to auto-generate it randomly
273
- USER_AGENT = ""
274
-
275
- # Optional: specify app version manually (e.g. '1.2.62.580.g7e3d9a4f')
276
- # Leave empty to auto-generate from USER_AGENT
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
- # Platform-specific values for token generation, typically leave unchanged
280
- # You can also extract these from a captured client token request Protobuf file (see below)
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
- # Optional: to extract app_version, cpu_arch, os_build, platform, os_major, os_minor and client_model from
289
- # binary client token request Protobuf file:
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': get_random_user_agent() if TOKEN_SOURCE == 'cookie' else get_random_spotify_user_agent()}, timeout=timeout, verify=verify)
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 = parse_request_body_file(LOGIN_REQUEST_BODY_FILE)
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
- # Retrieves a new Spotify access token using the sp_dc cookie, tries first with mode "transport" and if needed with "init"
1135
- def refresh_token(sp_dc: str) -> dict:
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
- ua = get_random_user_agent()
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": ua,
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", ""), ua)):
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("refresh_token(): Unsuccessful token request")
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, SP_CACHED_USER_AGENT
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, SP_CACHED_USER_AGENT):
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 = refresh_token(sp_dc)
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, SP_CACHED_USER_AGENT):
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
- 1 {
1323
- 1: "device_id"
1324
- 2: "system_id"
1325
- }
1326
- 100 {
1327
- 1: "user_uri_id"
1328
- 2: "refresh_token"
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 parse_request_body_file(file_path):
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: device_details
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 = {"Authorization": f"Bearer {access_token}"}
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 search URL:\t\t{apple_search_url}")
1837
- print(f"YouTube Music search URL:\t{youtube_music_search_url}")
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 = {"Authorization": f"Bearer {access_token}"}
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 = {"Authorization": f"Bearer {access_token}"}
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 = {"Authorization": f"Bearer {access_token}"}
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
- # Main function that monitors activity of the specified Spotify friend's user URI ID
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', 'unsuccessful token request']
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 token or refresh token might have expired!")
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 token or refresh token might have expired! (uri: {user_uri_id})"
2142
- m_body = f"Client token or refresh token might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2143
- m_body_html = f"<html><head></head><body>Client token or refresh token might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
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 might have expired!")
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 might have expired! (uri: {user_uri_id})"
2152
- m_body = f"sp_dc might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2153
- m_body_html = f"<html><head></head><body>sp_dc might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
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 search URL:\t\t{apple_search_url}")
2263
- print(f"YouTube Music search URL:\t{youtube_music_search_url}")
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
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
- # Main loop
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', 'unsuccessful token request']
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 token or refresh token might have expired!")
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 token or refresh token might have expired! (uri: {user_uri_id})"
2389
- m_body = f"Client token or refresh token might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2390
- m_body_html = f"<html><head></head><body>Client token or refresh token might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
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 might have expired!")
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 might have expired! (uri: {user_uri_id})"
2399
- m_body = f"sp_dc might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2400
- m_body_html = f"<html><head></head><body>sp_dc might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
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
- print(f"Spotify user {user_uri_id} ({sp_username}) disappeared - make sure your friend is followed and has activity sharing enabled. Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
2412
- if ERROR_NOTIFICATION:
2413
- m_subject = f"Spotify user {user_uri_id} ({sp_username}) disappeared!"
2414
- m_body = f"Spotify user {user_uri_id} ({sp_username}) 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: ')}"
2415
- m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) 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>"
2416
- print(f"Sending email notification to {RECEIVER_EMAIL}")
2417
- send_email(m_subject, m_body, m_body_html, SMTP_SSL)
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}) appeared again!")
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}) appeared!"
2428
- m_body = f"Spotify user {user_uri_id} ({sp_username}) appeared again!{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2429
- m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) appeared again!{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
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 search URL:\t\t{apple_search_url}")
2562
- print(f"YouTube Music search URL:\t{youtube_music_search_url}")
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
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
- 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)}.")
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
- # Cookie token source credentials (used when token source is set to cookie)
2805
- api_creds = parser.add_argument_group("Cookie token source credentials")
2806
- api_creds.add_argument(
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
- # Client token source credentials (used when token source is set to client)
2815
- client_creds = parser.add_argument_group("Client token source credentials")
2816
- client_creds.add_argument(
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
- client_creds.add_argument(
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 pip3 install python-dotenv\n\nOnce installed, re-run this tool\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 pip3 install pyotp\n\nOnce installed, re-run this tool")
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 = parse_request_body_file(LOGIN_REQUEST_BODY_FILE)
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
- if not USER_AGENT:
3077
- USER_AGENT = get_random_spotify_user_agent()
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
- if any([
3080
- not LOGIN_URL,
3081
- not USER_AGENT,
3082
- not DEVICE_ID,
3083
- DEVICE_ID == "your_spotify_app_device_id",
3084
- not SYSTEM_ID,
3085
- SYSTEM_ID == "your_spotify_app_system_id",
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 ""))