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.
- {spotify_monitor-2.2.1.dist-info → spotify_monitor-2.3.dist-info}/METADATA +15 -6
- spotify_monitor-2.3.dist-info/RECORD +7 -0
- spotify_monitor.py +136 -23
- spotify_monitor-2.2.1.dist-info/RECORD +0 -7
- {spotify_monitor-2.2.1.dist-info → spotify_monitor-2.3.dist-info}/WHEEL +0 -0
- {spotify_monitor-2.2.1.dist-info → spotify_monitor-2.3.dist-info}/entry_points.txt +0 -0
- {spotify_monitor-2.2.1.dist-info → spotify_monitor-2.3.dist-info}/licenses/LICENSE +0 -0
- {spotify_monitor-2.2.1.dist-info → spotify_monitor-2.3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spotify_monitor
|
|
3
|
-
Version: 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
|
|
@@ -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
|
-
|
|
203
|
+
- Run an intercepting proxy of your choice (like [Proxyman](https://proxyman.com) - the trial version is sufficient)
|
|
204
204
|
|
|
205
|
-
-
|
|
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://
|
|
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,
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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[
|
|
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":
|
|
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
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|