spotify-monitor 2.2.1__py3-none-any.whl → 2.3__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spotify_monitor
3
- Version: 2.2.1
3
+ Version: 2.3
4
4
  Summary: Tool implementing real-time tracking of Spotify friends music activity
5
5
  Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
6
6
  License-Expression: GPL-3.0-or-later
@@ -200,14 +200,23 @@ If you store the `SP_DC_COOKIE` in a dotenv file you can update its value and se
200
200
 
201
201
  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.
202
202
 
203
- **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.
203
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
204
204
 
205
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
205
+ - Enable SSL traffic decryption for `spotify.com` domain
206
+ - in Proxyman: click **Tools → SSL Proxying List → + button → Add Domain → paste `*.spotify.com` → Add**
206
207
 
207
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
208
- - Note: The `login` part is suffixed with one or more digits (e.g. `login5`).
208
+ - 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`
209
209
 
210
- - If you don't see this request, log out from the Spotify desktop client and log back in.
210
+ - If you don't see this request, try following steps (stop once it works):
211
+ - restart the Spotify desktop client
212
+ - log out from the Spotify desktop client and log back in
213
+ - point Spotify at the intercepting proxy directly in its settings, i.e. in **Spotify → Settings → Proxy Settings**, set:
214
+ - **proxy type**: `HTTP`
215
+ - **host**: `127.0.0.1` (IP/FQDN of your proxy, for Proxyman use the IP you see at the top bar)
216
+ - **port**: `9090` (port of your proxy, for Proxyman use the port you see at the top bar)
217
+ - 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
218
+ - 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
219
+ - try an older version of the Spotify desktop client
211
220
 
212
221
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
213
222
  - In Proxyman: **right click the request → Export → Request Body → Save File**.
@@ -0,0 +1,7 @@
1
+ spotify_monitor.py,sha256=Nq4RTsG9bHUeVpGJLOOCX8lr5xySEniNsu4aohrpU-o,158392
2
+ spotify_monitor-2.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ spotify_monitor-2.3.dist-info/METADATA,sha256=MFBSxyNhzVr5ZCJHMWWenD4CH9EWGYvgA0UB2ZM-bgI,23411
4
+ spotify_monitor-2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ spotify_monitor-2.3.dist-info/entry_points.txt,sha256=8HzePfUcCSXrYaXOwLbNNYO8GJcnhgCSl4wcDNECht8,57
6
+ spotify_monitor-2.3.dist-info/top_level.txt,sha256=EP6IPD4vHT12rLM5b_jo2i3nrfOuwk3ehhr2gWdQx9Y,16
7
+ spotify_monitor-2.3.dist-info/RECORD,,
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.2.1
4
+ v2.3
5
5
 
6
6
  Tool implementing real-time tracking of Spotify friends music activity:
7
7
  https://github.com/misiektoja/spotify_monitor/
@@ -15,7 +15,7 @@ pyotp (optional, needed when the token source is set to cookie)
15
15
  python-dotenv (optional)
16
16
  """
17
17
 
18
- VERSION = "2.2.1"
18
+ VERSION = "2.3"
19
19
 
20
20
  # ---------------------------
21
21
  # CONFIGURATION SECTION START
@@ -223,6 +223,18 @@ HORIZONTAL_LINE = 113
223
223
  # Whether to clear the terminal screen after starting the tool
224
224
  CLEAR_SCREEN = True
225
225
 
226
+ # Path to a file that is created when the user is active and deleted when inactive
227
+ # Useful for external tools to detect streaming status
228
+ # Can also be set via the --flag-file flag
229
+ FLAG_FILE = ""
230
+
231
+ # Max characters per line when printing to screen to avoid line wrapping
232
+ # Does not affect log file output
233
+ # Set to 999 to auto-detect terminal width
234
+ # Applies only when DISABLE_LOGGING is False
235
+ # Can also be set via the --truncate flag
236
+ TRUNCATE_CHARS = 0
237
+
226
238
  # Value added/subtracted via signal handlers to adjust inactivity timeout (SPOTIFY_INACTIVITY_CHECK); in seconds
227
239
  SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 30 # 30 seconds
228
240
 
@@ -434,6 +446,8 @@ CLEAR_SCREEN = False
434
446
  SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 0
435
447
  TOKEN_MAX_RETRIES = 0
436
448
  TOKEN_RETRY_TIMEOUT = 0.0
449
+ FLAG_FILE = ""
450
+ TRUNCATE_CHARS = 0
437
451
 
438
452
  exec(CONFIG_BLOCK, globals())
439
453
 
@@ -463,6 +477,12 @@ SP_CACHED_CLIENT_ID = ""
463
477
  # URL of the Spotify Web Player endpoint to get access token
464
478
  TOKEN_URL = "https://open.spotify.com/api/token"
465
479
 
480
+ # URL of the endpoint to get server time needed to create TOTP object
481
+ SERVER_TIME_URL = "https://open.spotify.com/"
482
+
483
+ # Identifier used to select the appropriate encrypted secret from secret_cipher_dict when generating a TOTP token
484
+ TOTP_VER = 10
485
+
466
486
  # Variables for caching functionality of the Spotify client token to avoid unnecessary refreshing
467
487
  SP_CACHED_CLIENT_TOKEN = None
468
488
  SP_CLIENT_TOKEN_EXPIRES_AT = 0
@@ -540,6 +560,19 @@ SESSION.mount("https://", adapter)
540
560
  SESSION.mount("http://", adapter)
541
561
 
542
562
 
563
+ # Truncates each line of a string to a specified number of characters including tab expansion and multi-line support
564
+ def truncate_string_per_line(message, truncate_chars, tabsize=8):
565
+ lines = message.split('\n')
566
+ truncated_lines = []
567
+
568
+ for line in lines:
569
+ expanded_line = line.expandtabs(tabsize=tabsize)
570
+ truncated_line = expanded_line[:truncate_chars]
571
+ truncated_lines.append(truncated_line)
572
+
573
+ return '\n'.join(truncated_lines)
574
+
575
+
543
576
  # Logger class to output messages to stdout and log file
544
577
  class Logger(object):
545
578
  def __init__(self, filename):
@@ -547,8 +580,10 @@ class Logger(object):
547
580
  self.logfile = open(filename, "a", buffering=1, encoding="utf-8")
548
581
 
549
582
  def write(self, message):
550
- self.terminal.write(message)
551
583
  self.logfile.write(message)
584
+ if (TRUNCATE_CHARS):
585
+ message = truncate_string_per_line(message, TRUNCATE_CHARS)
586
+ self.terminal.write(message)
552
587
  self.terminal.flush()
553
588
  self.logfile.flush()
554
589
 
@@ -556,6 +591,22 @@ class Logger(object):
556
591
  pass
557
592
 
558
593
 
594
+ def flag_file_create():
595
+ try:
596
+ with open(FLAG_FILE, "w") as f:
597
+ f.write("This indicates active streaming by monitored user")
598
+ except Exception:
599
+ pass
600
+
601
+
602
+ def flag_file_delete():
603
+ try:
604
+ if os.path.exists(FLAG_FILE):
605
+ os.remove(FLAG_FILE)
606
+ except Exception:
607
+ pass
608
+
609
+
559
610
  # Class used to generate timeout exceptions
560
611
  class TimeoutException(Exception):
561
612
  pass
@@ -570,6 +621,8 @@ def timeout_handler(sig, frame):
570
621
  def signal_handler(sig, frame):
571
622
  sys.stdout = stdout_bck
572
623
  print('\n* You pressed Ctrl+C, tool is terminated.')
624
+ if FLAG_FILE:
625
+ flag_file_delete()
573
626
  sys.exit(0)
574
627
 
575
628
 
@@ -1167,7 +1220,7 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
1167
1220
  if platform.system() != 'Windows':
1168
1221
  signal.signal(signal.SIGALRM, timeout_handler)
1169
1222
  signal.alarm(FUNCTION_TIMEOUT + 2)
1170
- response = session.head("https://open.spotify.com/", headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1223
+ response = session.head(SERVER_TIME_URL, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1171
1224
  response.raise_for_status()
1172
1225
  except TimeoutException as e:
1173
1226
  raise Exception(f"fetch_server_time() head network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
@@ -1177,7 +1230,11 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
1177
1230
  if platform.system() != 'Windows':
1178
1231
  signal.alarm(0)
1179
1232
 
1180
- return int(parsedate_to_datetime(response.headers["Date"]).timestamp())
1233
+ date_hdr = response.headers.get("Date")
1234
+ if not date_hdr:
1235
+ raise Exception("fetch_server_time() missing 'Date' header")
1236
+
1237
+ return int(parsedate_to_datetime(date_hdr).timestamp())
1181
1238
 
1182
1239
 
1183
1240
  # Creates a TOTP object using a secret derived from transformed cipher bytes
@@ -1185,12 +1242,14 @@ def generate_totp():
1185
1242
  import pyotp
1186
1243
 
1187
1244
  secret_cipher_dict = {
1245
+ "10": [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75],
1246
+ "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],
1188
1247
  "8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
1189
1248
  "7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
1190
1249
  "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]
1250
+ "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54],
1192
1251
  }
1193
- secret_cipher_bytes = secret_cipher_dict["8"]
1252
+ secret_cipher_bytes = secret_cipher_dict[str(TOTP_VER)]
1194
1253
 
1195
1254
  transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
1196
1255
  joined = "".join(str(num) for num in transformed)
@@ -1205,7 +1264,6 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1205
1264
  transport = True
1206
1265
  init = True
1207
1266
  session = req.Session()
1208
- session.cookies.set("sp_dc", sp_dc)
1209
1267
  data: dict = {}
1210
1268
  token = ""
1211
1269
 
@@ -1219,13 +1277,17 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1219
1277
  "productType": "web-player",
1220
1278
  "totp": otp_value,
1221
1279
  "totpServer": otp_value,
1222
- "totpVer": 8,
1223
- "sTime": server_time,
1224
- "cTime": client_time,
1225
- "buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
1226
- "buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
1280
+ "totpVer": TOTP_VER,
1227
1281
  }
1228
1282
 
1283
+ if TOTP_VER < 10:
1284
+ params.update({
1285
+ "sTime": server_time,
1286
+ "cTime": client_time,
1287
+ "buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
1288
+ "buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
1289
+ })
1290
+
1229
1291
  headers = {
1230
1292
  "User-Agent": USER_AGENT,
1231
1293
  "Accept": "application/json",
@@ -2402,6 +2464,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2402
2464
  song_on_loop = 1
2403
2465
  print("\n*** Friend is currently ACTIVE !")
2404
2466
 
2467
+ if FLAG_FILE:
2468
+ flag_file_create()
2469
+
2405
2470
  if sp_track.upper() in tracks_upper or sp_playlist.upper() in tracks_upper or sp_album.upper() in tracks_upper:
2406
2471
  print("*** Track/playlist/album matched with the list!")
2407
2472
 
@@ -2413,8 +2478,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2413
2478
 
2414
2479
  if ACTIVE_NOTIFICATION:
2415
2480
  m_subject = f"Spotify user {sp_username} is active: '{sp_artist} - {sp_track}'"
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>"
2481
+ 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\nSongs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2482
+ 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>Songs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
2418
2483
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2419
2484
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2420
2485
 
@@ -2431,6 +2496,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2431
2496
  sp_active_ts_stop = sp_ts
2432
2497
  print(f"\n*** Friend is OFFLINE for: {calculate_timespan(int(cur_ts), int(sp_ts))}")
2433
2498
 
2499
+ if listened_songs:
2500
+ print(f"\nSongs played:\t\t\t{listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})")
2501
+
2434
2502
  print(f"\nTracks/playlists/albums to monitor: {tracks}")
2435
2503
  print_cur_ts("\nTimestamp:\t\t\t")
2436
2504
 
@@ -2722,6 +2790,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2722
2790
  looped_songs = 0
2723
2791
  song_on_loop = 1
2724
2792
 
2793
+ if FLAG_FILE:
2794
+ flag_file_create()
2795
+
2725
2796
  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)})")
2726
2797
  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)})"
2727
2798
  friend_active_m_body = f"\n\nFriend got active after being offline for {calculate_timespan(int(sp_active_ts_start), int(sp_active_ts_stop))}\nLast activity (before getting offline): {get_date_from_ts(sp_active_ts_stop)}"
@@ -2737,8 +2808,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2737
2808
  sp_active_ts_start = sp_active_ts_start_old
2738
2809
  sp_active_ts_stop = 0
2739
2810
 
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>"
2811
+ 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\nSongs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2812
+ 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>Songs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
2742
2813
 
2743
2814
  if ACTIVE_NOTIFICATION:
2744
2815
  print(f"Sending email notification to {RECEIVER_EMAIL}")
@@ -2752,16 +2823,16 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2752
2823
 
2753
2824
  if (TRACK_NOTIFICATION and on_the_list and not email_sent) or (SONG_NOTIFICATION and not email_sent):
2754
2825
  m_subject = f"Spotify user {sp_username}: '{sp_artist} - {sp_track}'"
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>"
2826
+ 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\nSongs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2827
+ 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>Songs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
2757
2828
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2758
2829
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2759
2830
  email_sent = True
2760
2831
 
2761
2832
  if song_on_loop == SONG_ON_LOOP_VALUE and SONG_ON_LOOP_NOTIFICATION:
2762
2833
  m_subject = f"Spotify user {sp_username} plays song on loop: '{sp_artist} - {sp_track}'"
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>"
2834
+ 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\nSongs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2835
+ 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>Songs played: {listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
2765
2836
  if not email_sent:
2766
2837
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2767
2838
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
@@ -2772,6 +2843,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2772
2843
  except Exception as e:
2773
2844
  print(f"* Error: {e}")
2774
2845
 
2846
+ if listened_songs:
2847
+ print(f"\nSongs played:\t\t\t{listened_songs} ({calculate_timespan(int(sp_ts), int(sp_active_ts_start))})")
2848
+
2775
2849
  print_cur_ts("\nTimestamp:\t\t\t")
2776
2850
  sp_ts_old = sp_ts
2777
2851
  # Track has not changed
@@ -2783,6 +2857,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2783
2857
  print(f"*** Friend got INACTIVE after listening to music for {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}")
2784
2858
  print(f"*** Friend played music from {get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True, between_sep=' to ')}")
2785
2859
 
2860
+ if FLAG_FILE:
2861
+ flag_file_delete()
2862
+
2786
2863
  listened_songs_text = f"*** User played {listened_songs} songs"
2787
2864
  listened_songs_mbody = f"\n\nUser played {listened_songs} songs"
2788
2865
  listened_songs_mbody_html = f"<br><br>User played <b>{listened_songs}</b> songs"
@@ -2880,7 +2957,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2880
2957
 
2881
2958
 
2882
2959
  def main():
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
2960
+ 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, FLAG_FILE, TRUNCATE_CHARS
2884
2961
 
2885
2962
  if "--generate-config" in sys.argv:
2886
2963
  print(CONFIG_BLOCK.strip("\n"))
@@ -3088,6 +3165,12 @@ def main():
3088
3165
  type=str,
3089
3166
  help="Filename with Spotify tracks/playlists/albums to alert on"
3090
3167
  )
3168
+ opts.add_argument(
3169
+ "--flag-file",
3170
+ dest="flag_file",
3171
+ metavar="PATH",
3172
+ help="Path to flag file that is created when the user is active and deleted when inactive",
3173
+ )
3091
3174
  opts.add_argument(
3092
3175
  "--user-agent",
3093
3176
  dest="user_agent",
@@ -3109,6 +3192,13 @@ def main():
3109
3192
  default=None,
3110
3193
  help="Disable logging to spotify_monitor_<user_uri_id/file_suffix>.log"
3111
3194
  )
3195
+ opts.add_argument(
3196
+ "--truncate",
3197
+ dest="truncate",
3198
+ metavar="N",
3199
+ type=int,
3200
+ help="Max characters per screen line (not log), use 999 to auto-detect terminal width, ignored if -d is set"
3201
+ )
3112
3202
 
3113
3203
  args = parser.parse_args()
3114
3204
 
@@ -3191,6 +3281,13 @@ def main():
3191
3281
  if not check_internet():
3192
3282
  sys.exit(1)
3193
3283
 
3284
+ if args.flag_file:
3285
+ FLAG_FILE = os.path.expanduser(args.flag_file)
3286
+ flag_file_delete()
3287
+ else:
3288
+ if FLAG_FILE:
3289
+ FLAG_FILE = os.path.expanduser(FLAG_FILE)
3290
+
3194
3291
  if args.send_test_email:
3195
3292
  print("* Sending test email notification ...\n")
3196
3293
  if send_email("spotify_monitor: test email", "This is test email - your SMTP settings seems to be correct !", "", SMTP_SSL, smtp_timeout=5) == 0:
@@ -3405,6 +3502,18 @@ def main():
3405
3502
  if not FILE_SUFFIX:
3406
3503
  FILE_SUFFIX = str(args.user_id)
3407
3504
 
3505
+ if args.truncate:
3506
+ if args.truncate != 999:
3507
+ TRUNCATE_CHARS = args.truncate
3508
+ else:
3509
+ try:
3510
+ terminal_size = shutil.get_terminal_size()
3511
+ print(f"The detected terminal screen width is: {terminal_size.columns} characters\n")
3512
+ TRUNCATE_CHARS = terminal_size.columns
3513
+ except Exception as e:
3514
+ print(f"Error: Cannot determine terminal screen width: {e}")
3515
+ sys.exit(1)
3516
+
3408
3517
  if args.disable_logging is True:
3409
3518
  DISABLE_LOGGING = True
3410
3519
 
@@ -3451,7 +3560,7 @@ def main():
3451
3560
  SONG_ON_LOOP_NOTIFICATION = False
3452
3561
  ERROR_NOTIFICATION = False
3453
3562
 
3454
- print(f"* Spotify polling intervals:\t[check: {display_time(SPOTIFY_CHECK_INTERVAL)}] [inactivity: {display_time(SPOTIFY_INACTIVITY_CHECK)}]\n\t\t\t\t[disappeared: {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)}] [error: {display_time(SPOTIFY_ERROR_INTERVAL)}]")
3563
+ print(f"* Spotify polling intervals:\t[check: {display_time(SPOTIFY_CHECK_INTERVAL)}] [inactivity: {display_time(SPOTIFY_INACTIVITY_CHECK)}]\n*\t\t\t\t[disappeared: {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)}] [error: {display_time(SPOTIFY_ERROR_INTERVAL)}]")
3455
3564
  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}]")
3456
3565
  print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
3457
3566
  print(f"* Track listened songs:\t\t{TRACK_SONGS}")
@@ -3460,6 +3569,10 @@ def main():
3460
3569
  print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
3461
3570
  print(f"* Alert on monitored tracks:\t{bool(MONITOR_LIST_FILE)}" + (f" ({MONITOR_LIST_FILE})" if MONITOR_LIST_FILE else ""))
3462
3571
  print(f"* Output logging enabled:\t{not DISABLE_LOGGING}" + (f" ({FINAL_LOG_PATH})" if not DISABLE_LOGGING else ""))
3572
+ if not DISABLE_LOGGING and TRUNCATE_CHARS > 0:
3573
+ print(f"* Truncate terminal lines:\t{TRUNCATE_CHARS} chars")
3574
+ if FLAG_FILE:
3575
+ print(f"* Flag file:\t\t\t{FLAG_FILE}")
3463
3576
  print(f"* Configuration file:\t\t{cfg_path}")
3464
3577
  print(f"* Dotenv file:\t\t\t{env_path or 'None'}\n")
3465
3578
 
@@ -1,7 +0,0 @@
1
- spotify_monitor.py,sha256=-uJet8WI668Ihnqs3uArowPfrYa4MUzB1QPYEL_1EGg,153671
2
- spotify_monitor-2.2.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- spotify_monitor-2.2.1.dist-info/METADATA,sha256=q0IT4d33nTghRZ4boNvp0KxUgmD17-URY6bQMTgPAI8,22613
4
- spotify_monitor-2.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- spotify_monitor-2.2.1.dist-info/entry_points.txt,sha256=8HzePfUcCSXrYaXOwLbNNYO8GJcnhgCSl4wcDNECht8,57
6
- spotify_monitor-2.2.1.dist-info/top_level.txt,sha256=EP6IPD4vHT12rLM5b_jo2i3nrfOuwk3ehhr2gWdQx9Y,16
7
- spotify_monitor-2.2.1.dist-info/RECORD,,