spotify-monitor 2.2__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
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
@@ -25,7 +25,7 @@ Dynamic: license-file
25
25
 
26
26
  # spotify_monitor
27
27
 
28
- spotify_monitor is a tool for real-time monitoring of Spotify friends' music activity.
28
+ Tool for real-time monitoring of Spotify friends' music activity feed.
29
29
 
30
30
  NOTE: If you're interested in tracking changes to Spotify users' profiles including their playlists, take a look at another tool I've developed: [spotify_profile_monitor](https://github.com/misiektoja/spotify_profile_monitor).
31
31
 
@@ -200,12 +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
- - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com)).
203
+ - Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
204
204
 
205
- - Launch the Spotify desktop client and look for POST requests to `https://login{n}.spotify.com/v3/login`
206
- - Note: The `login` part is suffixed with one or more digits (e.g. `login5`).
205
+ - Enable SSL traffic decryption for `spotify.com` domain
206
+ - in Proxyman: click **Tools SSL Proxying List + button Add Domain → paste `*.spotify.com` → Add**
207
207
 
208
- - If you don't see this request, log out from the Spotify desktop client and log back in.
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
+
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
209
220
 
210
221
  - Export the login request body (a binary Protobuf payload) to a file (e.g. ***login-request-body-file***)
211
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
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"
18
+ VERSION = "2.3"
19
19
 
20
20
  # ---------------------------
21
21
  # CONFIGURATION SECTION START
@@ -149,6 +149,10 @@ SP_USER_GOT_OFFLINE_TRACK_ID = ""
149
149
  # Set to 0 to keep playing indefinitely until manually paused
150
150
  SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 5 # 5 seconds
151
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
+
152
156
  # Optional: specify user agent manually
153
157
  #
154
158
  # When the token source is 'cookie' - set it to web browser user agent, some examples:
@@ -219,6 +223,18 @@ HORIZONTAL_LINE = 113
219
223
  # Whether to clear the terminal screen after starting the tool
220
224
  CLEAR_SCREEN = True
221
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
+
222
238
  # Value added/subtracted via signal handlers to adjust inactivity timeout (SPOTIFY_INACTIVITY_CHECK); in seconds
223
239
  SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 30 # 30 seconds
224
240
 
@@ -409,6 +425,7 @@ SONG_ON_LOOP_VALUE = 0
409
425
  SKIPPED_SONG_THRESHOLD = 0
410
426
  SP_USER_GOT_OFFLINE_TRACK_ID = ""
411
427
  SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 0
428
+ REMOVED_DISAPPEARED_COUNTER = 0
412
429
  USER_AGENT = ""
413
430
  LIVENESS_CHECK_INTERVAL = 0
414
431
  CHECK_INTERNET_URL = ""
@@ -429,6 +446,8 @@ CLEAR_SCREEN = False
429
446
  SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 0
430
447
  TOKEN_MAX_RETRIES = 0
431
448
  TOKEN_RETRY_TIMEOUT = 0.0
449
+ FLAG_FILE = ""
450
+ TRUNCATE_CHARS = 0
432
451
 
433
452
  exec(CONFIG_BLOCK, globals())
434
453
 
@@ -458,6 +477,12 @@ SP_CACHED_CLIENT_ID = ""
458
477
  # URL of the Spotify Web Player endpoint to get access token
459
478
  TOKEN_URL = "https://open.spotify.com/api/token"
460
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
+
461
486
  # Variables for caching functionality of the Spotify client token to avoid unnecessary refreshing
462
487
  SP_CACHED_CLIENT_TOKEN = None
463
488
  SP_CLIENT_TOKEN_EXPIRES_AT = 0
@@ -535,6 +560,19 @@ SESSION.mount("https://", adapter)
535
560
  SESSION.mount("http://", adapter)
536
561
 
537
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
+
538
576
  # Logger class to output messages to stdout and log file
539
577
  class Logger(object):
540
578
  def __init__(self, filename):
@@ -542,8 +580,10 @@ class Logger(object):
542
580
  self.logfile = open(filename, "a", buffering=1, encoding="utf-8")
543
581
 
544
582
  def write(self, message):
545
- self.terminal.write(message)
546
583
  self.logfile.write(message)
584
+ if (TRUNCATE_CHARS):
585
+ message = truncate_string_per_line(message, TRUNCATE_CHARS)
586
+ self.terminal.write(message)
547
587
  self.terminal.flush()
548
588
  self.logfile.flush()
549
589
 
@@ -551,6 +591,22 @@ class Logger(object):
551
591
  pass
552
592
 
553
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
+
554
610
  # Class used to generate timeout exceptions
555
611
  class TimeoutException(Exception):
556
612
  pass
@@ -565,6 +621,8 @@ def timeout_handler(sig, frame):
565
621
  def signal_handler(sig, frame):
566
622
  sys.stdout = stdout_bck
567
623
  print('\n* You pressed Ctrl+C, tool is terminated.')
624
+ if FLAG_FILE:
625
+ flag_file_delete()
568
626
  sys.exit(0)
569
627
 
570
628
 
@@ -1162,7 +1220,7 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
1162
1220
  if platform.system() != 'Windows':
1163
1221
  signal.signal(signal.SIGALRM, timeout_handler)
1164
1222
  signal.alarm(FUNCTION_TIMEOUT + 2)
1165
- 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)
1166
1224
  response.raise_for_status()
1167
1225
  except TimeoutException as e:
1168
1226
  raise Exception(f"fetch_server_time() head network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
@@ -1172,17 +1230,26 @@ def fetch_server_time(session: req.Session, ua: str) -> int:
1172
1230
  if platform.system() != 'Windows':
1173
1231
  signal.alarm(0)
1174
1232
 
1175
- 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())
1176
1238
 
1177
1239
 
1178
1240
  # Creates a TOTP object using a secret derived from transformed cipher bytes
1179
1241
  def generate_totp():
1180
1242
  import pyotp
1181
1243
 
1182
- secret_cipher_bytes = [
1183
- 12, 56, 76, 33, 88, 44, 88, 33,
1184
- 78, 78, 11, 66, 22, 22, 55, 69, 54,
1185
- ]
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],
1247
+ "8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
1248
+ "7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
1249
+ "6": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
1250
+ "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54],
1251
+ }
1252
+ secret_cipher_bytes = secret_cipher_dict[str(TOTP_VER)]
1186
1253
 
1187
1254
  transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
1188
1255
  joined = "".join(str(num) for num in transformed)
@@ -1197,7 +1264,6 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1197
1264
  transport = True
1198
1265
  init = True
1199
1266
  session = req.Session()
1200
- session.cookies.set("sp_dc", sp_dc)
1201
1267
  data: dict = {}
1202
1268
  token = ""
1203
1269
 
@@ -1211,13 +1277,17 @@ def refresh_access_token_from_sp_dc(sp_dc: str) -> dict:
1211
1277
  "productType": "web-player",
1212
1278
  "totp": otp_value,
1213
1279
  "totpServer": otp_value,
1214
- "totpVer": 5,
1215
- "sTime": server_time,
1216
- "cTime": client_time,
1217
- "buildDate": time.strftime("%Y-%m-%d", time.gmtime(server_time)),
1218
- "buildVer": f"web-player_{time.strftime('%Y-%m-%d', time.gmtime(server_time))}_{server_time * 1000}_{secrets.token_hex(4)}",
1280
+ "totpVer": TOTP_VER,
1219
1281
  }
1220
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
+
1221
1291
  headers = {
1222
1292
  "User-Agent": USER_AGENT,
1223
1293
  "Accept": "application/json",
@@ -2251,7 +2321,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2251
2321
  SP_CACHED_ACCESS_TOKEN = None
2252
2322
 
2253
2323
  client_errs = ['access token', 'invalid client token', 'expired client token', 'refresh token has been revoked', 'refresh token has expired', 'refresh token is invalid', 'invalid grant during refresh']
2254
- cookie_errs = ['access token', 'unauthorized']
2324
+ cookie_errs = ['access token', 'unauthorized', 'unsuccessful token request']
2255
2325
 
2256
2326
  if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
2257
2327
  print(f"* Error: client or refresh token may be invalid or expired!")
@@ -2264,11 +2334,11 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2264
2334
  email_sent = True
2265
2335
 
2266
2336
  elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
2267
- print(f"* Error: sp_dc may be invalid or expired!")
2337
+ print(f"* Error: sp_dc may be invalid/expired or Spotify has broken sth again!")
2268
2338
  if ERROR_NOTIFICATION and not email_sent:
2269
- m_subject = f"spotify_monitor: sp_dc may be invalid or expired! (uri: {user_uri_id})"
2270
- m_body = f"sp_dc may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2271
- m_body_html = f"<html><head></head><body>sp_dc may be invalid or expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
2339
+ m_subject = f"spotify_monitor: sp_dc may be invalid/expired or Spotify has broken sth again! (uri: {user_uri_id})"
2340
+ 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: ')}"
2341
+ 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>"
2272
2342
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2273
2343
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2274
2344
  email_sent = True
@@ -2394,6 +2464,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2394
2464
  song_on_loop = 1
2395
2465
  print("\n*** Friend is currently ACTIVE !")
2396
2466
 
2467
+ if FLAG_FILE:
2468
+ flag_file_create()
2469
+
2397
2470
  if sp_track.upper() in tracks_upper or sp_playlist.upper() in tracks_upper or sp_album.upper() in tracks_upper:
2398
2471
  print("*** Track/playlist/album matched with the list!")
2399
2472
 
@@ -2405,8 +2478,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2405
2478
 
2406
2479
  if ACTIVE_NOTIFICATION:
2407
2480
  m_subject = f"Spotify user {sp_username} is active: '{sp_artist} - {sp_track}'"
2408
- m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2409
- m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
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>"
2410
2483
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2411
2484
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2412
2485
 
@@ -2423,6 +2496,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2423
2496
  sp_active_ts_stop = sp_ts
2424
2497
  print(f"\n*** Friend is OFFLINE for: {calculate_timespan(int(cur_ts), int(sp_ts))}")
2425
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
+
2426
2502
  print(f"\nTracks/playlists/albums to monitor: {tracks}")
2427
2503
  print_cur_ts("\nTimestamp:\t\t\t")
2428
2504
 
@@ -2431,6 +2507,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2431
2507
 
2432
2508
  email_sent = False
2433
2509
 
2510
+ disappeared_counter = 0
2511
+
2434
2512
  # Primary loop
2435
2513
  while True:
2436
2514
 
@@ -2498,7 +2576,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2498
2576
  print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: '{e}'")
2499
2577
 
2500
2578
  client_errs = ['access token', 'invalid client token', 'expired client token', 'refresh token has been revoked', 'refresh token has expired', 'refresh token is invalid', 'invalid grant during refresh']
2501
- cookie_errs = ['access token', 'unauthorized']
2579
+ cookie_errs = ['access token', 'unauthorized', 'unsuccessful token request']
2502
2580
 
2503
2581
  if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
2504
2582
  print(f"* Error: client or refresh token may be invalid or expired!")
@@ -2511,11 +2589,11 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2511
2589
  email_sent = True
2512
2590
 
2513
2591
  elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
2514
- print(f"* Error: sp_dc may be invalid or expired!")
2592
+ print(f"* Error: sp_dc may be invalid/expired or Spotify has broken sth again!")
2515
2593
  if ERROR_NOTIFICATION and not email_sent:
2516
- m_subject = f"spotify_monitor: sp_dc may be invalid or expired! (uri: {user_uri_id})"
2517
- m_body = f"sp_dc may be invalid or expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2518
- m_body_html = f"<html><head></head><body>sp_dc may be invalid or expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
2594
+ m_subject = f"spotify_monitor: sp_dc may be invalid/expired or Spotify has broken sth again! (uri: {user_uri_id})"
2595
+ 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: ')}"
2596
+ 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>"
2519
2597
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2520
2598
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2521
2599
  email_sent = True
@@ -2525,6 +2603,10 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2525
2603
 
2526
2604
  if sp_found is False:
2527
2605
  # User has disappeared from the Spotify's friend list or account has been removed
2606
+ disappeared_counter += 1
2607
+ if disappeared_counter < REMOVED_DISAPPEARED_COUNTER:
2608
+ time.sleep(SPOTIFY_CHECK_INTERVAL)
2609
+ continue
2528
2610
  if user_not_found is False:
2529
2611
  if is_user_removed(sp_accessToken, user_uri_id):
2530
2612
  print(f"Spotify user '{user_uri_id}' ({sp_username}) was probably removed! Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
@@ -2548,6 +2630,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2548
2630
  continue
2549
2631
  else:
2550
2632
  # User reappeared in the Spotify's friend list
2633
+ disappeared_counter = 0
2551
2634
  if user_not_found is True:
2552
2635
  print(f"Spotify user {user_uri_id} ({sp_username}) has reappeared!")
2553
2636
  if ERROR_NOTIFICATION:
@@ -2707,6 +2790,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2707
2790
  looped_songs = 0
2708
2791
  song_on_loop = 1
2709
2792
 
2793
+ if FLAG_FILE:
2794
+ flag_file_create()
2795
+
2710
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)})")
2711
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)})"
2712
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)}"
@@ -2722,8 +2808,8 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2722
2808
  sp_active_ts_start = sp_active_ts_start_old
2723
2809
  sp_active_ts_stop = 0
2724
2810
 
2725
- m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}{friend_active_m_body}\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2726
- m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a>{friend_active_m_body_html}<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
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>"
2727
2813
 
2728
2814
  if ACTIVE_NOTIFICATION:
2729
2815
  print(f"Sending email notification to {RECEIVER_EMAIL}")
@@ -2737,16 +2823,16 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2737
2823
 
2738
2824
  if (TRACK_NOTIFICATION and on_the_list and not email_sent) or (SONG_NOTIFICATION and not email_sent):
2739
2825
  m_subject = f"Spotify user {sp_username}: '{sp_artist} - {sp_track}'"
2740
- m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2741
- m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
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>"
2742
2828
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2743
2829
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2744
2830
  email_sent = True
2745
2831
 
2746
2832
  if song_on_loop == SONG_ON_LOOP_VALUE and SONG_ON_LOOP_NOTIFICATION:
2747
2833
  m_subject = f"Spotify user {sp_username} plays song on loop: '{sp_artist} - {sp_track}'"
2748
- m_body = f"Last played: {sp_artist} - {sp_track}\nDuration: {display_time(sp_track_duration)}{played_for_m_body}{playlist_m_body}\nAlbum: {sp_album}{context_m_body}\n\nApple Music URL: {apple_search_url}\nYouTube Music URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nUser plays song on LOOP ({song_on_loop} times)\n\nLast activity: {get_date_from_ts(sp_ts)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
2749
- m_body_html = f"<html><head></head><body>Last played: <b><a href=\"{sp_artist_url}\">{escape(sp_artist)}</a> - <a href=\"{sp_track_url}\">{escape(sp_track)}</a></b><br>Duration: {display_time(sp_track_duration)}{played_for_m_body_html}{playlist_m_body_html}<br>Album: <a href=\"{sp_album_url}\">{escape(sp_album)}</a>{context_m_body_html}<br><br>Apple Music URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music URL: <a href=\"{youtube_music_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>Genius lyrics URL: <a href=\"{genius_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br><br>User plays song on LOOP (<b>{song_on_loop}</b> times)<br><br>Last activity: {get_date_from_ts(sp_ts)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
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>"
2750
2836
  if not email_sent:
2751
2837
  print(f"Sending email notification to {RECEIVER_EMAIL}")
2752
2838
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
@@ -2757,6 +2843,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2757
2843
  except Exception as e:
2758
2844
  print(f"* Error: {e}")
2759
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
+
2760
2849
  print_cur_ts("\nTimestamp:\t\t\t")
2761
2850
  sp_ts_old = sp_ts
2762
2851
  # Track has not changed
@@ -2768,6 +2857,9 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2768
2857
  print(f"*** Friend got INACTIVE after listening to music for {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}")
2769
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 ')}")
2770
2859
 
2860
+ if FLAG_FILE:
2861
+ flag_file_delete()
2862
+
2771
2863
  listened_songs_text = f"*** User played {listened_songs} songs"
2772
2864
  listened_songs_mbody = f"\n\nUser played {listened_songs} songs"
2773
2865
  listened_songs_mbody_html = f"<br><br>User played <b>{listened_songs}</b> songs"
@@ -2865,7 +2957,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2865
2957
 
2866
2958
 
2867
2959
  def main():
2868
- global CLI_CONFIG_PATH, DOTENV_FILE, LIVENESS_CHECK_COUNTER, LOGIN_REQUEST_BODY_FILE, CLIENTTOKEN_REQUEST_BODY_FILE, REFRESH_TOKEN, LOGIN_URL, USER_AGENT, DEVICE_ID, SYSTEM_ID, USER_URI_ID, SP_DC_COOKIE, CSV_FILE, MONITOR_LIST_FILE, FILE_SUFFIX, DISABLE_LOGGING, SP_LOGFILE, ACTIVE_NOTIFICATION, INACTIVE_NOTIFICATION, TRACK_NOTIFICATION, SONG_NOTIFICATION, SONG_ON_LOOP_NOTIFICATION, ERROR_NOTIFICATION, SPOTIFY_CHECK_INTERVAL, SPOTIFY_INACTIVITY_CHECK, SPOTIFY_ERROR_INTERVAL, SPOTIFY_DISAPPEARED_CHECK_INTERVAL, TRACK_SONGS, SMTP_PASSWORD, stdout_bck, APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL, TOKEN_SOURCE, ALARM_TIMEOUT, pyotp, USER_AGENT
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
2869
2961
 
2870
2962
  if "--generate-config" in sys.argv:
2871
2963
  print(CONFIG_BLOCK.strip("\n"))
@@ -3073,6 +3165,12 @@ def main():
3073
3165
  type=str,
3074
3166
  help="Filename with Spotify tracks/playlists/albums to alert on"
3075
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
+ )
3076
3174
  opts.add_argument(
3077
3175
  "--user-agent",
3078
3176
  dest="user_agent",
@@ -3094,6 +3192,13 @@ def main():
3094
3192
  default=None,
3095
3193
  help="Disable logging to spotify_monitor_<user_uri_id/file_suffix>.log"
3096
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
+ )
3097
3202
 
3098
3203
  args = parser.parse_args()
3099
3204
 
@@ -3176,6 +3281,13 @@ def main():
3176
3281
  if not check_internet():
3177
3282
  sys.exit(1)
3178
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
+
3179
3291
  if args.send_test_email:
3180
3292
  print("* Sending test email notification ...\n")
3181
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:
@@ -3390,6 +3502,18 @@ def main():
3390
3502
  if not FILE_SUFFIX:
3391
3503
  FILE_SUFFIX = str(args.user_id)
3392
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
+
3393
3517
  if args.disable_logging is True:
3394
3518
  DISABLE_LOGGING = True
3395
3519
 
@@ -3436,7 +3560,7 @@ def main():
3436
3560
  SONG_ON_LOOP_NOTIFICATION = False
3437
3561
  ERROR_NOTIFICATION = False
3438
3562
 
3439
- 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)}]")
3440
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}]")
3441
3565
  print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
3442
3566
  print(f"* Track listened songs:\t\t{TRACK_SONGS}")
@@ -3445,6 +3569,10 @@ def main():
3445
3569
  print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
3446
3570
  print(f"* Alert on monitored tracks:\t{bool(MONITOR_LIST_FILE)}" + (f" ({MONITOR_LIST_FILE})" if MONITOR_LIST_FILE else ""))
3447
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}")
3448
3576
  print(f"* Configuration file:\t\t{cfg_path}")
3449
3577
  print(f"* Dotenv file:\t\t\t{env_path or 'None'}\n")
3450
3578
 
@@ -1,7 +0,0 @@
1
- spotify_monitor.py,sha256=OGM62kDeXOa8SO0RZK1Lhuagbw_Ltchy_oWtWW3xkIA,152513
2
- spotify_monitor-2.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- spotify_monitor-2.2.dist-info/METADATA,sha256=yXfwE_3vT7INKsIWxyBX9EgC2i3SL7-chl9gFNmH1lY,22382
4
- spotify_monitor-2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- spotify_monitor-2.2.dist-info/entry_points.txt,sha256=8HzePfUcCSXrYaXOwLbNNYO8GJcnhgCSl4wcDNECht8,57
6
- spotify_monitor-2.2.dist-info/top_level.txt,sha256=EP6IPD4vHT12rLM5b_jo2i3nrfOuwk3ehhr2gWdQx9Y,16
7
- spotify_monitor-2.2.dist-info/RECORD,,