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.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.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.2"
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 - 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,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
- # 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
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
- # Can also be set using the -w flag
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 configuration options below manually
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
- # 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 protoc tool (part of protobuf pip package):
263
+ # pip install protobuf
264
+ # protoc --decode_raw < <path-to-login-request-body-file>
244
265
  #
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>
266
+ # - Use the built-in Protobuf decoder in your intercepting proxy (if supported)
247
267
  #
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:
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
- # Default values below are typically fine - only modify if necessary
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
- # Optional: specify user agent manually, some examples:
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
- # Spotify/126200580 Win32_x86_64/0 (PC desktop)
270
- # Spotify/126400408 OSX_ARM64/OS X 15.5.0 [arm 2]
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
- # 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 = ""
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
- # 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)
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
- # 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 = ""
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': get_random_user_agent() if TOKEN_SOURCE == 'cookie' else get_random_spotify_user_agent()}, timeout=timeout, verify=verify)
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 = parse_request_body_file(LOGIN_REQUEST_BODY_FILE)
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
- secret_cipher_bytes = [
1122
- 12, 56, 76, 33, 88, 44, 88, 33,
1123
- 78, 78, 11, 66, 22, 22, 55, 69, 54,
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
- # 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:
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
- ua = get_random_user_agent()
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": 5,
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": ua,
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", ""), ua)):
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("refresh_token(): Unsuccessful token request")
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, SP_CACHED_USER_AGENT
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, SP_CACHED_USER_AGENT):
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 = refresh_token(sp_dc)
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, SP_CACHED_USER_AGENT):
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
- 1 {
1323
- 1: "device_id"
1324
- 2: "system_id"
1325
- }
1326
- 100 {
1327
- 1: "user_uri_id"
1328
- 2: "refresh_token"
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 parse_request_body_file(file_path):
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: device_details
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 = {"Authorization": f"Bearer {access_token}"}
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 search URL:\t\t{apple_search_url}")
1837
- print(f"YouTube Music search URL:\t{youtube_music_search_url}")
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 = {"Authorization": f"Bearer {access_token}"}
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 = {"Authorization": f"Bearer {access_token}"}
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 = {"Authorization": f"Bearer {access_token}"}
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
- # Main function that monitors activity of the specified Spotify friend's user URI ID
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 token or refresh token might have expired!")
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 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>"
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 might have expired!")
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 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>"
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 search URL:\t\t{apple_search_url}")
2263
- print(f"YouTube Music search URL:\t{youtube_music_search_url}")
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 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>"
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
- # Main loop
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 token or refresh token might have expired!")
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 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>"
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 might have expired!")
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 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>"
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
- 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)
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}) appeared again!")
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}) 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>"
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 search URL:\t\t{apple_search_url}")
2562
- print(f"YouTube Music search URL:\t{youtube_music_search_url}")
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 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>"
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 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>"
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 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>"
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 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>"
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
- 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)}.")
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
- # 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(
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
- # 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(
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
- client_creds.add_argument(
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 pip3 install python-dotenv\n\nOnce installed, re-run this tool\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 pip3 install pyotp\n\nOnce installed, re-run this tool")
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 = parse_request_body_file(LOGIN_REQUEST_BODY_FILE)
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
- if not USER_AGENT:
3077
- USER_AGENT = get_random_spotify_user_agent()
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
- 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")
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 ""))