spotify-profile-monitor 2.5.2__tar.gz → 2.6__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spotify_profile_monitor
3
- Version: 2.5.2
3
+ Version: 2.6
4
4
  Summary: Tool implementing real-time tracking of Spotify users activities and profile changes including playlists
5
5
  Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
6
6
  License-Expression: GPL-3.0-or-later
@@ -28,7 +28,7 @@ Dynamic: license-file
28
28
 
29
29
  # spotify_profile_monitor
30
30
 
31
- spotify_profile_monitor is an OSINT tool for real-time monitoring of Spotify users' activities and profile changes including playlists.
31
+ OSINT tool for real-time monitoring of Spotify users' activities and profile changes including playlists.
32
32
 
33
33
  NOTE: If you want to track Spotify friends' music activity, check out another tool I developed: [spotify_monitor](https://github.com/misiektoja/spotify_monitor).
34
34
 
@@ -246,12 +246,23 @@ If you store the `SP_DC_COOKIE` in a dotenv file you can update its value and se
246
246
 
247
247
  This is the alternative method used to obtain a Spotify access token which simulates a login from the real Spotify desktop app using credentials intercepted from a real session.
248
248
 
249
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
249
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
250
+
251
+ - Enable SSL traffic decryption for `spotify.com` domain
252
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
250
253
 
251
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
252
- - Note: The `login` part is suffixed with one or more digits (e.g. `login5`).
254
+ - Launch the Spotify desktop client, then switch to your intercepting proxy (like Proxyman) and look for POST requests to `https://login5.spotify.com/v3/login`
253
255
 
254
- - If you don't see this request, log out from the Spotify desktop client and log back in.
256
+ - If you don't see this request, try following steps (stop once it works):
257
+ - restart the Spotify desktop client
258
+ - log out from the Spotify desktop client and log back in
259
+ - point Spotify at the intercepting proxy directly in its settings, i.e. in **Spotify → Settings → Proxy Settings**, set:
260
+ - **proxy type**: `HTTP`
261
+ - **host**: `127.0.0.1` (IP/FQDN of your proxy, for Proxyman use the IP you see at the top bar)
262
+ - **port**: `9090` (port of your proxy, for Proxyman use the port you see at the top bar)
263
+ - restart the app; since QUIC (HTTP/3) requires raw UDP and can't tunnel over HTTP CONNECT, Spotify will downgrade to TCP-only HTTP/2 or 1.1, which intercepting proxy can decrypt
264
+ - block Spotify's UDP port 443 at the OS level with a firewall of your choice - this prevents QUIC (HTTP/3), forcing TLS over TCP and letting intercepting proxy perform MITM
265
+ - try an older version of the Spotify desktop client
255
266
 
256
267
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
257
268
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -512,6 +523,19 @@ spotify_profile_monitor <spotify_user_uri_id> -k
512
523
 
513
524
  It is helpful in the case of playlists created by another user added to another user profile.
514
525
 
526
+ Some users don't list all their public playlists on their profile, but if you know a playlist's URI, you can still monitor it.
527
+
528
+ To do so, add entries to the `ADD_PLAYLISTS_TO_MONITOR` configuration option. Example:
529
+
530
+ ```python
531
+ ADD_PLAYLISTS_TO_MONITOR = [
532
+ {'uri': 'spotify:playlist:{playlist_id1}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'},
533
+ {'uri': 'spotify:playlist:{playlist_id2}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'}
534
+ ]
535
+ ```
536
+
537
+ Replace `{playlist_id1}` and `{playlist_id2}` with the playlists URI IDs you want to monitor and `{user_id}` with the owner's URI ID (`spotify_user_uri_id`).
538
+
515
539
  If you want to completely disable detection of changes in user's public playlists (like added/removed tracks in playlists, playlists name and description changes, number of likes for playlists):
516
540
  - set `DETECT_CHANGES_IN_PLAYLISTS` to `False`
517
541
  - or use the `-q` flag
@@ -1,6 +1,6 @@
1
1
  # spotify_profile_monitor
2
2
 
3
- spotify_profile_monitor is an OSINT tool for real-time monitoring of Spotify users' activities and profile changes including playlists.
3
+ OSINT tool for real-time monitoring of Spotify users' activities and profile changes including playlists.
4
4
 
5
5
  NOTE: If you want to track Spotify friends' music activity, check out another tool I developed: [spotify_monitor](https://github.com/misiektoja/spotify_monitor).
6
6
 
@@ -218,12 +218,23 @@ If you store the `SP_DC_COOKIE` in a dotenv file you can update its value and se
218
218
 
219
219
  This is the alternative method used to obtain a Spotify access token which simulates a login from the real Spotify desktop app using credentials intercepted from a real session.
220
220
 
221
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
221
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
222
+
223
+ - Enable SSL traffic decryption for `spotify.com` domain
224
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
222
225
 
223
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
224
- - Note: The `login` part is suffixed with one or more digits (e.g. `login5`).
226
+ - Launch the Spotify desktop client, then switch to your intercepting proxy (like Proxyman) and look for POST requests to `https://login5.spotify.com/v3/login`
225
227
 
226
- - If you don't see this request, log out from the Spotify desktop client and log back in.
228
+ - If you don't see this request, try following steps (stop once it works):
229
+ - restart the Spotify desktop client
230
+ - log out from the Spotify desktop client and log back in
231
+ - point Spotify at the intercepting proxy directly in its settings, i.e. in **Spotify → Settings → Proxy Settings**, set:
232
+ - **proxy type**: `HTTP`
233
+ - **host**: `127.0.0.1` (IP/FQDN of your proxy, for Proxyman use the IP you see at the top bar)
234
+ - **port**: `9090` (port of your proxy, for Proxyman use the port you see at the top bar)
235
+ - restart the app; since QUIC (HTTP/3) requires raw UDP and can't tunnel over HTTP CONNECT, Spotify will downgrade to TCP-only HTTP/2 or 1.1, which intercepting proxy can decrypt
236
+ - block Spotify's UDP port 443 at the OS level with a firewall of your choice - this prevents QUIC (HTTP/3), forcing TLS over TCP and letting intercepting proxy perform MITM
237
+ - try an older version of the Spotify desktop client
227
238
 
228
239
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
229
240
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -484,6 +495,19 @@ spotify_profile_monitor <spotify_user_uri_id> -k
484
495
 
485
496
  It is helpful in the case of playlists created by another user added to another user profile.
486
497
 
498
+ Some users don't list all their public playlists on their profile, but if you know a playlist's URI, you can still monitor it.
499
+
500
+ To do so, add entries to the `ADD_PLAYLISTS_TO_MONITOR` configuration option. Example:
501
+
502
+ ```python
503
+ ADD_PLAYLISTS_TO_MONITOR = [
504
+ {'uri': 'spotify:playlist:{playlist_id1}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'},
505
+ {'uri': 'spotify:playlist:{playlist_id2}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'}
506
+ ]
507
+ ```
508
+
509
+ Replace `{playlist_id1}` and `{playlist_id2}` with the playlists URI IDs you want to monitor and `{user_id}` with the owner's URI ID (`spotify_user_uri_id`).
510
+
487
511
  If you want to completely disable detection of changes in user's public playlists (like added/removed tracks in playlists, playlists name and description changes, number of likes for playlists):
488
512
  - set `DETECT_CHANGES_IN_PLAYLISTS` to `False`
489
513
  - or use the `-q` flag
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spotify_profile_monitor"
7
- version = "2.5.2"
7
+ version = "2.6"
8
8
  description = "Tool implementing real-time tracking of Spotify users activities and profile changes including playlists"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spotify_profile_monitor
3
- Version: 2.5.2
3
+ Version: 2.6
4
4
  Summary: Tool implementing real-time tracking of Spotify users activities and profile changes including playlists
5
5
  Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
6
6
  License-Expression: GPL-3.0-or-later
@@ -28,7 +28,7 @@ Dynamic: license-file
28
28
 
29
29
  # spotify_profile_monitor
30
30
 
31
- spotify_profile_monitor is an OSINT tool for real-time monitoring of Spotify users' activities and profile changes including playlists.
31
+ OSINT tool for real-time monitoring of Spotify users' activities and profile changes including playlists.
32
32
 
33
33
  NOTE: If you want to track Spotify friends' music activity, check out another tool I developed: [spotify_monitor](https://github.com/misiektoja/spotify_monitor).
34
34
 
@@ -246,12 +246,23 @@ If you store the `SP_DC_COOKIE` in a dotenv file you can update its value and se
246
246
 
247
247
  This is the alternative method used to obtain a Spotify access token which simulates a login from the real Spotify desktop app using credentials intercepted from a real session.
248
248
 
249
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
249
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
250
+
251
+ - Enable SSL traffic decryption for `spotify.com` domain
252
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
250
253
 
251
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
252
- - Note: The `login` part is suffixed with one or more digits (e.g. `login5`).
254
+ - Launch the Spotify desktop client, then switch to your intercepting proxy (like Proxyman) and look for POST requests to `https://login5.spotify.com/v3/login`
253
255
 
254
- - If you don't see this request, log out from the Spotify desktop client and log back in.
256
+ - If you don't see this request, try following steps (stop once it works):
257
+ - restart the Spotify desktop client
258
+ - log out from the Spotify desktop client and log back in
259
+ - point Spotify at the intercepting proxy directly in its settings, i.e. in **Spotify → Settings → Proxy Settings**, set:
260
+ - **proxy type**: `HTTP`
261
+ - **host**: `127.0.0.1` (IP/FQDN of your proxy, for Proxyman use the IP you see at the top bar)
262
+ - **port**: `9090` (port of your proxy, for Proxyman use the port you see at the top bar)
263
+ - restart the app; since QUIC (HTTP/3) requires raw UDP and can't tunnel over HTTP CONNECT, Spotify will downgrade to TCP-only HTTP/2 or 1.1, which intercepting proxy can decrypt
264
+ - block Spotify's UDP port 443 at the OS level with a firewall of your choice - this prevents QUIC (HTTP/3), forcing TLS over TCP and letting intercepting proxy perform MITM
265
+ - try an older version of the Spotify desktop client
255
266
 
256
267
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
257
268
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -512,6 +523,19 @@ spotify_profile_monitor <spotify_user_uri_id> -k
512
523
 
513
524
  It is helpful in the case of playlists created by another user added to another user profile.
514
525
 
526
+ Some users don't list all their public playlists on their profile, but if you know a playlist's URI, you can still monitor it.
527
+
528
+ To do so, add entries to the `ADD_PLAYLISTS_TO_MONITOR` configuration option. Example:
529
+
530
+ ```python
531
+ ADD_PLAYLISTS_TO_MONITOR = [
532
+ {'uri': 'spotify:playlist:{playlist_id1}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'},
533
+ {'uri': 'spotify:playlist:{playlist_id2}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'}
534
+ ]
535
+ ```
536
+
537
+ Replace `{playlist_id1}` and `{playlist_id2}` with the playlists URI IDs you want to monitor and `{user_id}` with the owner's URI ID (`spotify_user_uri_id`).
538
+
515
539
  If you want to completely disable detection of changes in user's public playlists (like added/removed tracks in playlists, playlists name and description changes, number of likes for playlists):
516
540
  - set `DETECT_CHANGES_IN_PLAYLISTS` to `False`
517
541
  - or use the `-q` flag
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Author: Michal Szymanski <misiektoja-github@rm-rf.ninja>
4
- v2.5.2
4
+ v2.6
5
5
 
6
6
  OSINT tool implementing real-time tracking of Spotify users activities and profile changes including playlists:
7
7
  https://github.com/misiektoja/spotify_profile_monitor/
@@ -18,7 +18,7 @@ python-dotenv (optional)
18
18
  spotipy (optional, needed when the token source is set to oauth_app)
19
19
  """
20
20
 
21
- VERSION = "2.5.2"
21
+ VERSION = "2.6"
22
22
 
23
23
  # ---------------------------
24
24
  # CONFIGURATION SECTION START
@@ -137,6 +137,17 @@ DETECT_CHANGES_IN_PLAYLISTS = True
137
137
  # Can also be enabled via the -k flag
138
138
  GET_ALL_PLAYLISTS = False
139
139
 
140
+ # Some users don't list all their public playlists on their profile, but if you know a playlist's URI, you can still monitor it
141
+ #
142
+ # Example:
143
+ #
144
+ # ADD_PLAYLISTS_TO_MONITOR = [
145
+ # {'uri': 'spotify:playlist:{playlist_id1}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'},
146
+ # {'uri': 'spotify:playlist:{playlist_id2}', 'owner_name': '{user_id}', 'owner_uri': 'spotify:user:{user_id}'}
147
+ # ]
148
+ # Replace {playlist_id1} and {playlist_id2} with the playlists URI IDs you want to monitor and {user_id} with the owner's URI ID
149
+ ADD_PLAYLISTS_TO_MONITOR = []
150
+
140
151
  # Ignore Spotify-owned playlists when monitoring?
141
152
  # Set to True to avoid tracking Spotify-generated playlists that often change frequently (likes, tracks etc.)
142
153
  IGNORE_SPOTIFY_PLAYLISTS = True
@@ -225,6 +236,13 @@ HORIZONTAL_LINE = 113
225
236
  # Whether to clear the terminal screen after starting the tool
226
237
  CLEAR_SCREEN = True
227
238
 
239
+ # Max characters per line when printing to screen to avoid line wrapping
240
+ # Does not affect log file output
241
+ # Set to 999 to auto-detect terminal width
242
+ # Applies only when DISABLE_LOGGING is False
243
+ # Can also be set via the --truncate flag
244
+ TRUNCATE_CHARS = 0
245
+
228
246
  # Value used by signal handlers to increase or decrease profile check interval (SPOTIFY_CHECK_INTERVAL); in seconds
229
247
  SPOTIFY_CHECK_SIGNAL_VALUE = 300 # 5 minutes
230
248
 
@@ -478,6 +496,7 @@ IMGCAT_PATH = ""
478
496
  SP_SHA256 = ""
479
497
  DETECT_CHANGES_IN_PLAYLISTS = False
480
498
  GET_ALL_PLAYLISTS = False
499
+ ADD_PLAYLISTS_TO_MONITOR = []
481
500
  IGNORE_SPOTIFY_PLAYLISTS = False
482
501
  PLAYLISTS_LIMIT = 0
483
502
  RECENTLY_PLAYED_ARTISTS_LIMIT = 0
@@ -502,6 +521,7 @@ CLEAR_SCREEN = False
502
521
  SPOTIFY_CHECK_SIGNAL_VALUE = 0
503
522
  TOKEN_MAX_RETRIES = 0
504
523
  TOKEN_RETRY_TIMEOUT = 0.0
524
+ TRUNCATE_CHARS = 0
505
525
 
506
526
  exec(CONFIG_BLOCK, globals())
507
527
 
@@ -531,6 +551,12 @@ SP_CACHED_CLIENT_ID = ""
531
551
  # URL of the Spotify Web Player endpoint to get access token
532
552
  TOKEN_URL = "https://open.spotify.com/api/token"
533
553
 
554
+ # URL of the endpoint to get server time needed to create TOTP object
555
+ SERVER_TIME_URL = "https://open.spotify.com/"
556
+
557
+ # Identifier used to select the appropriate encrypted secret from secret_cipher_dict when generating a TOTP token
558
+ TOTP_VER = 10
559
+
534
560
  # Variables for caching functionality of the Spotify client token to avoid unnecessary refreshing
535
561
  SP_CACHED_CLIENT_TOKEN = None
536
562
  SP_CLIENT_TOKEN_EXPIRES_AT = 0
@@ -634,6 +660,19 @@ SESSION.mount("https://", adapter)
634
660
  SESSION.mount("http://", adapter)
635
661
 
636
662
 
663
+ # Truncates each line of a string to a specified number of characters including tab expansion and multi-line support
664
+ def truncate_string_per_line(message, truncate_chars, tabsize=8):
665
+ lines = message.split('\n')
666
+ truncated_lines = []
667
+
668
+ for line in lines:
669
+ expanded_line = line.expandtabs(tabsize=tabsize)
670
+ truncated_line = expanded_line[:truncate_chars]
671
+ truncated_lines.append(truncated_line)
672
+
673
+ return '\n'.join(truncated_lines)
674
+
675
+
637
676
  # Logger class to output messages to stdout and log file
638
677
  class Logger(object):
639
678
  def __init__(self, filename):
@@ -641,8 +680,10 @@ class Logger(object):
641
680
  self.logfile = open(filename, "a", buffering=1, encoding="utf-8")
642
681
 
643
682
  def write(self, message):
644
- self.terminal.write(message)
645
683
  self.logfile.write(message)
684
+ if (TRUNCATE_CHARS):
685
+ message = truncate_string_per_line(message, TRUNCATE_CHARS)
686
+ self.terminal.write(message)
646
687
  self.terminal.flush()
647
688
  self.logfile.flush()
648
689
 
@@ -1375,7 +1416,7 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
1375
1416
  if platform.system() != 'Windows':
1376
1417
  signal.signal(signal.SIGALRM, timeout_handler)
1377
1418
  signal.alarm(FUNCTION_TIMEOUT + 2)
1378
- response = session.head("https://open.spotify.com/", headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1419
+ response = session.head(SERVER_TIME_URL, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1379
1420
  response.raise_for_status()
1380
1421
  except TimeoutException as e:
1381
1422
  raise Exception(f"fetch_server_time() head network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
@@ -1385,17 +1426,26 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
1385
1426
  if platform.system() != 'Windows':
1386
1427
  signal.alarm(0)
1387
1428
 
1388
- return int(parsedate_to_datetime(response.headers["Date"]).timestamp())
1429
+ date_hdr = response.headers.get("Date")
1430
+ if not date_hdr:
1431
+ raise Exception("fetch_server_time() missing 'Date' header")
1432
+
1433
+ return int(parsedate_to_datetime(date_hdr).timestamp())
1389
1434
 
1390
1435
 
1391
1436
  # Creates a TOTP object using a secret derived from transformed cipher bytes
1392
1437
  def generate_totp():
1393
1438
  import pyotp
1394
1439
 
1395
- secret_cipher_bytes = [
1396
- 12, 56, 76, 33, 88, 44, 88, 33,
1397
- 78, 78, 11, 66, 22, 22, 55, 69, 54,
1398
- ]
1440
+ secret_cipher_dict = {
1441
+ "10": [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75],
1442
+ "9": [109, 101, 90, 99, 66, 92, 116, 108, 85, 70, 86, 49, 68, 54, 87, 50, 72, 121, 52, 64, 57, 43, 36, 81, 97, 72, 53, 41, 78, 56],
1443
+ "8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
1444
+ "7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
1445
+ "6": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
1446
+ "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54],
1447
+ }
1448
+ secret_cipher_bytes = secret_cipher_dict[str(TOTP_VER)]
1399
1449
 
1400
1450
  transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
1401
1451
  joined = "".join(str(num) for num in transformed)
@@ -1410,7 +1460,6 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1410
1460
  transport = True
1411
1461
  init = True
1412
1462
  session = req.Session()
1413
- session.cookies.set("sp_dc", sp_dc)
1414
1463
  data: dict = {}
1415
1464
  token = ""
1416
1465
 
@@ -1424,13 +1473,17 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1424
1473
  "productType": "web-player",
1425
1474
  "totp": otp_value,
1426
1475
  "totpServer": otp_value,
1427
- "totpVer": 5,
1428
- "sTime": server_time,
1429
- "cTime": client_time,
1430
- "buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
1431
- "buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
1476
+ "totpVer": TOTP_VER,
1432
1477
  }
1433
1478
 
1479
+ if TOTP_VER < 10:
1480
+ params.update({
1481
+ "sTime": server_time,
1482
+ "cTime": client_time,
1483
+ "buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
1484
+ "buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
1485
+ })
1486
+
1434
1487
  headers = {
1435
1488
  "User-Agent": USER_AGENT,
1436
1489
  "Accept": "application/json",
@@ -3775,14 +3828,14 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
3775
3828
  SP_CACHED_ACCESS_TOKEN = None
3776
3829
 
3777
3830
  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']
3778
- cookie_errs = ['access token', 'unauthorized']
3831
+ cookie_errs = ['access token', 'unauthorized', 'unsuccessful token request']
3779
3832
  oauth_app_errs = ['invalid_client', 'invalid_client_id', 'could not authenticate you', '401']
3780
3833
  oauth_user_errs = ['invalid_client', 'invalid_grant', 'invalid_scope', 'authorization_required', 'refresh token has been revoked', 'refresh token has expired']
3781
3834
 
3782
3835
  if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
3783
3836
  print(f"* Error: client or refresh token may be invalid or expired!\n{str(e)}")
3784
3837
  elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
3785
- print(f"* Error: sp_dc may be invalid or expired!\n{str(e)}")
3838
+ print(f"* Error: sp_dc may be invalid/expired or Spotify has broken sth again!\n{str(e)}")
3786
3839
  elif TOKEN_SOURCE == 'oauth_app' and any(k in err for k in oauth_app_errs):
3787
3840
  print(f"* Error: OAuth-app client_id/client_secret may be invalid or expired!\n{str(e)}")
3788
3841
  elif TOKEN_SOURCE == 'oauth_user' and any(k in err for k in oauth_user_errs):
@@ -3825,6 +3878,10 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
3825
3878
  playlists_count = sp_user_data["sp_user_public_playlists_count"]
3826
3879
  playlists = sp_user_data["sp_user_public_playlists_uris"]
3827
3880
 
3881
+ if ADD_PLAYLISTS_TO_MONITOR:
3882
+ playlists.extend(ADD_PLAYLISTS_TO_MONITOR)
3883
+ playlists_count += len(ADD_PLAYLISTS_TO_MONITOR)
3884
+
3828
3885
  recently_played_artists = sp_user_data["sp_user_recently_played_artists"]
3829
3886
 
3830
3887
  print(f"Username:\t\t\t{username}")
@@ -4105,7 +4162,7 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
4105
4162
  SP_CACHED_ACCESS_TOKEN = None
4106
4163
 
4107
4164
  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']
4108
- cookie_errs = ['access token', 'unauthorized']
4165
+ cookie_errs = ['access token', 'unauthorized', 'unsuccessful token request']
4109
4166
  oauth_app_errs = ['invalid_client', 'invalid_client_id', 'could not authenticate you', '401']
4110
4167
  oauth_user_errs = ['invalid_client', 'invalid_grant', 'invalid_scope', 'authorization_required', 'refresh token has been revoked', 'refresh token has expired']
4111
4168
 
@@ -4120,11 +4177,11 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
4120
4177
  email_sent = True
4121
4178
 
4122
4179
  elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
4123
- print(f"* Error: sp_dc may be invalid or expired!")
4180
+ print(f"* Error: sp_dc may be invalid/expired or Spotify has broken sth again!")
4124
4181
  if ERROR_NOTIFICATION and not email_sent:
4125
- m_subject = f"spotify_profile_monitor: sp_dc may be invalid or expired! (uri: {user_uri_id})"
4126
- m_body = f"sp_dc may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
4127
- 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>"
4182
+ m_subject = f"spotify_profile_monitor: sp_dc may be invalid/expired or Spotify has broken sth again! (uri: {user_uri_id})"
4183
+ 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: ')}"
4184
+ 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>"
4128
4185
  print(f"Sending email notification to {RECEIVER_EMAIL}")
4129
4186
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
4130
4187
  email_sent = True
@@ -4217,6 +4274,10 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
4217
4274
  playlists_count = sp_user_data["sp_user_public_playlists_count"]
4218
4275
  playlists = sp_user_data["sp_user_public_playlists_uris"]
4219
4276
 
4277
+ if ADD_PLAYLISTS_TO_MONITOR:
4278
+ playlists.extend(ADD_PLAYLISTS_TO_MONITOR)
4279
+ playlists_count += len(ADD_PLAYLISTS_TO_MONITOR)
4280
+
4220
4281
  recently_played_artists = sp_user_data["sp_user_recently_played_artists"]
4221
4282
 
4222
4283
  if followers_count != followers_old_count:
@@ -4742,7 +4803,7 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
4742
4803
 
4743
4804
 
4744
4805
  def main():
4745
- global CLI_CONFIG_PATH, DOTENV_FILE, LOCAL_TIMEZONE, LIVENESS_CHECK_COUNTER, SP_DC_COOKIE, SP_APP_CLIENT_ID, SP_APP_CLIENT_SECRET, SP_USER_CLIENT_ID, SP_USER_CLIENT_SECRET, LOGIN_REQUEST_BODY_FILE, CLIENTTOKEN_REQUEST_BODY_FILE, REFRESH_TOKEN, LOGIN_URL, USER_AGENT, DEVICE_ID, SYSTEM_ID, USER_URI_ID, CSV_FILE, PLAYLISTS_TO_SKIP_FILE, FILE_SUFFIX, DISABLE_LOGGING, SP_LOGFILE, PROFILE_NOTIFICATION, SPOTIFY_CHECK_INTERVAL, SPOTIFY_ERROR_INTERVAL, FOLLOWERS_FOLLOWINGS_NOTIFICATION, ERROR_NOTIFICATION, DETECT_CHANGED_PROFILE_PIC, DETECT_CHANGES_IN_PLAYLISTS, GET_ALL_PLAYLISTS, imgcat_exe, SMTP_PASSWORD, SP_SHA256, stdout_bck, APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL, TOKEN_SOURCE, ALARM_TIMEOUT, pyotp, CLEAN_OUTPUT, USER_AGENT, SP_APP_TOKENS_FILE, SP_USER_TOKENS_FILE
4806
+ global CLI_CONFIG_PATH, DOTENV_FILE, LOCAL_TIMEZONE, LIVENESS_CHECK_COUNTER, SP_DC_COOKIE, SP_APP_CLIENT_ID, SP_APP_CLIENT_SECRET, SP_USER_CLIENT_ID, SP_USER_CLIENT_SECRET, LOGIN_REQUEST_BODY_FILE, CLIENTTOKEN_REQUEST_BODY_FILE, REFRESH_TOKEN, LOGIN_URL, USER_AGENT, DEVICE_ID, SYSTEM_ID, USER_URI_ID, CSV_FILE, PLAYLISTS_TO_SKIP_FILE, FILE_SUFFIX, DISABLE_LOGGING, SP_LOGFILE, PROFILE_NOTIFICATION, SPOTIFY_CHECK_INTERVAL, SPOTIFY_ERROR_INTERVAL, FOLLOWERS_FOLLOWINGS_NOTIFICATION, ERROR_NOTIFICATION, DETECT_CHANGED_PROFILE_PIC, DETECT_CHANGES_IN_PLAYLISTS, GET_ALL_PLAYLISTS, imgcat_exe, SMTP_PASSWORD, SP_SHA256, stdout_bck, APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL, TOKEN_SOURCE, ALARM_TIMEOUT, pyotp, CLEAN_OUTPUT, USER_AGENT, SP_APP_TOKENS_FILE, SP_USER_TOKENS_FILE, TRUNCATE_CHARS
4746
4807
 
4747
4808
  if "--generate-config" in sys.argv:
4748
4809
  print(CONFIG_BLOCK.strip("\n"))
@@ -5007,6 +5068,13 @@ def main():
5007
5068
  default=None,
5008
5069
  help="Disable logging to spotify_profile_monitor_<user_uri_id/file_suffix>.log"
5009
5070
  )
5071
+ opts.add_argument(
5072
+ "--truncate",
5073
+ dest="truncate",
5074
+ metavar="N",
5075
+ type=int,
5076
+ help="Max characters per screen line (not log), use 999 to auto-detect terminal width, ignored if -d is set"
5077
+ )
5010
5078
 
5011
5079
  args = parser.parse_args()
5012
5080
 
@@ -5519,6 +5587,18 @@ def main():
5519
5587
  if not FILE_SUFFIX:
5520
5588
  FILE_SUFFIX = str(args.user_id)
5521
5589
 
5590
+ if args.truncate:
5591
+ if args.truncate != 999:
5592
+ TRUNCATE_CHARS = args.truncate
5593
+ else:
5594
+ try:
5595
+ terminal_size = shutil.get_terminal_size()
5596
+ print(f"The detected terminal screen width is: {terminal_size.columns} characters\n")
5597
+ TRUNCATE_CHARS = terminal_size.columns
5598
+ except Exception as e:
5599
+ print(f"Error: Cannot determine terminal screen width: {e}")
5600
+ sys.exit(1)
5601
+
5522
5602
  if args.disable_logging is True:
5523
5603
  DISABLE_LOGGING = True
5524
5604
 
@@ -5554,7 +5634,7 @@ def main():
5554
5634
  ERROR_NOTIFICATION = False
5555
5635
 
5556
5636
  print(f"* Spotify polling intervals:\t[check: {display_time(SPOTIFY_CHECK_INTERVAL)}] [error: {display_time(SPOTIFY_ERROR_INTERVAL)}]")
5557
- print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}] [followers/followings = {FOLLOWERS_FOLLOWINGS_NOTIFICATION}]\n\t\t\t\t[errors = {ERROR_NOTIFICATION}]")
5637
+ print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}] [followers/followings = {FOLLOWERS_FOLLOWINGS_NOTIFICATION}]\n*\t\t\t\t[errors = {ERROR_NOTIFICATION}]")
5558
5638
  print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
5559
5639
  print(f"* Profile pic changes:\t\t{DETECT_CHANGED_PROFILE_PIC}")
5560
5640
  print(f"* Playlist changes:\t\t{DETECT_CHANGES_IN_PLAYLISTS}")
@@ -5566,6 +5646,8 @@ def main():
5566
5646
  print(f"* Ignore listed playlists:\t{bool(PLAYLISTS_TO_SKIP_FILE)}" + (f" ({PLAYLISTS_TO_SKIP_FILE})" if PLAYLISTS_TO_SKIP_FILE else ""))
5567
5647
  print(f"* Display profile pics:\t\t{bool(imgcat_exe)}" + (f" (via {imgcat_exe})" if imgcat_exe else ""))
5568
5648
  print(f"* Output logging enabled:\t{not DISABLE_LOGGING}" + (f" ({FINAL_LOG_PATH})" if not DISABLE_LOGGING else ""))
5649
+ if not DISABLE_LOGGING and TRUNCATE_CHARS > 0:
5650
+ print(f"* Truncate terminal lines:\t{TRUNCATE_CHARS} chars")
5569
5651
  if TOKEN_SOURCE in ('oauth_user', 'oauth_app'):
5570
5652
  print(f"* Spotify token cache file:\t{({'oauth_app': SP_APP_TOKENS_FILE, 'oauth_user': SP_USER_TOKENS_FILE}.get(TOKEN_SOURCE) or 'None (memory only)')}")
5571
5653
  print(f"* Configuration file:\t\t{cfg_path}")