spotify-monitor 2.0rc2__py3-none-any.whl → 2.1.1__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.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.0
4
+ v2.1.1
5
5
 
6
6
  Tool implementing real-time tracking of Spotify friends music activity:
7
7
  https://github.com/misiektoja/spotify_monitor/
@@ -11,20 +11,30 @@ Python pip3 requirements:
11
11
  requests
12
12
  python-dateutil
13
13
  urllib3
14
- pyotp
14
+ pyotp (needed when the token source is set to cookie)
15
15
  python-dotenv (optional)
16
16
  """
17
17
 
18
- VERSION = "2.0"
18
+ VERSION = "2.1.1"
19
19
 
20
20
  # ---------------------------
21
21
  # CONFIGURATION SECTION START
22
22
  # ---------------------------
23
23
 
24
24
  CONFIG_BLOCK = """
25
+ # Select the method used to obtain the Spotify access token
26
+ # Available options:
27
+ # cookie - uses the sp_dc cookie to retrieve a token via the Spotify web endpoint (recommended)
28
+ # client - uses captured credentials from the Spotify desktop client and a Protobuf-based login flow (for advanced users)
29
+ TOKEN_SOURCE = "cookie"
30
+
31
+ # ------------------------------------------------
32
+
33
+ # The section below is used when the token source is set to 'cookie'
34
+ # (to configure the alternative 'client' method, see the section at the end of this config block)
35
+ #
25
36
  # Log in to Spotify web client (https://open.spotify.com/) and retrieve your sp_dc cookie
26
37
  # Use your web browser's dev console or "Cookie-Editor" by cgagnier to extract it easily: https://cookie-editor.com/
27
- # The sp_dc cookie is typically valid for up to 2 weeks
28
38
  #
29
39
  # Provide the SP_DC_COOKIE secret using one of the following methods:
30
40
  # - Pass it at runtime with -u / --spotify-dc-cookie
@@ -34,6 +44,8 @@ CONFIG_BLOCK = """
34
44
  # - Hard-code it in the code or config file
35
45
  SP_DC_COOKIE = "your_sp_dc_cookie_value"
36
46
 
47
+ # ------------------------------------------------
48
+
37
49
  # SMTP settings for sending email notifications
38
50
  # If left as-is, no notifications will be sent
39
51
  #
@@ -97,7 +109,7 @@ SPOTIFY_INACTIVITY_CHECK = 660 # 11 mins
97
109
  # Can also be set using the -m flag
98
110
  SPOTIFY_DISAPPEARED_CHECK_INTERVAL = 180 # 3 mins
99
111
 
100
- # Whether to autoplay each listened song in your Spotify client
112
+ # Whether to auto-play each listened song in your Spotify client
101
113
  # Can also be set using the -g flag
102
114
  TRACK_SONGS = False
103
115
 
@@ -190,7 +202,7 @@ SP_LOGFILE = "spotify_monitor"
190
202
  # Can also be disabled via the -d flag
191
203
  DISABLE_LOGGING = False
192
204
 
193
- # Width of horizontal line (─)
205
+ # Width of horizontal line
194
206
  HORIZONTAL_LINE = 113
195
207
 
196
208
  # Whether to clear the terminal screen after starting the tool
@@ -199,11 +211,93 @@ CLEAR_SCREEN = True
199
211
  # Value added/subtracted via signal handlers to adjust inactivity timeout (SPOTIFY_INACTIVITY_CHECK); in seconds
200
212
  SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 30 # 30 seconds
201
213
 
202
- # Maximum number of attempts to get a valid access token in a single run of the spotify_get_access_token() function
214
+ # Maximum number of attempts to get a valid access token in a single run of the spotify_get_access_token_from_sp_dc() function
215
+ # Used only when the token source is set to 'cookie'
203
216
  TOKEN_MAX_RETRIES = 10
204
217
 
205
218
  # Interval between access token retry attempts; in seconds
219
+ # Used only when the token source is set to 'cookie'
206
220
  TOKEN_RETRY_TIMEOUT = 0.5 # 0.5 second
221
+
222
+ # ------------------------------------------------
223
+ # The section below is used when the token source is set to 'client'
224
+ #
225
+ # To extract device_id, system_id, user_uri_id and refresh_token from binary login request Protobuf file:
226
+ #
227
+ # - run an intercepting proxy of your choice (like Proxyman)
228
+ # - launch the Spotify desktop client and look for requests to: https://login{n}.spotify.com/v3/login
229
+ # note: the 'login' part is suffixed with one or more digits
230
+ # - export the login request body (a binary Protobuf payload) to a file
231
+ # (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <login-request-body-file>)
232
+ #
233
+ # Can also be set using the -w flag
234
+ LOGIN_REQUEST_BODY_FILE = ""
235
+
236
+ # Alternatively, set the configuration options below manually
237
+ #
238
+ # For the binary login request Protobuf body, if your proxy supports Protobuf decoding, extract:
239
+ # - DEVICE_ID
240
+ # - SYSTEM_ID
241
+ # - USER_URI_ID
242
+ # - REFRESH_TOKEN
243
+ # and assign them to respective configuration options
244
+ #
245
+ # Alternatively, you can use protoc tool (part of Protobuf pip package) to decode the binary:
246
+ # protoc --decode_raw < <path-to-login-request-body-file>
247
+ #
248
+ # If you decided to set the configuration options manually, it is recommended to provide the REFRESH_TOKEN
249
+ # secret using one of the following methods:
250
+ # - Set it as an environment variable (e.g. export REFRESH_TOKEN=...)
251
+ # - Add it to ".env" file (REFRESH_TOKEN=...) for persistent use
252
+ # Fallback:
253
+ # - Hard-code it in the code or config file
254
+ DEVICE_ID = "your_spotify_app_device_id"
255
+ SYSTEM_ID = "your_spotify_app_system_id"
256
+ USER_URI_ID = "your_spotify_user_uri_id"
257
+ REFRESH_TOKEN = "your_spotify_app_refresh_token"
258
+
259
+ # Default values below are typically fine - only modify if necessary
260
+
261
+ # Spotify login URL
262
+ LOGIN_URL = "https://login5.spotify.com/v3/login"
263
+
264
+ # Spotify client token URL
265
+ CLIENTTOKEN_URL = "https://clienttoken.spotify.com/v1/clienttoken"
266
+
267
+ # Optional: specify user agent manually, some examples:
268
+ #
269
+ # Spotify/126200580 Win32_x86_64/0 (PC desktop)
270
+ # Spotify/126400408 OSX_ARM64/OS X 15.5.0 [arm 2]
271
+ #
272
+ # Leave empty to auto-generate it randomly
273
+ USER_AGENT = ""
274
+
275
+ # Optional: specify app version manually (e.g. '1.2.62.580.g7e3d9a4f')
276
+ # Leave empty to auto-generate from USER_AGENT
277
+ APP_VERSION = ""
278
+
279
+ # Platform-specific values for token generation, typically leave unchanged
280
+ # You can also extract these from a captured client token request Protobuf file (see below)
281
+ CPU_ARCH = 10
282
+ OS_BUILD = 19045
283
+ PLATFORM = 2
284
+ OS_MAJOR = 9
285
+ OS_MINOR = 9
286
+ CLIENT_MODEL = 34404
287
+
288
+ # Optional: to extract app_version, cpu_arch, os_build, platform, os_major, os_minor and client_model from
289
+ # binary client token request Protobuf file:
290
+ #
291
+ # - run an intercepting proxy of your choice (like Proxyman)
292
+ # - launch the Spotify desktop client and look for requests to: https://clienttoken.spotify.com/v1/clienttoken
293
+ # (these requests are sent every time client token expires, usually every 2 weeks)
294
+ # - export the client token request body (a binary Protobuf payload) to a file
295
+ # (e.g. in Proxyman: right click the request -> Export -> Request Body -> Save File -> <clienttoken-request-body-file>)
296
+ #
297
+ # Can also be set via the -z flag
298
+ CLIENTTOKEN_REQUEST_BODY_FILE = ""
299
+
300
+ # ------------------------------------------------
207
301
  """
208
302
 
209
303
  # -------------------------
@@ -212,7 +306,24 @@ TOKEN_RETRY_TIMEOUT = 0.5 # 0.5 second
212
306
 
213
307
  # Default dummy values so linters shut up
214
308
  # Do not change values below - modify them in the configuration section or config file instead
309
+ TOKEN_SOURCE = ""
215
310
  SP_DC_COOKIE = ""
311
+ LOGIN_REQUEST_BODY_FILE = ""
312
+ CLIENTTOKEN_REQUEST_BODY_FILE = ""
313
+ LOGIN_URL = ""
314
+ USER_AGENT = ""
315
+ DEVICE_ID = ""
316
+ SYSTEM_ID = ""
317
+ USER_URI_ID = ""
318
+ REFRESH_TOKEN = ""
319
+ CLIENTTOKEN_URL = ""
320
+ APP_VERSION = ""
321
+ CPU_ARCH = 0
322
+ OS_BUILD = 0
323
+ PLATFORM = 0
324
+ OS_MAJOR = 0
325
+ OS_MINOR = 0
326
+ CLIENT_MODEL = 0
216
327
  SMTP_HOST = ""
217
328
  SMTP_PORT = 0
218
329
  SMTP_USER = ""
@@ -264,18 +375,23 @@ exec(CONFIG_BLOCK, globals())
264
375
  DEFAULT_CONFIG_FILENAME = "spotify_monitor.conf"
265
376
 
266
377
  # List of secret keys to load from env/config
267
- SECRET_KEYS = ("SP_DC_COOKIE", "SMTP_PASSWORD")
378
+ SECRET_KEYS = ("REFRESH_TOKEN", "SP_DC_COOKIE", "SMTP_PASSWORD")
268
379
 
269
380
  # Strings removed from track names for generating proper Genius search URLs
270
381
  re_search_str = r'remaster|extended|original mix|remix|original soundtrack|radio( |-)edit|\(feat\.|( \(.*version\))|( - .*version)'
271
382
  re_replace_str = r'( - (\d*)( )*remaster$)|( - (\d*)( )*remastered( version)*( \d*)*.*$)|( \((\d*)( )*remaster\)$)|( - (\d+) - remaster$)|( - extended$)|( - extended mix$)|( - (.*); extended mix$)|( - extended version$)|( - (.*) remix$)|( - remix$)|( - remixed by .*$)|( - original mix$)|( - .*original soundtrack$)|( - .*radio( |-)edit$)|( \(feat\. .*\)$)|( \(\d+.*Remaster.*\)$)|( \(.*Version\))|( - .*version)'
272
383
 
273
- # Default value for network-related timeouts in functions
384
+ # Default value for network-related timeouts in functions; in seconds
274
385
  FUNCTION_TIMEOUT = 15
275
386
 
276
- # Variables for caching functionality of the Spotify access token to avoid unnecessary refreshing
387
+ # Default value for alarm signal handler timeout; in seconds
388
+ ALARM_TIMEOUT = 15
389
+ ALARM_RETRY = 10
390
+
391
+ # Variables for caching functionality of the Spotify access and refresh token to avoid unnecessary refreshing
277
392
  SP_CACHED_ACCESS_TOKEN = None
278
- SP_TOKEN_EXPIRES_AT = 0
393
+ SP_CACHED_REFRESH_TOKEN = None
394
+ SP_ACCESS_TOKEN_EXPIRES_AT = 0
279
395
  SP_CACHED_CLIENT_ID = ""
280
396
  SP_CACHED_USER_AGENT = ""
281
397
 
@@ -285,9 +401,9 @@ TOKEN_URL = "https://open.spotify.com/get_access_token"
285
401
  # URL of the endpoint to get server time needed to create TOTP object
286
402
  SERVER_TIME_URL = "https://open.spotify.com/server-time"
287
403
 
288
- # Default value for alarm signal handler timeout; in seconds
289
- ALARM_TIMEOUT = int((TOKEN_MAX_RETRIES * TOKEN_RETRY_TIMEOUT) + 5)
290
- ALARM_RETRY = 10
404
+ # Variables for caching functionality of the Spotify client token to avoid unnecessary refreshing
405
+ SP_CACHED_CLIENT_TOKEN = None
406
+ SP_CLIENT_TOKEN_EXPIRES_AT = 0
291
407
 
292
408
  LIVENESS_CHECK_COUNTER = LIVENESS_CHECK_INTERVAL / SPOTIFY_CHECK_INTERVAL
293
409
 
@@ -323,20 +439,18 @@ from email.mime.multipart import MIMEMultipart
323
439
  from email.mime.text import MIMEText
324
440
  import argparse
325
441
  import csv
326
- from urllib.parse import quote_plus, quote
442
+ from urllib.parse import quote_plus, quote, urlparse
327
443
  import subprocess
328
444
  import platform
329
445
  import re
330
446
  import ipaddress
331
447
  from html import escape
332
- try:
333
- import pyotp
334
- except ModuleNotFoundError:
335
- raise SystemExit("Error: Couldn't find the pyotp library !\n\nTo install it, run:\n pip3 install pyotp\n\nOnce installed, re-run this tool")
336
448
  import base64
337
449
  import random
338
450
  import shutil
339
451
  from pathlib import Path
452
+ import secrets
453
+ from typing import Optional
340
454
 
341
455
  import urllib3
342
456
  if not VERIFY_SSL:
@@ -398,7 +512,7 @@ def signal_handler(sig, frame):
398
512
  # Checks internet connectivity
399
513
  def check_internet(url=CHECK_INTERNET_URL, timeout=CHECK_INTERNET_TIMEOUT, verify=VERIFY_SSL):
400
514
  try:
401
- _ = req.get(url, timeout=timeout, verify=verify)
515
+ _ = req.get(url, headers={'User-Agent': get_random_user_agent() if TOKEN_SOURCE == 'cookie' else get_random_spotify_user_agent()}, timeout=timeout, verify=verify)
402
516
  return True
403
517
  except req.RequestException as e:
404
518
  print(f"* No connectivity, please check your network:\n\n{e}")
@@ -536,7 +650,7 @@ def send_email(subject, body, body_html, use_ssl, smtp_timeout=15):
536
650
  return 1
537
651
 
538
652
  if not SMTP_USER or not isinstance(SMTP_USER, str) or SMTP_USER == "your_smtp_user" or not SMTP_PASSWORD or not isinstance(SMTP_PASSWORD, str) or SMTP_PASSWORD == "your_smtp_password":
539
- print("Error sending email - SMTP settings are incorrect (check SMTP_USER & SMTP_PASSWORD variables)")
653
+ print("Error sending email - SMTP settings are incorrect (check SMTP_USER & SMTP_PASSWORD configuration options)")
540
654
  return 1
541
655
 
542
656
  if not subject or not isinstance(subject, str):
@@ -772,10 +886,16 @@ def decrease_inactivity_check_signal_handler(sig, frame):
772
886
  print_cur_ts("Timestamp:\t\t\t")
773
887
 
774
888
 
775
- # Signal handler for SIGHUP allowing to reload secrets from .env
889
+ # Signal handler for SIGHUP allowing to reload secrets from dotenv files and token source credentials
890
+ # from login & client token requests body files
776
891
  def reload_secrets_signal_handler(sig, frame):
892
+ global DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN, LOGIN_URL, USER_AGENT, APP_VERSION, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL
893
+
777
894
  sig_name = signal.Signals(sig).name
778
- print(f"* Signal {sig_name} received")
895
+
896
+ print(f"* Signal {sig_name} received\n")
897
+
898
+ suffix = "\n" if TOKEN_SOURCE == 'client' else ""
779
899
 
780
900
  # disable autoscan if DOTENV_FILE set to none
781
901
  if DOTENV_FILE and DOTENV_FILE.lower() == 'none':
@@ -791,10 +911,10 @@ def reload_secrets_signal_handler(sig, frame):
791
911
  if env_path:
792
912
  load_dotenv(env_path, override=True)
793
913
  else:
794
- print("* No .env file found, skipping env-var reload")
914
+ print(f"* No .env file found, skipping env-var reload{suffix}")
795
915
  except ImportError:
796
916
  env_path = None
797
- print("* python-dotenv not installed, skipping env-var reload")
917
+ print(f"* python-dotenv not installed, skipping env-var reload{suffix}")
798
918
 
799
919
  if env_path:
800
920
  for secret in SECRET_KEYS:
@@ -802,12 +922,49 @@ def reload_secrets_signal_handler(sig, frame):
802
922
  val = os.getenv(secret)
803
923
  if val is not None and val != old_val:
804
924
  globals()[secret] = val
805
- print(f"* Reloaded {secret} from {env_path}")
925
+ print(f"* Reloaded {secret} from {env_path}{suffix}")
926
+
927
+ if TOKEN_SOURCE == 'client':
928
+
929
+ # Process the login request body file
930
+ if LOGIN_REQUEST_BODY_FILE:
931
+ if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
932
+ try:
933
+ DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN = parse_request_body_file(LOGIN_REQUEST_BODY_FILE)
934
+ except Exception as e:
935
+ print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
936
+ else:
937
+ print(f"* Login data correctly read from Protobuf file ({LOGIN_REQUEST_BODY_FILE}):")
938
+ print(" - Device ID:\t\t", DEVICE_ID)
939
+ print(" - System ID:\t\t", SYSTEM_ID)
940
+ print(" - User URI ID:\t\t", USER_URI_ID)
941
+ print(" - Refresh Token:\t<<hidden>>\n")
942
+ else:
943
+ print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) does not exist")
944
+
945
+ # Process the client token request body file
946
+ if CLIENTTOKEN_REQUEST_BODY_FILE:
947
+ if os.path.isfile(CLIENTTOKEN_REQUEST_BODY_FILE):
948
+ try:
949
+ (APP_VERSION, _, _, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL) = parse_clienttoken_request_body_file(CLIENTTOKEN_REQUEST_BODY_FILE)
950
+ except Exception as e:
951
+ print(f"* Error: Protobuf file ({CLIENTTOKEN_REQUEST_BODY_FILE}) cannot be processed: {e}")
952
+ else:
953
+ print(f"* Client token data correctly read from Protobuf file ({CLIENTTOKEN_REQUEST_BODY_FILE}):")
954
+ print(" - App version:\t\t", APP_VERSION)
955
+ print(" - CPU arch:\t\t", CPU_ARCH)
956
+ print(" - OS build:\t\t", OS_BUILD)
957
+ print(" - Platform:\t\t", PLATFORM)
958
+ print(" - OS major:\t\t", OS_MAJOR)
959
+ print(" - OS minor:\t\t", OS_MINOR)
960
+ print(" - Client model:\t", CLIENT_MODEL, "\n")
961
+ else:
962
+ print(f"* Error: Protobuf file ({CLIENTTOKEN_REQUEST_BODY_FILE}) does not exist")
806
963
 
807
964
  print_cur_ts("Timestamp:\t\t\t")
808
965
 
809
966
 
810
- # Prepares Apple & Genius search URLs for specified track
967
+ # Returns Apple & Genius search URLs for specified track
811
968
  def get_apple_genius_search_urls(artist, track):
812
969
  genius_search_string = f"{artist} {track}"
813
970
  youtube_music_search_string = quote_plus(f"{artist} {track}")
@@ -820,6 +977,39 @@ def get_apple_genius_search_urls(artist, track):
820
977
  return apple_search_url, genius_search_url, youtube_music_search_url
821
978
 
822
979
 
980
+ # Sends a lightweight request to check Spotify token validity
981
+ def check_token_validity(access_token: str, client_id: Optional[str] = None, user_agent: Optional[str] = None) -> bool:
982
+ url = "https://api.spotify.com/v1/me"
983
+ headers = {"Authorization": f"Bearer {access_token}"}
984
+
985
+ if user_agent is not None:
986
+ headers.update({
987
+ "User-Agent": user_agent
988
+ })
989
+
990
+ if TOKEN_SOURCE == "cookie" and client_id is not None:
991
+ headers.update({
992
+ "Client-Id": client_id
993
+ })
994
+
995
+ if platform.system() != 'Windows':
996
+ signal.signal(signal.SIGALRM, timeout_handler)
997
+ signal.alarm(FUNCTION_TIMEOUT + 2)
998
+ try:
999
+ response = req.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1000
+ valid = response.status_code == 200
1001
+ except Exception:
1002
+ valid = False
1003
+ finally:
1004
+ if platform.system() != 'Windows':
1005
+ signal.alarm(0)
1006
+ return valid
1007
+
1008
+
1009
+ # -------------------------------------------------------
1010
+ # Supporting functions when token source is set to cookie
1011
+ # -------------------------------------------------------
1012
+
823
1013
  # Returns random user agent string
824
1014
  def get_random_user_agent() -> str:
825
1015
  browser = random.choice(['chrome', 'firefox', 'edge', 'safari'])
@@ -909,6 +1099,8 @@ def hex_to_bytes(data: str) -> bytes:
909
1099
 
910
1100
  # Creates a TOTP object using a secret derived from transformed cipher bytes
911
1101
  def generate_totp(ua: str):
1102
+ import pyotp
1103
+
912
1104
  secret_cipher_bytes = [
913
1105
  12, 56, 76, 33, 88, 44, 88, 33,
914
1106
  78, 78, 11, 66, 22, 22, 55, 69, 54,
@@ -951,29 +1143,6 @@ def generate_totp(ua: str):
951
1143
  return totp_obj, server_time
952
1144
 
953
1145
 
954
- # Sends a lightweight request to check Spotify token validity
955
- def check_token_validity(token: str, client_id: str, user_agent: str) -> bool:
956
- url = "https://api.spotify.com/v1/me"
957
- headers = {
958
- "Authorization": f"Bearer {token}",
959
- "Client-Id": client_id,
960
- "User-Agent": user_agent,
961
- }
962
-
963
- if platform.system() != 'Windows':
964
- signal.signal(signal.SIGALRM, timeout_handler)
965
- signal.alarm(FUNCTION_TIMEOUT + 2)
966
- try:
967
- response = req.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
968
- valid = response.status_code == 200
969
- except Exception:
970
- valid = False
971
- finally:
972
- if platform.system() != 'Windows':
973
- signal.alarm(0)
974
- return valid
975
-
976
-
977
1146
  # Retrieves a new Spotify access token using the sp_dc cookie, tries first with mode "transport" and if needed with "init"
978
1147
  def refresh_token(sp_dc: str) -> dict:
979
1148
  transport = True
@@ -1052,12 +1221,12 @@ def refresh_token(sp_dc: str) -> dict:
1052
1221
 
1053
1222
 
1054
1223
  # Fetches Spotify access token based on provided SP_DC value
1055
- def spotify_get_access_token(sp_dc: str):
1056
- global SP_CACHED_ACCESS_TOKEN, SP_TOKEN_EXPIRES_AT, SP_CACHED_CLIENT_ID, SP_CACHED_USER_AGENT
1224
+ def spotify_get_access_token_from_sp_dc(sp_dc: str):
1225
+ global SP_CACHED_ACCESS_TOKEN, SP_ACCESS_TOKEN_EXPIRES_AT, SP_CACHED_CLIENT_ID, SP_CACHED_USER_AGENT
1057
1226
 
1058
1227
  now = time.time()
1059
1228
 
1060
- if SP_CACHED_ACCESS_TOKEN and now < SP_TOKEN_EXPIRES_AT and check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, SP_CACHED_USER_AGENT):
1229
+ if SP_CACHED_ACCESS_TOKEN and now < SP_ACCESS_TOKEN_EXPIRES_AT and check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, SP_CACHED_USER_AGENT):
1061
1230
  return SP_CACHED_ACCESS_TOKEN
1062
1231
 
1063
1232
  max_retries = TOKEN_MAX_RETRIES
@@ -1071,7 +1240,7 @@ def spotify_get_access_token(sp_dc: str):
1071
1240
  length = token_data["length"]
1072
1241
 
1073
1242
  SP_CACHED_ACCESS_TOKEN = token
1074
- SP_TOKEN_EXPIRES_AT = token_data["expires_at"]
1243
+ SP_ACCESS_TOKEN_EXPIRES_AT = token_data["expires_at"]
1075
1244
  SP_CACHED_CLIENT_ID = client_id
1076
1245
  SP_CACHED_USER_AGENT = user_agent
1077
1246
 
@@ -1096,14 +1265,490 @@ def spotify_get_access_token(sp_dc: str):
1096
1265
  return SP_CACHED_ACCESS_TOKEN
1097
1266
 
1098
1267
 
1268
+ # -------------------------------------------------------
1269
+ # Supporting functions when token source is set to client
1270
+ # -------------------------------------------------------
1271
+
1272
+ # Returns random Spotify client user agent string
1273
+ def get_random_spotify_user_agent() -> str:
1274
+ os_choice = random.choice(['windows', 'mac', 'linux'])
1275
+
1276
+ if os_choice == 'windows':
1277
+ build = random.randint(120000000, 130000000)
1278
+ arch = random.choice(['Win32', 'Win32_x86_64'])
1279
+ device = random.choice(['desktop', 'laptop'])
1280
+ return f"Spotify/{build} {arch}/0 (PC {device})"
1281
+
1282
+ elif os_choice == 'mac':
1283
+ build = random.randint(120000000, 130000000)
1284
+ arch = random.choice(['OSX_ARM64', 'OSX_X86_64'])
1285
+ major = random.randint(10, 15)
1286
+ minor = random.randint(0, 7)
1287
+ patch = random.randint(0, 5)
1288
+ os_version = f"OS X {major}.{minor}.{patch}"
1289
+ if arch == 'OSX_ARM64':
1290
+ bracket = f"[arm {random.randint(1, 3)}]"
1291
+ else:
1292
+ bracket = "[x86_64]"
1293
+ return f"Spotify/{build} {arch}/{os_version} {bracket}"
1294
+
1295
+ else: # linux
1296
+ build = random.randint(120000000, 130000000)
1297
+ arch = random.choice(['Linux; x86_64', 'Linux; x86'])
1298
+ return f"Spotify/{build} ({arch})"
1299
+
1300
+
1301
+ # Encodes an integer using Protobuf varint format
1302
+ def encode_varint(value):
1303
+ result = bytearray()
1304
+ while value > 0x7F:
1305
+ result.append((value & 0x7F) | 0x80)
1306
+ value //= 128
1307
+ result.append(value)
1308
+ return bytes(result)
1309
+
1310
+
1311
+ # Encodes a string field with the given tag
1312
+ def encode_string_field(tag, value):
1313
+ key = encode_varint((tag << 3) | 2) # wire type 2 (length-delimited)
1314
+ value_bytes = value.encode('utf-8')
1315
+ length = encode_varint(len(value_bytes))
1316
+ return key + length + value_bytes
1317
+
1318
+
1319
+ # Encodes a nested message field with the given tag
1320
+ def encode_nested_field(tag, nested_bytes):
1321
+ key = encode_varint((tag << 3) | 2)
1322
+ length = encode_varint(len(nested_bytes))
1323
+ return key + length + nested_bytes
1324
+
1325
+
1326
+ # Builds the Spotify Protobuf login request body
1327
+ def build_spotify_auth_protobuf(device_id, system_id, user_uri_id, refresh_token):
1328
+ """
1329
+ 1 {
1330
+ 1: "device_id"
1331
+ 2: "system_id"
1332
+ }
1333
+ 100 {
1334
+ 1: "user_uri_id"
1335
+ 2: "refresh_token"
1336
+ }
1337
+ """
1338
+ device_info_msg = encode_string_field(1, device_id) + encode_string_field(2, system_id)
1339
+ field_device_info = encode_nested_field(1, device_info_msg)
1340
+
1341
+ user_auth_msg = encode_string_field(1, user_uri_id) + encode_string_field(2, refresh_token)
1342
+ field_user_auth = encode_nested_field(100, user_auth_msg)
1343
+
1344
+ return field_device_info + field_user_auth
1345
+
1346
+
1347
+ # Reads a varint from data starting at index
1348
+ def read_varint(data, index):
1349
+ shift = 0
1350
+ result = 0
1351
+ bytes_read = 0
1352
+ while True:
1353
+ b = data[index]
1354
+ result |= ((b & 0x7F) << shift)
1355
+ bytes_read += 1
1356
+ index += 1
1357
+ if not (b & 0x80):
1358
+ break
1359
+ shift += 7
1360
+ return result, bytes_read
1361
+
1362
+
1363
+ # Parses Spotify Protobuf login response
1364
+ def parse_protobuf_message(data):
1365
+ index = 0
1366
+ result = {}
1367
+ while index < len(data):
1368
+ try:
1369
+ key, key_len = read_varint(data, index)
1370
+ except IndexError:
1371
+ break
1372
+ index += key_len
1373
+ tag = key >> 3
1374
+ wire_type = key & 0x07
1375
+ if wire_type == 2: # length-delimited
1376
+ length, len_len = read_varint(data, index)
1377
+ index += len_len
1378
+ raw_value = data[index:index + length]
1379
+ index += length
1380
+ # If the first byte is a control character (e.g. 0x0A) assume nested
1381
+ if raw_value and raw_value[0] < 0x20:
1382
+ value = parse_protobuf_message(raw_value)
1383
+ else:
1384
+ try:
1385
+ value = raw_value.decode('utf-8')
1386
+ except UnicodeDecodeError:
1387
+ value = raw_value
1388
+ result[tag] = value
1389
+ elif wire_type == 0: # varint
1390
+ value, var_len = read_varint(data, index)
1391
+ index += var_len
1392
+ result[tag] = value
1393
+ else:
1394
+ break
1395
+ return result # dictionary mapping tags to values
1396
+
1397
+
1398
+ # Parses the Protobuf-encoded login request body file (as dumped for example by Proxyman) and returns a tuple:
1399
+ # (device_id, system_id, user_uri_id, refresh_token)
1400
+ def parse_request_body_file(file_path):
1401
+ """
1402
+ {
1403
+ 1: {
1404
+ 1: "device_id",
1405
+ 2: "system_id"
1406
+ },
1407
+ 100: {
1408
+ 1: "user_uri_id",
1409
+ 2: "refresh_token"
1410
+ }
1411
+ }
1412
+ """
1413
+ with open(file_path, "rb") as f:
1414
+ data = f.read()
1415
+ parsed = parse_protobuf_message(data)
1416
+
1417
+ device_id = None
1418
+ system_id = None
1419
+ user_uri_id = None
1420
+ refresh_token = None
1421
+
1422
+ if 1 in parsed:
1423
+ device_info = parsed[1]
1424
+ if isinstance(device_info, dict):
1425
+ device_id = device_info.get(1)
1426
+ system_id = device_info.get(2)
1427
+ else:
1428
+ pass
1429
+
1430
+ if 100 in parsed:
1431
+ user_auth = parsed[100]
1432
+ if isinstance(user_auth, dict):
1433
+ user_uri_id = user_auth.get(1)
1434
+ refresh_token = user_auth.get(2)
1435
+
1436
+ protobuf_fields = {
1437
+ "device_id": device_id,
1438
+ "system_id": system_id,
1439
+ "user_uri_id": user_uri_id,
1440
+ "refresh_token": refresh_token,
1441
+ }
1442
+
1443
+ protobuf_missing_fields = [name for name, value in protobuf_fields.items() if value is None]
1444
+
1445
+ if protobuf_missing_fields:
1446
+ missing_str = ", ".join(protobuf_missing_fields)
1447
+ raise Exception(f"Following fields could not be extracted: {missing_str}")
1448
+
1449
+ return device_id, system_id, user_uri_id, refresh_token
1450
+
1451
+
1452
+ # Recursively flattens nested dictionaries or lists into a single string
1453
+ def deep_flatten(value):
1454
+ if isinstance(value, dict):
1455
+ return "".join(deep_flatten(v) for k, v in sorted(value.items()))
1456
+ elif isinstance(value, list):
1457
+ return "".join(deep_flatten(item) for item in value)
1458
+ else:
1459
+ return str(value)
1460
+
1461
+
1462
+ # Returns the input if it's a dict, parses as Protobuf it if it's bytes or returns an empty dict otherwise
1463
+ def ensure_dict(value):
1464
+ if isinstance(value, dict):
1465
+ return value
1466
+ if isinstance(value, (bytes, bytearray)):
1467
+ try:
1468
+ return parse_protobuf_message(value)
1469
+ except Exception:
1470
+ return {}
1471
+ return {}
1472
+
1473
+
1474
+ # Parses the Protobuf-encoded client token request body file (as dumped for example by Proxyman) and returns a tuple:
1475
+ # (app_version, device_id, system_id, cpu_arch, os_build, platform, os_major, os_minor, client_model)
1476
+ def parse_clienttoken_request_body_file(file_path):
1477
+
1478
+ with open(file_path, "rb") as f:
1479
+ data = f.read()
1480
+
1481
+ root = ensure_dict(parse_protobuf_message(data).get(2))
1482
+
1483
+ app_version = root.get(1)
1484
+ device_id = root.get(2)
1485
+
1486
+ nested_3 = ensure_dict(root.get(3))
1487
+ nested_1 = ensure_dict(nested_3.get(1))
1488
+ nested_4 = ensure_dict(nested_1.get(4))
1489
+
1490
+ cpu_arch = nested_4.get(1)
1491
+ os_build = nested_4.get(3)
1492
+ platform = nested_4.get(4)
1493
+ os_major = nested_4.get(5)
1494
+ os_minor = nested_4.get(6)
1495
+ client_model = nested_4.get(8)
1496
+
1497
+ system_id = nested_3.get(2)
1498
+
1499
+ required = {
1500
+ "app_version": app_version,
1501
+ "device_id": device_id,
1502
+ "system_id": system_id,
1503
+ }
1504
+ missing = [k for k, v in required.items() if v is None]
1505
+ if missing:
1506
+ raise Exception(f"Could not extract fields: {', '.join(missing)}")
1507
+
1508
+ return (app_version, device_id, system_id, cpu_arch, os_build, platform, os_major, os_minor, client_model)
1509
+
1510
+
1511
+ # Converts Spotify user agent string to Protobuf app_version string
1512
+ # For example: 'Spotify/126200580 Win32_x86_64/0 (PC desktop)' to '1.2.62.580.g<random-hex>'
1513
+ def ua_to_app_version(user_agent: str) -> str:
1514
+
1515
+ m = re.search(r"Spotify/(\d{5,})", user_agent)
1516
+ if not m:
1517
+ raise ValueError(f"User-Agent missing build number: {user_agent!r}")
1518
+
1519
+ digits = m.group(1)
1520
+ if len(digits) < 5:
1521
+ raise ValueError(f"Build number too short: {digits}")
1522
+
1523
+ major = digits[0]
1524
+ minor = digits[1]
1525
+ patch = str(int(digits[2:4]))
1526
+ build = str(int(digits[4:]))
1527
+ suffix = secrets.token_hex(4)
1528
+
1529
+ return f"{major}.{minor}.{patch}.{build}.g{suffix}"
1530
+
1531
+
1532
+ # Builds the Protobuf client token request body
1533
+ def build_clienttoken_request_protobuf(app_version, device_id, system_id, cpu_arch=10, os_build=19045, platform=2, os_major=9, os_minor=9, client_model=34404):
1534
+ """
1535
+ 1: 1 (const)
1536
+ 2: {
1537
+ 1: app_version
1538
+ 2: device_id
1539
+ 3: {
1540
+ 1: {
1541
+ 4: device_details
1542
+ }
1543
+ 2: system_id
1544
+ }
1545
+ }
1546
+ """
1547
+
1548
+ leaf = (
1549
+ encode_varint((1 << 3) | 0) + encode_varint(cpu_arch) + encode_varint((3 << 3) | 0) + encode_varint(os_build) + encode_varint((4 << 3) | 0) + encode_varint(platform) + encode_varint((5 << 3) | 0) + encode_varint(os_major) + encode_varint((6 << 3) | 0) + encode_varint(os_minor) + encode_varint((8 << 3) | 0) + encode_varint(client_model))
1550
+
1551
+ msg_4 = encode_nested_field(4, leaf)
1552
+ msg_1 = encode_nested_field(1, msg_4)
1553
+ msg_3 = msg_1 + encode_string_field(2, system_id)
1554
+
1555
+ payload = (encode_string_field(1, app_version) + encode_string_field(2, device_id) + encode_nested_field(3, msg_3))
1556
+
1557
+ root = (encode_varint((1 << 3) | 0) + encode_varint(1) + encode_nested_field(2, payload))
1558
+
1559
+ return root
1560
+
1561
+
1562
+ # Fetches Spotify access token based on provided device_id, system_id, user_uri_id, refresh_token and client_token value
1563
+ def spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refresh_token, client_token):
1564
+ global SP_CACHED_ACCESS_TOKEN, SP_CACHED_REFRESH_TOKEN, SP_ACCESS_TOKEN_EXPIRES_AT
1565
+
1566
+ if SP_CACHED_ACCESS_TOKEN and time.time() < SP_ACCESS_TOKEN_EXPIRES_AT and check_token_validity(SP_CACHED_ACCESS_TOKEN, user_agent=USER_AGENT):
1567
+ return SP_CACHED_ACCESS_TOKEN
1568
+
1569
+ if not client_token:
1570
+ raise Exception("Client token is missing")
1571
+
1572
+ if SP_CACHED_REFRESH_TOKEN:
1573
+ refresh_token = SP_CACHED_REFRESH_TOKEN
1574
+
1575
+ protobuf_body = build_spotify_auth_protobuf(device_id, system_id, user_uri_id, refresh_token)
1576
+
1577
+ parsed_url = urlparse(LOGIN_URL)
1578
+ host = parsed_url.netloc
1579
+ origin = f"{parsed_url.scheme}://{parsed_url.netloc}"
1580
+
1581
+ headers = {
1582
+ "Host": host,
1583
+ "Connection": "keep-alive",
1584
+ "Content-Type": "application/x-protobuf",
1585
+ "User-Agent": USER_AGENT,
1586
+ "X-Retry-Count": "0",
1587
+ "Client-Token": client_token,
1588
+ "Origin": origin,
1589
+ "Accept-Language": "en-Latn-GB,en-GB;q=0.9,en;q=0.8",
1590
+ "Sec-Fetch-Site": "same-origin",
1591
+ "Sec-Fetch-Mode": "no-cors",
1592
+ "Sec-Fetch-Dest": "empty",
1593
+ "Accept-Encoding": "gzip, deflate, br, zstd"
1594
+ }
1595
+
1596
+ try:
1597
+ if platform.system() != 'Windows':
1598
+ signal.signal(signal.SIGALRM, timeout_handler)
1599
+ signal.alarm(FUNCTION_TIMEOUT + 2)
1600
+ response = req.post(LOGIN_URL, headers=headers, data=protobuf_body, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1601
+ except (req.RequestException, TimeoutException) as e:
1602
+ raise Exception(f"spotify_get_access_token_from_client() network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
1603
+ finally:
1604
+ if platform.system() != 'Windows':
1605
+ signal.alarm(0)
1606
+
1607
+ if response.status_code != 200:
1608
+ if response.headers.get("client-token-error") == "INVALID_CLIENTTOKEN":
1609
+ raise Exception(f"Request failed with status {response.status_code}: invalid client token")
1610
+ elif response.headers.get("client-token-error") == "EXPIRED_CLIENTTOKEN":
1611
+ raise Exception(f"Request failed with status {response.status_code}: expired client token")
1612
+
1613
+ raise Exception(f"Request failed with status code {response.status_code}\nResponse Headers: {response.headers}\nResponse Content (raw): {response.content}\nResponse text: {response.text}")
1614
+
1615
+ parsed = parse_protobuf_message(response.content)
1616
+ # {1: {1: user_uri_id, 2: access_token, 3: refresh_token, 4: expires_in}}
1617
+ access_token_raw = None
1618
+ expires_in = 3600 # default
1619
+ if 1 in parsed and isinstance(parsed[1], dict):
1620
+ nested = parsed[1]
1621
+ access_token_raw = nested.get(2)
1622
+ user_uri_id = parsed[1].get(1)
1623
+
1624
+ if 4 in nested:
1625
+ raw_expires = nested.get(4)
1626
+ if isinstance(raw_expires, (int, str, bytes)):
1627
+ try:
1628
+ expires_in = int(raw_expires)
1629
+ except ValueError:
1630
+ expires_in = 3600
1631
+
1632
+ access_token = deep_flatten(access_token_raw) if access_token_raw else None
1633
+
1634
+ if not access_token:
1635
+ raise Exception("Access token not found in response")
1636
+
1637
+ SP_CACHED_ACCESS_TOKEN = access_token
1638
+ SP_CACHED_REFRESH_TOKEN = parsed[1].get(3)
1639
+ SP_ACCESS_TOKEN_EXPIRES_AT = time.time() + expires_in
1640
+ return access_token
1641
+
1642
+
1643
+ # Fetches fresh client token
1644
+ def spotify_get_client_token(app_version, device_id, system_id, **device_overrides):
1645
+ global SP_CACHED_CLIENT_TOKEN, SP_CLIENT_TOKEN_EXPIRES_AT
1646
+
1647
+ if SP_CACHED_CLIENT_TOKEN and time.time() < SP_CLIENT_TOKEN_EXPIRES_AT:
1648
+ return SP_CACHED_CLIENT_TOKEN
1649
+
1650
+ body = build_clienttoken_request_protobuf(app_version, device_id, system_id, **device_overrides)
1651
+
1652
+ headers = {
1653
+ "Host": "clienttoken.spotify.com",
1654
+ "Connection": "keep-alive",
1655
+ "Pragma": "no-cache",
1656
+ "Cache-Control": "no-cache, no-store, max-age=0",
1657
+ "Accept": "application/x-protobuf",
1658
+ "Content-Type": "application/x-protobuf",
1659
+ "User-Agent": USER_AGENT,
1660
+ "Origin": "https://clienttoken.spotify.com",
1661
+ "Accept-Language": "en-Latn-GB,en-GB;q=0.9,en;q=0.8",
1662
+ "Sec-Fetch-Site": "same-origin",
1663
+ "Sec-Fetch-Mode": "no-cors",
1664
+ "Sec-Fetch-Dest": "empty",
1665
+ "Accept-Encoding": "gzip, deflate, br, zstd",
1666
+ }
1667
+
1668
+ try:
1669
+ if platform.system() != 'Windows':
1670
+ signal.signal(signal.SIGALRM, timeout_handler)
1671
+ signal.alarm(FUNCTION_TIMEOUT + 2)
1672
+ response = req.post(CLIENTTOKEN_URL, headers=headers, data=body, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1673
+ except (req.RequestException, TimeoutException) as e:
1674
+ raise Exception(f"spotify_get_client_token() network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
1675
+ finally:
1676
+ if platform.system() != 'Windows':
1677
+ signal.alarm(0)
1678
+
1679
+ if response.status_code != 200:
1680
+ raise Exception(f"clienttoken request failed - status {response.status_code}\nHeaders: {response.headers}\nBody (raw): {response.content[:120]}...")
1681
+
1682
+ parsed = parse_protobuf_message(response.content)
1683
+ inner = parsed.get(2, {})
1684
+ client_token = deep_flatten(inner.get(1)) if inner.get(1) else None
1685
+ ttl = int(inner.get(3, 0)) or 1209600
1686
+
1687
+ if not client_token:
1688
+ raise Exception("clienttoken response did not contain a token")
1689
+
1690
+ SP_CACHED_CLIENT_TOKEN = client_token
1691
+ SP_CLIENT_TOKEN_EXPIRES_AT = time.time() + ttl
1692
+
1693
+ return client_token
1694
+
1695
+
1696
+ # Fetches Spotify access token with automatic client token refresh
1697
+ def spotify_get_access_token_from_client_auto(device_id, system_id, user_uri_id, refresh_token):
1698
+ client_token = None
1699
+
1700
+ if all([
1701
+ CLIENTTOKEN_URL,
1702
+ APP_VERSION,
1703
+ CPU_ARCH is not None and CPU_ARCH > 0,
1704
+ OS_BUILD is not None and OS_BUILD > 0,
1705
+ PLATFORM is not None and PLATFORM > 0,
1706
+ OS_MAJOR is not None and OS_MAJOR > 0,
1707
+ OS_MINOR is not None and OS_MINOR > 0,
1708
+ CLIENT_MODEL is not None and CLIENT_MODEL > 0
1709
+ ]):
1710
+ client_token = spotify_get_client_token(app_version=APP_VERSION, device_id=device_id, system_id=system_id, cpu_arch=CPU_ARCH, os_build=OS_BUILD, platform=PLATFORM, os_major=OS_MAJOR, os_minor=OS_MINOR, client_model=CLIENT_MODEL)
1711
+
1712
+ try:
1713
+ return spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refresh_token, client_token)
1714
+ except Exception as e:
1715
+ err = str(e).lower()
1716
+ if all([
1717
+ CLIENTTOKEN_URL,
1718
+ APP_VERSION,
1719
+ CPU_ARCH is not None and CPU_ARCH > 0,
1720
+ OS_BUILD is not None and OS_BUILD > 0,
1721
+ PLATFORM is not None and PLATFORM > 0,
1722
+ OS_MAJOR is not None and OS_MAJOR > 0,
1723
+ OS_MINOR is not None and OS_MINOR > 0,
1724
+ CLIENT_MODEL is not None and CLIENT_MODEL > 0
1725
+ ]) and ("invalid client token" in err or "expired client token" in err):
1726
+ global SP_CLIENT_TOKEN_EXPIRES_AT
1727
+ SP_CLIENT_TOKEN_EXPIRES_AT = 0
1728
+
1729
+ client_token = spotify_get_client_token(app_version=APP_VERSION, device_id=DEVICE_ID, system_id=SYSTEM_ID, cpu_arch=CPU_ARCH, os_build=OS_BUILD, platform=PLATFORM, os_major=OS_MAJOR, os_minor=OS_MINOR, client_model=CLIENT_MODEL)
1730
+
1731
+ return spotify_get_access_token_from_client(device_id, system_id, user_uri_id, refresh_token, client_token)
1732
+ raise
1733
+
1734
+
1735
+ # --------------------------------------------------------
1736
+
1737
+
1099
1738
  # Fetches list of Spotify friends
1100
1739
  def spotify_get_friends_json(access_token):
1101
1740
  url = "https://guc-spclient.spotify.com/presence-view/v1/buddylist"
1102
- headers = {
1103
- "Authorization": f"Bearer {access_token}",
1104
- "Client-Id": SP_CACHED_CLIENT_ID,
1105
- "User-Agent": SP_CACHED_USER_AGENT,
1106
- }
1741
+ headers = {"Authorization": f"Bearer {access_token}"}
1742
+
1743
+ if TOKEN_SOURCE == "cookie":
1744
+ headers.update({
1745
+ "Client-Id": SP_CACHED_CLIENT_ID,
1746
+ "User-Agent": SP_CACHED_USER_AGENT,
1747
+ })
1748
+ elif TOKEN_SOURCE == "client":
1749
+ headers.update({
1750
+ "User-Agent": USER_AGENT
1751
+ })
1107
1752
 
1108
1753
  response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1109
1754
  if response.status_code == 401:
@@ -1163,8 +1808,6 @@ def spotify_list_friends(friend_activity):
1163
1808
  sp_playlist_uri = friend["track"]["context"].get("uri")
1164
1809
  sp_track_uri = friend["track"].get("uri")
1165
1810
 
1166
- # if index > 0:
1167
- # print("─" * HORIZONTAL_LINE)
1168
1811
  print("─" * HORIZONTAL_LINE)
1169
1812
  print(f"Username:\t\t\t{sp_username}")
1170
1813
  print(f"User URI ID:\t\t\t{sp_uri}")
@@ -1226,11 +1869,17 @@ def spotify_get_friend_info(friend_activity, uri):
1226
1869
  def spotify_get_track_info(access_token, track_uri):
1227
1870
  track_id = track_uri.split(':', 2)[2]
1228
1871
  url = "https://api.spotify.com/v1/tracks/" + track_id
1229
- headers = {
1230
- "Authorization": f"Bearer {access_token}",
1231
- "Client-Id": SP_CACHED_CLIENT_ID,
1232
- "User-Agent": SP_CACHED_USER_AGENT,
1233
- }
1872
+ headers = {"Authorization": f"Bearer {access_token}"}
1873
+
1874
+ if TOKEN_SOURCE == "cookie":
1875
+ headers.update({
1876
+ "Client-Id": SP_CACHED_CLIENT_ID,
1877
+ "User-Agent": SP_CACHED_USER_AGENT,
1878
+ })
1879
+ elif TOKEN_SOURCE == "client":
1880
+ headers.update({
1881
+ "User-Agent": USER_AGENT
1882
+ })
1234
1883
  # add si parameter so link opens in native Spotify app after clicking
1235
1884
  si = "?si=1"
1236
1885
 
@@ -1254,11 +1903,17 @@ def spotify_get_track_info(access_token, track_uri):
1254
1903
  def spotify_get_playlist_info(access_token, playlist_uri):
1255
1904
  playlist_id = playlist_uri.split(':', 2)[2]
1256
1905
  url = f"https://api.spotify.com/v1/playlists/{playlist_id}?fields=name,owner,followers,external_urls"
1257
- headers = {
1258
- "Authorization": f"Bearer {access_token}",
1259
- "Client-Id": SP_CACHED_CLIENT_ID,
1260
- "User-Agent": SP_CACHED_USER_AGENT,
1261
- }
1906
+ headers = {"Authorization": f"Bearer {access_token}"}
1907
+
1908
+ if TOKEN_SOURCE == "cookie":
1909
+ headers.update({
1910
+ "Client-Id": SP_CACHED_CLIENT_ID,
1911
+ "User-Agent": SP_CACHED_USER_AGENT,
1912
+ })
1913
+ elif TOKEN_SOURCE == "client":
1914
+ headers.update({
1915
+ "User-Agent": USER_AGENT
1916
+ })
1262
1917
  # add si parameter so link opens in native Spotify app after clicking
1263
1918
  si = "?si=1"
1264
1919
 
@@ -1279,15 +1934,21 @@ def spotify_get_playlist_info(access_token, playlist_uri):
1279
1934
  # Gets basic information about access token owner
1280
1935
  def spotify_get_current_user(access_token) -> dict | None:
1281
1936
  url = "https://api.spotify.com/v1/me"
1282
- headers = {
1283
- "Authorization": f"Bearer {access_token}",
1284
- "Client-Id": SP_CACHED_CLIENT_ID,
1285
- "User-Agent": SP_CACHED_USER_AGENT,
1286
- }
1937
+ headers = {"Authorization": f"Bearer {access_token}"}
1938
+
1939
+ if TOKEN_SOURCE == "cookie":
1940
+ headers.update({
1941
+ "Client-Id": SP_CACHED_CLIENT_ID,
1942
+ "User-Agent": SP_CACHED_USER_AGENT,
1943
+ })
1944
+ elif TOKEN_SOURCE == "client":
1945
+ headers.update({
1946
+ "User-Agent": USER_AGENT
1947
+ })
1287
1948
 
1288
1949
  if platform.system() != 'Windows':
1289
1950
  signal.signal(signal.SIGALRM, timeout_handler)
1290
- signal.alarm(ALARM_TIMEOUT + 2)
1951
+ signal.alarm(FUNCTION_TIMEOUT + 2)
1291
1952
  try:
1292
1953
  response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
1293
1954
  response.raise_for_status()
@@ -1447,7 +2108,10 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1447
2108
  signal.signal(signal.SIGALRM, timeout_handler)
1448
2109
  signal.alarm(ALARM_TIMEOUT)
1449
2110
  try:
1450
- sp_accessToken = spotify_get_access_token(SP_DC_COOKIE)
2111
+ if TOKEN_SOURCE == "client":
2112
+ sp_accessToken = spotify_get_access_token_from_client_auto(DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN)
2113
+ else:
2114
+ sp_accessToken = spotify_get_access_token_from_sp_dc(SP_DC_COOKIE)
1451
2115
  sp_friends = spotify_get_friends_json(sp_accessToken)
1452
2116
  sp_found, sp_data = spotify_get_friend_info(sp_friends, user_uri_id)
1453
2117
  email_sent = False
@@ -1464,12 +2128,27 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1464
2128
  if platform.system() != 'Windows':
1465
2129
  signal.alarm(0)
1466
2130
 
2131
+ err = str(e).lower()
2132
+
1467
2133
  print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: {e}")
1468
2134
 
1469
- if "401" in str(e):
2135
+ if TOKEN_SOURCE == 'cookie' and '401' in err:
1470
2136
  SP_CACHED_ACCESS_TOKEN = None
1471
2137
 
1472
- if ('access token' in str(e)) or ('Unsuccessful token request' in str(e)):
2138
+ client_errs = ['access token', 'invalid client token', 'expired client token']
2139
+ cookie_errs = ['access token', 'unsuccessful token request']
2140
+
2141
+ if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
2142
+ print(f"* Error: client token or refresh token might have expired!")
2143
+ if ERROR_NOTIFICATION and not email_sent:
2144
+ m_subject = f"spotify_monitor: client token or refresh token might have expired! (uri: {user_uri_id})"
2145
+ m_body = f"Client token or refresh token might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2146
+ m_body_html = f"<html><head></head><body>Client token or refresh token might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
2147
+ print(f"Sending email notification to {RECEIVER_EMAIL}")
2148
+ send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2149
+ email_sent = True
2150
+
2151
+ elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
1473
2152
  print(f"* Error: sp_dc might have expired!")
1474
2153
  if ERROR_NOTIFICATION and not email_sent:
1475
2154
  m_subject = f"spotify_monitor: sp_dc might have expired! (uri: {user_uri_id})"
@@ -1478,6 +2157,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1478
2157
  print(f"Sending email notification to {RECEIVER_EMAIL}")
1479
2158
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
1480
2159
  email_sent = True
2160
+
1481
2161
  print_cur_ts("Timestamp:\t\t\t")
1482
2162
  time.sleep(SPOTIFY_ERROR_INTERVAL)
1483
2163
  continue
@@ -1494,7 +2174,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1494
2174
 
1495
2175
  user_info = spotify_get_current_user(sp_accessToken)
1496
2176
  if user_info:
1497
- print(f"Token belongs to:\t\t{user_info.get('display_name', '')}\n\t\t\t\t[ {user_info.get('spotify_url')} ]")
2177
+ print(f"Token belongs to:\t\t{user_info.get('display_name', '')} (via {TOKEN_SOURCE})\n\t\t\t\t[ {user_info.get('spotify_url')} ]")
1498
2178
 
1499
2179
  sp_track_uri = sp_data["sp_track_uri"]
1500
2180
  sp_track_uri_id = sp_data["sp_track_uri_id"]
@@ -1646,7 +2326,10 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1646
2326
  signal.signal(signal.SIGALRM, timeout_handler)
1647
2327
  signal.alarm(ALARM_TIMEOUT)
1648
2328
  try:
1649
- sp_accessToken = spotify_get_access_token(SP_DC_COOKIE)
2329
+ if TOKEN_SOURCE == "client":
2330
+ sp_accessToken = spotify_get_access_token_from_client_auto(DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN)
2331
+ else:
2332
+ sp_accessToken = spotify_get_access_token_from_sp_dc(SP_DC_COOKIE)
1650
2333
  sp_friends = spotify_get_friends_json(sp_accessToken)
1651
2334
  sp_found, sp_data = spotify_get_friend_info(sp_friends, user_uri_id)
1652
2335
  email_sent = False
@@ -1663,11 +2346,13 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1663
2346
  if platform.system() != 'Windows':
1664
2347
  signal.alarm(0)
1665
2348
 
1666
- if "401" in str(e):
2349
+ err = str(e).lower()
2350
+
2351
+ if TOKEN_SOURCE == 'cookie' and '401' in err:
1667
2352
  SP_CACHED_ACCESS_TOKEN = None
1668
2353
 
1669
2354
  str_matches = ["500 server", "504 server", "502 server", "503 server"]
1670
- if any(x in str(e).lower() for x in str_matches):
2355
+ if any(x in err for x in str_matches):
1671
2356
  if not error_500_start_ts:
1672
2357
  error_500_start_ts = int(time.time())
1673
2358
  error_500_counter = 1
@@ -1675,7 +2360,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1675
2360
  error_500_counter += 1
1676
2361
 
1677
2362
  str_matches = ["timed out", "timeout", "name resolution", "failed to resolve", "family not supported", "429 client", "aborted"]
1678
- if any(x in str(e).lower() for x in str_matches) or str(e) == '':
2363
+ if any(x in err for x in str_matches) or str(e) == '':
1679
2364
  if not error_network_issue_start_ts:
1680
2365
  error_network_issue_start_ts = int(time.time())
1681
2366
  error_network_issue_counter = 1
@@ -1696,7 +2381,21 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1696
2381
 
1697
2382
  elif not error_500_start_ts and not error_network_issue_start_ts:
1698
2383
  print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: '{e}'")
1699
- if ('access token' in str(e)) or ('Unsuccessful token request' in str(e)):
2384
+
2385
+ client_errs = ['access token', 'invalid client token', 'expired client token']
2386
+ cookie_errs = ['access token', 'unsuccessful token request']
2387
+
2388
+ if TOKEN_SOURCE == 'client' and any(k in err for k in client_errs):
2389
+ print(f"* Error: client token or refresh token might have expired!")
2390
+ if ERROR_NOTIFICATION and not email_sent:
2391
+ m_subject = f"spotify_monitor: client token or refresh token might have expired! (uri: {user_uri_id})"
2392
+ m_body = f"Client token or refresh token might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
2393
+ m_body_html = f"<html><head></head><body>Client token or refresh token might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
2394
+ print(f"Sending email notification to {RECEIVER_EMAIL}")
2395
+ send_email(m_subject, m_body, m_body_html, SMTP_SSL)
2396
+ email_sent = True
2397
+
2398
+ elif TOKEN_SOURCE == 'cookie' and any(k in err for k in cookie_errs):
1700
2399
  print(f"* Error: sp_dc might have expired!")
1701
2400
  if ERROR_NOTIFICATION and not email_sent:
1702
2401
  m_subject = f"spotify_monitor: sp_dc might have expired! (uri: {user_uri_id})"
@@ -1705,6 +2404,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
1705
2404
  print(f"Sending email notification to {RECEIVER_EMAIL}")
1706
2405
  send_email(m_subject, m_body, m_body_html, SMTP_SSL)
1707
2406
  email_sent = True
2407
+
1708
2408
  print_cur_ts("Timestamp:\t\t\t")
1709
2409
  time.sleep(SPOTIFY_ERROR_INTERVAL)
1710
2410
 
@@ -2036,7 +2736,7 @@ def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
2036
2736
 
2037
2737
 
2038
2738
  def main():
2039
- global CLI_CONFIG_PATH, DOTENV_FILE, LIVENESS_CHECK_COUNTER, 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
2739
+ 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
2040
2740
 
2041
2741
  if "--generate-config" in sys.argv:
2042
2742
  print(CONFIG_BLOCK.strip("\n"))
@@ -2065,7 +2765,8 @@ def main():
2065
2765
  "user_id",
2066
2766
  nargs="?",
2067
2767
  metavar="SPOTIFY_USER_URI_ID",
2068
- help="Spotify user URI ID"
2768
+ help="Spotify user URI ID",
2769
+ type=str
2069
2770
  )
2070
2771
 
2071
2772
  # Version, just to list in help, it is handled earlier
@@ -2095,15 +2796,40 @@ def main():
2095
2796
  help="Path to optional dotenv file (auto-search if not set, disable with 'none')",
2096
2797
  )
2097
2798
 
2098
- # API credentials
2099
- creds = parser.add_argument_group("API credentials")
2100
- creds.add_argument(
2799
+ # Token source
2800
+ parser.add_argument(
2801
+ "--token-source",
2802
+ dest="token_source",
2803
+ choices=["cookie", "client"],
2804
+ help="Method to obtain Spotify access token: 'cookie' (via sp_dc cookie) or 'client' (via desktop client login protobuf)"
2805
+ )
2806
+
2807
+ # Cookie token source credentials (used when token source is set to cookie)
2808
+ api_creds = parser.add_argument_group("Cookie token source credentials")
2809
+ api_creds.add_argument(
2101
2810
  "-u", "--spotify-dc-cookie",
2102
2811
  dest="spotify_dc_cookie",
2103
2812
  metavar="SP_DC_COOKIE",
2813
+ type=str,
2104
2814
  help="Spotify sp_dc cookie"
2105
2815
  )
2106
2816
 
2817
+ # Client token source credentials (used when token source is set to client)
2818
+ client_creds = parser.add_argument_group("Client token source credentials")
2819
+ client_creds.add_argument(
2820
+ "-w", "--login-request-body-file",
2821
+ dest="login_request_body_file",
2822
+ metavar="PROTOBUF_FILENAME",
2823
+ help="Read device_id, system_id, user_uri_id and refresh_token from binary Protobuf login file"
2824
+ )
2825
+
2826
+ client_creds.add_argument(
2827
+ "-z", "--clienttoken-request-body-file",
2828
+ dest="clienttoken_request_body_file",
2829
+ metavar="PROTOBUF_FILENAME",
2830
+ help="Read app_version, cpu_arch, os_build, platform, os_major, os_minor and client_model from binary Protobuf client token file"
2831
+ )
2832
+
2107
2833
  # Notifications
2108
2834
  notify = parser.add_argument_group("Notifications")
2109
2835
  notify.add_argument(
@@ -2146,7 +2872,7 @@ def main():
2146
2872
  dest="notify_errors",
2147
2873
  action="store_false",
2148
2874
  default=None,
2149
- help="Disable email on errors (e.g. expired sp_dc)"
2875
+ help="Disable emails on errors"
2150
2876
  )
2151
2877
  notify.add_argument(
2152
2878
  "--send-test-email",
@@ -2201,7 +2927,7 @@ def main():
2201
2927
  dest="track_in_spotify",
2202
2928
  action="store_true",
2203
2929
  default=None,
2204
- help="Autoplay each listened song in your Spotify client"
2930
+ help="Auto-play each listened song in your Spotify client"
2205
2931
  )
2206
2932
  opts.add_argument(
2207
2933
  "-b", "--csv-file",
@@ -2229,7 +2955,7 @@ def main():
2229
2955
  dest="disable_logging",
2230
2956
  action="store_true",
2231
2957
  default=None,
2232
- help="Disable logging to file spotify_monitor_<user_uri_id/file_suffix>.log"
2958
+ help="Disable logging to spotify_monitor_<user_uri_id/file_suffix>.log"
2233
2959
  )
2234
2960
 
2235
2961
  args = parser.parse_args()
@@ -2288,6 +3014,19 @@ def main():
2288
3014
  if val is not None:
2289
3015
  globals()[secret] = val
2290
3016
 
3017
+ if args.token_source:
3018
+ TOKEN_SOURCE = args.token_source
3019
+
3020
+ if not TOKEN_SOURCE:
3021
+ TOKEN_SOURCE = "cookie"
3022
+
3023
+ if TOKEN_SOURCE == "cookie":
3024
+ ALARM_TIMEOUT = int((TOKEN_MAX_RETRIES * TOKEN_RETRY_TIMEOUT) + 5)
3025
+ try:
3026
+ import pyotp
3027
+ except ModuleNotFoundError:
3028
+ raise SystemExit("Error: Couldn't find the pyotp library !\n\nTo install it, run:\n pip3 install pyotp\n\nOnce installed, re-run this tool")
3029
+
2291
3030
  if not check_internet():
2292
3031
  sys.exit(1)
2293
3032
 
@@ -2309,21 +3048,112 @@ def main():
2309
3048
  if args.disappeared_timer:
2310
3049
  SPOTIFY_DISAPPEARED_CHECK_INTERVAL = args.disappeared_timer
2311
3050
 
2312
- if args.spotify_dc_cookie:
2313
- SP_DC_COOKIE = args.spotify_dc_cookie
3051
+ if TOKEN_SOURCE == "client":
3052
+ login_request_body_file_param = False
3053
+ if args.login_request_body_file:
3054
+ LOGIN_REQUEST_BODY_FILE = os.path.expanduser(args.login_request_body_file)
3055
+ login_request_body_file_param = True
3056
+ else:
3057
+ if LOGIN_REQUEST_BODY_FILE:
3058
+ LOGIN_REQUEST_BODY_FILE = os.path.expanduser(LOGIN_REQUEST_BODY_FILE)
2314
3059
 
2315
- if not SP_DC_COOKIE or SP_DC_COOKIE == "your_sp_dc_cookie_value":
2316
- print("* Error: SP_DC_COOKIE (-u / --spotify-dc-cookie) value is empty or incorrect")
2317
- sys.exit(1)
3060
+ if LOGIN_REQUEST_BODY_FILE:
3061
+ if os.path.isfile(LOGIN_REQUEST_BODY_FILE):
3062
+ try:
3063
+ DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN = parse_request_body_file(LOGIN_REQUEST_BODY_FILE)
3064
+ except Exception as e:
3065
+ print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) cannot be processed: {e}")
3066
+ sys.exit(1)
3067
+ else:
3068
+ if not args.user_id and not args.list_friends and not args.show_user_info and login_request_body_file_param:
3069
+ print(f"* Login data correctly read from Protobuf file ({LOGIN_REQUEST_BODY_FILE}):")
3070
+ print(" - Device ID:\t\t", DEVICE_ID)
3071
+ print(" - System ID:\t\t", SYSTEM_ID)
3072
+ print(" - User URI ID:\t\t", USER_URI_ID)
3073
+ print(" - Refresh Token:\t", REFRESH_TOKEN, "\n")
3074
+ sys.exit(0)
3075
+ else:
3076
+ print(f"* Error: Protobuf file ({LOGIN_REQUEST_BODY_FILE}) does not exist")
3077
+ sys.exit(1)
3078
+
3079
+ if not USER_AGENT:
3080
+ USER_AGENT = get_random_spotify_user_agent()
3081
+
3082
+ if any([
3083
+ not LOGIN_URL,
3084
+ not USER_AGENT,
3085
+ not DEVICE_ID,
3086
+ DEVICE_ID == "your_spotify_app_device_id",
3087
+ not SYSTEM_ID,
3088
+ SYSTEM_ID == "your_spotify_app_system_id",
3089
+ not USER_URI_ID,
3090
+ USER_URI_ID == "your_spotify_user_uri_id",
3091
+ not REFRESH_TOKEN,
3092
+ REFRESH_TOKEN == "your_spotify_app_refresh_token",
3093
+ ]):
3094
+ print("* Error: Some login values are empty or incorrect")
3095
+ sys.exit(1)
3096
+
3097
+ clienttoken_request_body_file_param = False
3098
+ if args.clienttoken_request_body_file:
3099
+ CLIENTTOKEN_REQUEST_BODY_FILE = os.path.expanduser(args.clienttoken_request_body_file)
3100
+ clienttoken_request_body_file_param = True
3101
+ else:
3102
+ if CLIENTTOKEN_REQUEST_BODY_FILE:
3103
+ CLIENTTOKEN_REQUEST_BODY_FILE = os.path.expanduser(CLIENTTOKEN_REQUEST_BODY_FILE)
3104
+
3105
+ if CLIENTTOKEN_REQUEST_BODY_FILE:
3106
+ if os.path.isfile(CLIENTTOKEN_REQUEST_BODY_FILE):
3107
+ try:
3108
+
3109
+ (APP_VERSION, _, _, CPU_ARCH, OS_BUILD, PLATFORM, OS_MAJOR, OS_MINOR, CLIENT_MODEL) = parse_clienttoken_request_body_file(CLIENTTOKEN_REQUEST_BODY_FILE)
3110
+ except Exception as e:
3111
+ print(f"* Error: Protobuf file ({CLIENTTOKEN_REQUEST_BODY_FILE}) cannot be processed: {e}")
3112
+ sys.exit(1)
3113
+ else:
3114
+ if not args.user_id and not args.list_friends and not args.show_user_info and clienttoken_request_body_file_param:
3115
+ print(f"* Client token data correctly read from Protobuf file ({CLIENTTOKEN_REQUEST_BODY_FILE}):")
3116
+ print(" - App version:\t\t", APP_VERSION)
3117
+ print(" - CPU arch:\t\t", CPU_ARCH)
3118
+ print(" - OS build:\t\t", OS_BUILD)
3119
+ print(" - Platform:\t\t", PLATFORM)
3120
+ print(" - OS major:\t\t", OS_MAJOR)
3121
+ print(" - OS minor:\t\t", OS_MINOR)
3122
+ print(" - Client model:\t", CLIENT_MODEL)
3123
+ sys.exit(0)
3124
+ else:
3125
+ print(f"* Error: Protobuf file ({CLIENTTOKEN_REQUEST_BODY_FILE}) does not exist")
3126
+ sys.exit(1)
3127
+
3128
+ app_version_default = "1.2.62.580.g7e3d9a4f"
3129
+ if USER_AGENT and not APP_VERSION:
3130
+ try:
3131
+ APP_VERSION = ua_to_app_version(USER_AGENT)
3132
+ except Exception as e:
3133
+ print(f"Warning: wrong USER_AGENT defined, reverting to the default one: {e}")
3134
+ APP_VERSION = app_version_default
3135
+ else:
3136
+ APP_VERSION = app_version_default
3137
+
3138
+ else:
3139
+ if args.spotify_dc_cookie:
3140
+ SP_DC_COOKIE = args.spotify_dc_cookie
3141
+
3142
+ if not SP_DC_COOKIE or SP_DC_COOKIE == "your_sp_dc_cookie_value":
3143
+ print("* Error: SP_DC_COOKIE (-u / --spotify_dc_cookie) value is empty or incorrect")
3144
+ sys.exit(1)
2318
3145
 
2319
3146
  if args.show_user_info:
2320
3147
  print("* Getting basic information about access token owner ...\n")
2321
3148
  try:
2322
- accessToken = spotify_get_access_token(SP_DC_COOKIE)
2323
- user_info = spotify_get_current_user(accessToken)
3149
+ if TOKEN_SOURCE == "client":
3150
+ sp_accessToken = spotify_get_access_token_from_client_auto(DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN)
3151
+ else:
3152
+ sp_accessToken = spotify_get_access_token_from_sp_dc(SP_DC_COOKIE)
3153
+ user_info = spotify_get_current_user(sp_accessToken)
2324
3154
 
2325
3155
  if user_info:
2326
- print(f"Token belongs to:\n")
3156
+ print(f"Token fetched via {TOKEN_SOURCE} belongs to:\n")
2327
3157
 
2328
3158
  print(f"Username:\t\t{user_info.get('display_name', '')}")
2329
3159
  print(f"User URI ID:\t\t{user_info.get('uri', '').split('spotify:user:', 1)[1]}")
@@ -2343,11 +3173,14 @@ def main():
2343
3173
  if args.list_friends:
2344
3174
  print("* Listing Spotify friends ...\n")
2345
3175
  try:
2346
- accessToken = spotify_get_access_token(SP_DC_COOKIE)
2347
- user_info = spotify_get_current_user(accessToken)
3176
+ if TOKEN_SOURCE == "client":
3177
+ sp_accessToken = spotify_get_access_token_from_client_auto(DEVICE_ID, SYSTEM_ID, USER_URI_ID, REFRESH_TOKEN)
3178
+ else:
3179
+ sp_accessToken = spotify_get_access_token_from_sp_dc(SP_DC_COOKIE)
3180
+ user_info = spotify_get_current_user(sp_accessToken)
2348
3181
  if user_info:
2349
- print(f"Token belongs to:\t\t{user_info.get('display_name', '')}\n\t\t\t\t[ {user_info.get('spotify_url')} ]")
2350
- sp_friends = spotify_get_friends_json(accessToken)
3182
+ print(f"Token belongs to:\t\t{user_info.get('display_name', '')} (via {TOKEN_SOURCE})\n\t\t\t\t[ {user_info.get('spotify_url')} ]")
3183
+ sp_friends = spotify_get_friends_json(sp_accessToken)
2351
3184
  spotify_list_friends(sp_friends)
2352
3185
  print("─" * HORIZONTAL_LINE)
2353
3186
  except Exception as e:
@@ -2453,6 +3286,7 @@ def main():
2453
3286
 
2454
3287
  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)}]")
2455
3288
  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}]")
3289
+ print(f"* Token source:\t\t\t{TOKEN_SOURCE}")
2456
3290
  print(f"* Track listened songs:\t\t{TRACK_SONGS}")
2457
3291
  print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
2458
3292
  print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))