spotify-profile-monitor 2.5.3__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.3
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
@@ -246,14 +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
- **NOTE**: Spotify appears to have changed something in client versions released after June 2025 (likely a switch to HTTP/3 and/or certificate pinning). You may need to use an older version of the Spotify desktop client for this method to work.
249
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
250
250
 
251
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
251
+ - Enable SSL traffic decryption for `spotify.com` domain
252
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
252
253
 
253
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
254
- - 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`
255
255
 
256
- - 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
257
266
 
258
267
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
259
268
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -514,6 +523,19 @@ spotify_profile_monitor <spotify_user_uri_id> -k
514
523
 
515
524
  It is helpful in the case of playlists created by another user added to another user profile.
516
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
+
517
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):
518
540
  - set `DETECT_CHANGES_IN_PLAYLISTS` to `False`
519
541
  - or use the `-q` flag
@@ -218,14 +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
- **NOTE**: Spotify appears to have changed something in client versions released after June 2025 (likely a switch to HTTP/3 and/or certificate pinning). You may need to use an older version of the Spotify desktop client for this method to work.
221
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
222
222
 
223
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
223
+ - Enable SSL traffic decryption for `spotify.com` domain
224
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
224
225
 
225
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
226
- - 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`
227
227
 
228
- - 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
229
238
 
230
239
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
231
240
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -486,6 +495,19 @@ spotify_profile_monitor <spotify_user_uri_id> -k
486
495
 
487
496
  It is helpful in the case of playlists created by another user added to another user profile.
488
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
+
489
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):
490
512
  - set `DETECT_CHANGES_IN_PLAYLISTS` to `False`
491
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.3"
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.3
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
@@ -246,14 +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
- **NOTE**: Spotify appears to have changed something in client versions released after June 2025 (likely a switch to HTTP/3 and/or certificate pinning). You may need to use an older version of the Spotify desktop client for this method to work.
249
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
250
250
 
251
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
251
+ - Enable SSL traffic decryption for `spotify.com` domain
252
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
252
253
 
253
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
254
- - 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`
255
255
 
256
- - 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
257
266
 
258
267
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
259
268
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -514,6 +523,19 @@ spotify_profile_monitor <spotify_user_uri_id> -k
514
523
 
515
524
  It is helpful in the case of playlists created by another user added to another user profile.
516
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
+
517
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):
518
540
  - set `DETECT_CHANGES_IN_PLAYLISTS` to `False`
519
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.3
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.3"
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,7 +1426,11 @@ 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
@@ -1393,12 +1438,14 @@ def generate_totp():
1393
1438
  import pyotp
1394
1439
 
1395
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],
1396
1443
  "8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
1397
1444
  "7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
1398
1445
  "6": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
1399
- "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54]
1446
+ "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54],
1400
1447
  }
1401
- secret_cipher_bytes = secret_cipher_dict["8"]
1448
+ secret_cipher_bytes = secret_cipher_dict[str(TOTP_VER)]
1402
1449
 
1403
1450
  transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
1404
1451
  joined = "".join(str(num) for num in transformed)
@@ -1413,7 +1460,6 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1413
1460
  transport = True
1414
1461
  init = True
1415
1462
  session = req.Session()
1416
- session.cookies.set("sp_dc", sp_dc)
1417
1463
  data: dict = {}
1418
1464
  token = ""
1419
1465
 
@@ -1427,13 +1473,17 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1427
1473
  "productType": "web-player",
1428
1474
  "totp": otp_value,
1429
1475
  "totpServer": otp_value,
1430
- "totpVer": 8,
1431
- "sTime": server_time,
1432
- "cTime": client_time,
1433
- "buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
1434
- "buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
1476
+ "totpVer": TOTP_VER,
1435
1477
  }
1436
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
+
1437
1487
  headers = {
1438
1488
  "User-Agent": USER_AGENT,
1439
1489
  "Accept": "application/json",
@@ -3828,6 +3878,10 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
3828
3878
  playlists_count = sp_user_data["sp_user_public_playlists_count"]
3829
3879
  playlists = sp_user_data["sp_user_public_playlists_uris"]
3830
3880
 
3881
+ if ADD_PLAYLISTS_TO_MONITOR:
3882
+ playlists.extend(ADD_PLAYLISTS_TO_MONITOR)
3883
+ playlists_count += len(ADD_PLAYLISTS_TO_MONITOR)
3884
+
3831
3885
  recently_played_artists = sp_user_data["sp_user_recently_played_artists"]
3832
3886
 
3833
3887
  print(f"Username:\t\t\t{username}")
@@ -4220,6 +4274,10 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
4220
4274
  playlists_count = sp_user_data["sp_user_public_playlists_count"]
4221
4275
  playlists = sp_user_data["sp_user_public_playlists_uris"]
4222
4276
 
4277
+ if ADD_PLAYLISTS_TO_MONITOR:
4278
+ playlists.extend(ADD_PLAYLISTS_TO_MONITOR)
4279
+ playlists_count += len(ADD_PLAYLISTS_TO_MONITOR)
4280
+
4223
4281
  recently_played_artists = sp_user_data["sp_user_recently_played_artists"]
4224
4282
 
4225
4283
  if followers_count != followers_old_count:
@@ -4745,7 +4803,7 @@ def spotify_profile_monitor_uri(user_uri_id, csv_file_name, playlists_to_skip):
4745
4803
 
4746
4804
 
4747
4805
  def main():
4748
- 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
4749
4807
 
4750
4808
  if "--generate-config" in sys.argv:
4751
4809
  print(CONFIG_BLOCK.strip("\n"))
@@ -5010,6 +5068,13 @@ def main():
5010
5068
  default=None,
5011
5069
  help="Disable logging to spotify_profile_monitor_<user_uri_id/file_suffix>.log"
5012
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
+ )
5013
5078
 
5014
5079
  args = parser.parse_args()
5015
5080
 
@@ -5522,6 +5587,18 @@ def main():
5522
5587
  if not FILE_SUFFIX:
5523
5588
  FILE_SUFFIX = str(args.user_id)
5524
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
+
5525
5602
  if args.disable_logging is True:
5526
5603
  DISABLE_LOGGING = True
5527
5604
 
@@ -5557,7 +5634,7 @@ def main():
5557
5634
  ERROR_NOTIFICATION = False
5558
5635
 
5559
5636
  print(f"* Spotify polling intervals:\t[check: {display_time(SPOTIFY_CHECK_INTERVAL)}] [error: {display_time(SPOTIFY_ERROR_INTERVAL)}]")
5560
- 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}]")
5561
5638
  print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
5562
5639
  print(f"* Profile pic changes:\t\t{DETECT_CHANGED_PROFILE_PIC}")
5563
5640
  print(f"* Playlist changes:\t\t{DETECT_CHANGES_IN_PLAYLISTS}")
@@ -5569,6 +5646,8 @@ def main():
5569
5646
  print(f"* Ignore listed playlists:\t{bool(PLAYLISTS_TO_SKIP_FILE)}" + (f" ({PLAYLISTS_TO_SKIP_FILE})" if PLAYLISTS_TO_SKIP_FILE else ""))
5570
5647
  print(f"* Display profile pics:\t\t{bool(imgcat_exe)}" + (f" (via {imgcat_exe})" if imgcat_exe else ""))
5571
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")
5572
5651
  if TOKEN_SOURCE in ('oauth_user', 'oauth_app'):
5573
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)')}")
5574
5653
  print(f"* Configuration file:\t\t{cfg_path}")