spotify-monitor 2.0rc1__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.0rc1.dist-info/METADATA +503 -0
- spotify_monitor-2.0rc1.dist-info/RECORD +7 -0
- spotify_monitor-2.0rc1.dist-info/WHEEL +5 -0
- spotify_monitor-2.0rc1.dist-info/entry_points.txt +2 -0
- spotify_monitor-2.0rc1.dist-info/licenses/LICENSE +674 -0
- spotify_monitor-2.0rc1.dist-info/top_level.txt +1 -0
- spotify_monitor.py +2478 -0
spotify_monitor.py
ADDED
|
@@ -0,0 +1,2478 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Author: Michal Szymanski <misiektoja-github@rm-rf.ninja>
|
|
4
|
+
v2.0
|
|
5
|
+
|
|
6
|
+
Tool implementing real-time tracking of Spotify friends music activity:
|
|
7
|
+
https://github.com/misiektoja/spotify_monitor/
|
|
8
|
+
|
|
9
|
+
Python pip3 requirements:
|
|
10
|
+
|
|
11
|
+
requests
|
|
12
|
+
python-dateutil
|
|
13
|
+
urllib3
|
|
14
|
+
pyotp
|
|
15
|
+
python-dotenv (optional)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
VERSION = "2.0"
|
|
19
|
+
|
|
20
|
+
# ---------------------------
|
|
21
|
+
# CONFIGURATION SECTION START
|
|
22
|
+
# ---------------------------
|
|
23
|
+
|
|
24
|
+
CONFIG_BLOCK = """
|
|
25
|
+
# Log in to Spotify web client (https://open.spotify.com/) and retrieve your sp_dc cookie
|
|
26
|
+
# 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
|
+
#
|
|
29
|
+
# Provide the SP_DC_COOKIE secret using one of the following methods:
|
|
30
|
+
# - Pass it at runtime with -u / --spotify-dc-cookie
|
|
31
|
+
# - Set it as an environment variable (e.g. export SP_DC_COOKIE=...)
|
|
32
|
+
# - Add it to ".env" file (SP_DC_COOKIE=...) for persistent use
|
|
33
|
+
# Fallback:
|
|
34
|
+
# - Hard-code it in the code or config file
|
|
35
|
+
SP_DC_COOKIE = "your_sp_dc_cookie_value"
|
|
36
|
+
|
|
37
|
+
# SMTP settings for sending email notifications
|
|
38
|
+
# If left as-is, no notifications will be sent
|
|
39
|
+
#
|
|
40
|
+
# Provide the SMTP_PASSWORD secret using one of the following methods:
|
|
41
|
+
# - Set it as an environment variable (e.g. export SMTP_PASSWORD=...)
|
|
42
|
+
# - Add it to ".env" file (SMTP_PASSWORD=...) for persistent use
|
|
43
|
+
# Fallback:
|
|
44
|
+
# - Hard-code it in the code or config file
|
|
45
|
+
SMTP_HOST = "your_smtp_server_ssl"
|
|
46
|
+
SMTP_PORT = 587
|
|
47
|
+
SMTP_USER = "your_smtp_user"
|
|
48
|
+
SMTP_PASSWORD = "your_smtp_password"
|
|
49
|
+
SMTP_SSL = True
|
|
50
|
+
SENDER_EMAIL = "your_sender_email"
|
|
51
|
+
RECEIVER_EMAIL = "your_receiver_email"
|
|
52
|
+
|
|
53
|
+
# Whether to send an email when user becomes active
|
|
54
|
+
# Can also be enabled via the -a flag
|
|
55
|
+
ACTIVE_NOTIFICATION = False
|
|
56
|
+
|
|
57
|
+
# Whether to send an email when user goes inactive
|
|
58
|
+
# Can also be enabled via the -i flag
|
|
59
|
+
INACTIVE_NOTIFICATION = False
|
|
60
|
+
|
|
61
|
+
# Whether to send an email when a monitored track/playlist/album plays
|
|
62
|
+
# Can also be enabled via the -t flag
|
|
63
|
+
TRACK_NOTIFICATION = False
|
|
64
|
+
|
|
65
|
+
# Whether to send an email on every song change
|
|
66
|
+
# Can also be enabled via the -j flag
|
|
67
|
+
SONG_NOTIFICATION = False
|
|
68
|
+
|
|
69
|
+
# Whether to send an email when user plays a song on loop
|
|
70
|
+
# Triggered if the same song is played more than SONG_ON_LOOP_VALUE times
|
|
71
|
+
# Can also be enabled via the -x flag
|
|
72
|
+
SONG_ON_LOOP_NOTIFICATION = False
|
|
73
|
+
|
|
74
|
+
# Whether to send an email on errors
|
|
75
|
+
# Can also be disabled via the -e flag
|
|
76
|
+
ERROR_NOTIFICATION = True
|
|
77
|
+
|
|
78
|
+
# How often to check for user activity; in seconds
|
|
79
|
+
# Can also be set using the -c flag
|
|
80
|
+
SPOTIFY_CHECK_INTERVAL = 30 # 30 seconds
|
|
81
|
+
|
|
82
|
+
# Time to wait before retrying after an error; in seconds
|
|
83
|
+
SPOTIFY_ERROR_INTERVAL = 180 # 3 mins
|
|
84
|
+
|
|
85
|
+
# Time after which a user is considered inactive (based on last activity); in seconds
|
|
86
|
+
# Can also be set using the -o flag
|
|
87
|
+
# Note: If the user listens to songs longer than this value, they may be marked as inactive
|
|
88
|
+
SPOTIFY_INACTIVITY_CHECK = 660 # 11 mins
|
|
89
|
+
|
|
90
|
+
# Interval for checking if a user who disappeared from the list of recently active friends has reappeared; in seconds
|
|
91
|
+
# Can happen due to:
|
|
92
|
+
# - unfollowing the user
|
|
93
|
+
# - Spotify service issues
|
|
94
|
+
# - private session bugs
|
|
95
|
+
# - user inactivity for over a week
|
|
96
|
+
# In such a case, the tool will continuously check for the user's reappearance using the time interval specified below
|
|
97
|
+
# Can also be set using the -m flag
|
|
98
|
+
SPOTIFY_DISAPPEARED_CHECK_INTERVAL = 180 # 3 mins
|
|
99
|
+
|
|
100
|
+
# Whether to auto‑play each listened song in your Spotify client
|
|
101
|
+
# Can also be set using the -g flag
|
|
102
|
+
TRACK_SONGS = False
|
|
103
|
+
|
|
104
|
+
# Method used to play the song listened by the tracked user in local Spotify client under macOS
|
|
105
|
+
# (i.e. when TRACK_SONGS / -g functionality is enabled)
|
|
106
|
+
# Methods:
|
|
107
|
+
# "apple-script" (recommended)
|
|
108
|
+
# "trigger-url"
|
|
109
|
+
SPOTIFY_MACOS_PLAYING_METHOD = "apple-script"
|
|
110
|
+
|
|
111
|
+
# Method used to play the song listened by the tracked user in local Spotify client under Linux OS
|
|
112
|
+
# (i.e. when TRACK_SONGS / -g functionality is enabled)
|
|
113
|
+
# Methods:
|
|
114
|
+
# "dbus-send" (most common one)
|
|
115
|
+
# "qdbus"
|
|
116
|
+
# "trigger-url"
|
|
117
|
+
SPOTIFY_LINUX_PLAYING_METHOD = "dbus-send"
|
|
118
|
+
|
|
119
|
+
# Method used to play the song listened by the tracked user in local Spotify client under Windows OS
|
|
120
|
+
# (if TRACK_SONGS / -g functionality is enabled)
|
|
121
|
+
# Methods:
|
|
122
|
+
# "start-uri" (recommended)
|
|
123
|
+
# "spotify-cmd"
|
|
124
|
+
# "trigger-url"
|
|
125
|
+
SPOTIFY_WINDOWS_PLAYING_METHOD = "start-uri"
|
|
126
|
+
|
|
127
|
+
# Number of consecutive plays of the same song considered to be on loop
|
|
128
|
+
SONG_ON_LOOP_VALUE = 3
|
|
129
|
+
|
|
130
|
+
# Threshold for considering a song as skipped (fraction of duration)
|
|
131
|
+
SKIPPED_SONG_THRESHOLD = 0.55 # song is treated as skipped if played for <= 55% of its total length
|
|
132
|
+
|
|
133
|
+
# Spotify track ID to play when the user goes offline (used when TRACK_SONGS / -g functionality is enabled)
|
|
134
|
+
# Leave empty to simply pause
|
|
135
|
+
# SP_USER_GOT_OFFLINE_TRACK_ID = "5wCjNjnugSUqGDBrmQhn0e"
|
|
136
|
+
SP_USER_GOT_OFFLINE_TRACK_ID = ""
|
|
137
|
+
|
|
138
|
+
# Delay before pausing the above track after the user goes offline; in seconds
|
|
139
|
+
# Set to 0 to keep playing indefinitely until manually paused
|
|
140
|
+
SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 5 # 5 seconds
|
|
141
|
+
|
|
142
|
+
# How often to print a "liveness check" message to the output; in seconds
|
|
143
|
+
# Set to 0 to disable
|
|
144
|
+
LIVENESS_CHECK_INTERVAL = 43200 # 12 hours
|
|
145
|
+
|
|
146
|
+
# URL used to verify internet connectivity at startup
|
|
147
|
+
CHECK_INTERNET_URL = 'https://api.spotify.com/v1'
|
|
148
|
+
|
|
149
|
+
# Timeout used when checking initial internet connectivity; in seconds
|
|
150
|
+
CHECK_INTERNET_TIMEOUT = 5
|
|
151
|
+
|
|
152
|
+
# Whether to enable / disable SSL certificate verification while sending https requests
|
|
153
|
+
VERIFY_SSL = True
|
|
154
|
+
|
|
155
|
+
# Threshold for displaying Spotify 50x errors - it is to suppress sporadic issues with Spotify API endpoint
|
|
156
|
+
# Adjust the values according to the SPOTIFY_CHECK_INTERVAL timer
|
|
157
|
+
# If more than 6 Spotify API related errors in 4 minutes, show an alert
|
|
158
|
+
ERROR_500_NUMBER_LIMIT = 6
|
|
159
|
+
ERROR_500_TIME_LIMIT = 240 # 4 min
|
|
160
|
+
|
|
161
|
+
# Threshold for displaying network errors - it is to suppress sporadic issues with internet connectivity
|
|
162
|
+
# Adjust the values according to the SPOTIFY_CHECK_INTERVAL timer
|
|
163
|
+
# If more than 6 network related errors in 4 minutes, show an alert
|
|
164
|
+
ERROR_NETWORK_ISSUES_NUMBER_LIMIT = 6
|
|
165
|
+
ERROR_NETWORK_ISSUES_TIME_LIMIT = 240 # 4 min
|
|
166
|
+
|
|
167
|
+
# CSV file to write every listened track
|
|
168
|
+
# Can also be set using the -b flag
|
|
169
|
+
CSV_FILE = ""
|
|
170
|
+
|
|
171
|
+
# Filename with Spotify tracks/playlists/albums to alert on
|
|
172
|
+
# Can also be set using the -s flag
|
|
173
|
+
MONITOR_LIST_FILE = ""
|
|
174
|
+
|
|
175
|
+
# Location of the optional dotenv file which can keep secrets
|
|
176
|
+
# If not specified it will try to auto-search for .env files
|
|
177
|
+
# To disable auto-search, set this to the literal string "none"
|
|
178
|
+
# Can also be set using the --env-file flag
|
|
179
|
+
DOTENV_FILE = ""
|
|
180
|
+
|
|
181
|
+
# Suffix to append to the output filenames instead of default user URI ID
|
|
182
|
+
# Can also be set using the -y flag
|
|
183
|
+
FILE_SUFFIX = ""
|
|
184
|
+
|
|
185
|
+
# Base name for the log file. Output will be saved to spotify_monitor_<user_uri_id/file_suffix>.log
|
|
186
|
+
# Can include a directory path to specify the location, e.g. ~/some_dir/spotify_monitor
|
|
187
|
+
SP_LOGFILE = "spotify_monitor"
|
|
188
|
+
|
|
189
|
+
# Whether to disable logging to spotify_monitor_<user_uri_id/file_suffix>.log
|
|
190
|
+
# Can also be disabled via the -d flag
|
|
191
|
+
DISABLE_LOGGING = False
|
|
192
|
+
|
|
193
|
+
# Width of horizontal line (─)
|
|
194
|
+
HORIZONTAL_LINE = 113
|
|
195
|
+
|
|
196
|
+
# Whether to clear the terminal screen after starting the tool
|
|
197
|
+
CLEAR_SCREEN = True
|
|
198
|
+
|
|
199
|
+
# Value added/subtracted via signal handlers to adjust inactivity timeout (SPOTIFY_INACTIVITY_CHECK); in seconds
|
|
200
|
+
SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 30 # 30 seconds
|
|
201
|
+
|
|
202
|
+
# Maximum number of attempts to get a valid access token in a single run of the spotify_get_access_token() function
|
|
203
|
+
TOKEN_MAX_RETRIES = 10
|
|
204
|
+
|
|
205
|
+
# Interval between access token retry attempts; in seconds
|
|
206
|
+
TOKEN_RETRY_TIMEOUT = 0.5 # 0.5 second
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# -------------------------
|
|
210
|
+
# CONFIGURATION SECTION END
|
|
211
|
+
# -------------------------
|
|
212
|
+
|
|
213
|
+
# Default dummy values so linters shut up
|
|
214
|
+
# Do not change values below - modify them in the configuration section or config file instead
|
|
215
|
+
SP_DC_COOKIE = ""
|
|
216
|
+
SMTP_HOST = ""
|
|
217
|
+
SMTP_PORT = 0
|
|
218
|
+
SMTP_USER = ""
|
|
219
|
+
SMTP_PASSWORD = ""
|
|
220
|
+
SMTP_SSL = False
|
|
221
|
+
SENDER_EMAIL = ""
|
|
222
|
+
RECEIVER_EMAIL = ""
|
|
223
|
+
ACTIVE_NOTIFICATION = False
|
|
224
|
+
INACTIVE_NOTIFICATION = False
|
|
225
|
+
TRACK_NOTIFICATION = False
|
|
226
|
+
SONG_NOTIFICATION = False
|
|
227
|
+
SONG_ON_LOOP_NOTIFICATION = False
|
|
228
|
+
ERROR_NOTIFICATION = False
|
|
229
|
+
SPOTIFY_CHECK_INTERVAL = 0
|
|
230
|
+
SPOTIFY_ERROR_INTERVAL = 0
|
|
231
|
+
SPOTIFY_INACTIVITY_CHECK = 0
|
|
232
|
+
SPOTIFY_DISAPPEARED_CHECK_INTERVAL = 0
|
|
233
|
+
TRACK_SONGS = False
|
|
234
|
+
SPOTIFY_MACOS_PLAYING_METHOD = ""
|
|
235
|
+
SPOTIFY_LINUX_PLAYING_METHOD = ""
|
|
236
|
+
SPOTIFY_WINDOWS_PLAYING_METHOD = ""
|
|
237
|
+
SONG_ON_LOOP_VALUE = 0
|
|
238
|
+
SKIPPED_SONG_THRESHOLD = 0
|
|
239
|
+
SP_USER_GOT_OFFLINE_TRACK_ID = ""
|
|
240
|
+
SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE = 0
|
|
241
|
+
LIVENESS_CHECK_INTERVAL = 0
|
|
242
|
+
CHECK_INTERNET_URL = ""
|
|
243
|
+
CHECK_INTERNET_TIMEOUT = 0
|
|
244
|
+
VERIFY_SSL = False
|
|
245
|
+
ERROR_500_NUMBER_LIMIT = 0
|
|
246
|
+
ERROR_500_TIME_LIMIT = 0
|
|
247
|
+
ERROR_NETWORK_ISSUES_NUMBER_LIMIT = 0
|
|
248
|
+
ERROR_NETWORK_ISSUES_TIME_LIMIT = 0
|
|
249
|
+
CSV_FILE = ""
|
|
250
|
+
MONITOR_LIST_FILE = ""
|
|
251
|
+
DOTENV_FILE = ""
|
|
252
|
+
FILE_SUFFIX = ""
|
|
253
|
+
SP_LOGFILE = ""
|
|
254
|
+
DISABLE_LOGGING = False
|
|
255
|
+
HORIZONTAL_LINE = 0
|
|
256
|
+
CLEAR_SCREEN = False
|
|
257
|
+
SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE = 0
|
|
258
|
+
TOKEN_MAX_RETRIES = 0
|
|
259
|
+
TOKEN_RETRY_TIMEOUT = 0.0
|
|
260
|
+
|
|
261
|
+
exec(CONFIG_BLOCK, globals())
|
|
262
|
+
|
|
263
|
+
# Default name for the optional config file
|
|
264
|
+
DEFAULT_CONFIG_FILENAME = "spotify_monitor.conf"
|
|
265
|
+
|
|
266
|
+
# List of secret keys to load from env/config
|
|
267
|
+
SECRET_KEYS = ("SP_DC_COOKIE", "SMTP_PASSWORD")
|
|
268
|
+
|
|
269
|
+
# Strings removed from track names for generating proper Genius search URLs
|
|
270
|
+
re_search_str = r'remaster|extended|original mix|remix|original soundtrack|radio( |-)edit|\(feat\.|( \(.*version\))|( - .*version)'
|
|
271
|
+
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
|
+
|
|
273
|
+
# Default value for network-related timeouts in functions
|
|
274
|
+
FUNCTION_TIMEOUT = 15
|
|
275
|
+
|
|
276
|
+
# Variables for caching functionality of the Spotify access token to avoid unnecessary refreshing
|
|
277
|
+
SP_CACHED_ACCESS_TOKEN = None
|
|
278
|
+
SP_TOKEN_EXPIRES_AT = 0
|
|
279
|
+
SP_CACHED_CLIENT_ID = ""
|
|
280
|
+
SP_CACHED_USER_AGENT = ""
|
|
281
|
+
|
|
282
|
+
# URL of the Spotify Web Player endpoint to get access token
|
|
283
|
+
TOKEN_URL = "https://open.spotify.com/get_access_token"
|
|
284
|
+
|
|
285
|
+
# URL of the endpoint to get server time needed to create TOTP object
|
|
286
|
+
SERVER_TIME_URL = "https://open.spotify.com/server-time"
|
|
287
|
+
|
|
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
|
|
291
|
+
|
|
292
|
+
LIVENESS_CHECK_COUNTER = LIVENESS_CHECK_INTERVAL / SPOTIFY_CHECK_INTERVAL
|
|
293
|
+
|
|
294
|
+
stdout_bck = None
|
|
295
|
+
csvfieldnames = ['Date', 'Artist', 'Track', 'Playlist', 'Album', 'Last activity']
|
|
296
|
+
|
|
297
|
+
CLI_CONFIG_PATH = None
|
|
298
|
+
|
|
299
|
+
# to solve the issue: 'SyntaxError: f-string expression part cannot include a backslash'
|
|
300
|
+
nl_ch = "\n"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
import sys
|
|
304
|
+
|
|
305
|
+
if sys.version_info < (3, 6):
|
|
306
|
+
print("* Error: Python version 3.6 or higher required !")
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
import time
|
|
310
|
+
from time import time_ns
|
|
311
|
+
import string
|
|
312
|
+
import json
|
|
313
|
+
import os
|
|
314
|
+
from datetime import datetime
|
|
315
|
+
from dateutil import relativedelta
|
|
316
|
+
import calendar
|
|
317
|
+
import requests as req
|
|
318
|
+
import signal
|
|
319
|
+
import smtplib
|
|
320
|
+
import ssl
|
|
321
|
+
from email.header import Header
|
|
322
|
+
from email.mime.multipart import MIMEMultipart
|
|
323
|
+
from email.mime.text import MIMEText
|
|
324
|
+
import argparse
|
|
325
|
+
import csv
|
|
326
|
+
from urllib.parse import quote_plus, quote
|
|
327
|
+
import subprocess
|
|
328
|
+
import platform
|
|
329
|
+
import re
|
|
330
|
+
import ipaddress
|
|
331
|
+
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
|
+
import base64
|
|
337
|
+
import random
|
|
338
|
+
import shutil
|
|
339
|
+
from pathlib import Path
|
|
340
|
+
|
|
341
|
+
import urllib3
|
|
342
|
+
if not VERIFY_SSL:
|
|
343
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
344
|
+
|
|
345
|
+
SESSION = req.Session()
|
|
346
|
+
|
|
347
|
+
from requests.adapters import HTTPAdapter
|
|
348
|
+
from urllib3.util.retry import Retry
|
|
349
|
+
|
|
350
|
+
retry = Retry(
|
|
351
|
+
total=5,
|
|
352
|
+
connect=3,
|
|
353
|
+
read=3,
|
|
354
|
+
backoff_factor=1,
|
|
355
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
356
|
+
allowed_methods=["GET", "HEAD", "OPTIONS"],
|
|
357
|
+
raise_on_status=False
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
adapter = HTTPAdapter(max_retries=retry, pool_connections=100, pool_maxsize=100)
|
|
361
|
+
SESSION.mount("https://", adapter)
|
|
362
|
+
SESSION.mount("http://", adapter)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# Logger class to output messages to stdout and log file
|
|
366
|
+
class Logger(object):
|
|
367
|
+
def __init__(self, filename):
|
|
368
|
+
self.terminal = sys.stdout
|
|
369
|
+
self.logfile = open(filename, "a", buffering=1, encoding="utf-8")
|
|
370
|
+
|
|
371
|
+
def write(self, message):
|
|
372
|
+
self.terminal.write(message)
|
|
373
|
+
self.logfile.write(message)
|
|
374
|
+
self.terminal.flush()
|
|
375
|
+
self.logfile.flush()
|
|
376
|
+
|
|
377
|
+
def flush(self):
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# Class used to generate timeout exceptions
|
|
382
|
+
class TimeoutException(Exception):
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# Signal handler for SIGALRM when the operation times out
|
|
387
|
+
def timeout_handler(sig, frame):
|
|
388
|
+
raise TimeoutException
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# Signal handler when user presses Ctrl+C
|
|
392
|
+
def signal_handler(sig, frame):
|
|
393
|
+
sys.stdout = stdout_bck
|
|
394
|
+
print('\n* You pressed Ctrl+C, tool is terminated.')
|
|
395
|
+
sys.exit(0)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# Checks internet connectivity
|
|
399
|
+
def check_internet(url=CHECK_INTERNET_URL, timeout=CHECK_INTERNET_TIMEOUT, verify=VERIFY_SSL):
|
|
400
|
+
try:
|
|
401
|
+
_ = req.get(url, timeout=timeout, verify=verify)
|
|
402
|
+
return True
|
|
403
|
+
except req.RequestException as e:
|
|
404
|
+
print(f"* No connectivity, please check your network:\n\n{e}")
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Clears the terminal screen
|
|
409
|
+
def clear_screen(enabled=True):
|
|
410
|
+
if not enabled:
|
|
411
|
+
return
|
|
412
|
+
try:
|
|
413
|
+
if platform.system() == 'Windows':
|
|
414
|
+
os.system('cls')
|
|
415
|
+
else:
|
|
416
|
+
os.system('clear')
|
|
417
|
+
except Exception:
|
|
418
|
+
print("* Cannot clear the screen contents")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# Converts absolute value of seconds to human readable format
|
|
422
|
+
def display_time(seconds, granularity=2):
|
|
423
|
+
intervals = (
|
|
424
|
+
('years', 31556952), # approximation
|
|
425
|
+
('months', 2629746), # approximation
|
|
426
|
+
('weeks', 604800), # 60 * 60 * 24 * 7
|
|
427
|
+
('days', 86400), # 60 * 60 * 24
|
|
428
|
+
('hours', 3600), # 60 * 60
|
|
429
|
+
('minutes', 60),
|
|
430
|
+
('seconds', 1),
|
|
431
|
+
)
|
|
432
|
+
result = []
|
|
433
|
+
|
|
434
|
+
if seconds > 0:
|
|
435
|
+
for name, count in intervals:
|
|
436
|
+
value = seconds // count
|
|
437
|
+
if value:
|
|
438
|
+
seconds -= value * count
|
|
439
|
+
if value == 1:
|
|
440
|
+
name = name.rstrip('s')
|
|
441
|
+
result.append(f"{value} {name}")
|
|
442
|
+
return ', '.join(result[:granularity])
|
|
443
|
+
else:
|
|
444
|
+
return '0 seconds'
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Calculates time span between two timestamps, accepts timestamp integers, floats and datetime objects
|
|
448
|
+
def calculate_timespan(timestamp1, timestamp2, show_weeks=True, show_hours=True, show_minutes=True, show_seconds=True, granularity=3):
|
|
449
|
+
result = []
|
|
450
|
+
intervals = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds']
|
|
451
|
+
ts1 = timestamp1
|
|
452
|
+
ts2 = timestamp2
|
|
453
|
+
|
|
454
|
+
if type(timestamp1) is int:
|
|
455
|
+
dt1 = datetime.fromtimestamp(int(ts1))
|
|
456
|
+
elif type(timestamp1) is float:
|
|
457
|
+
ts1 = int(round(ts1))
|
|
458
|
+
dt1 = datetime.fromtimestamp(ts1)
|
|
459
|
+
elif type(timestamp1) is datetime:
|
|
460
|
+
dt1 = timestamp1
|
|
461
|
+
ts1 = int(round(dt1.timestamp()))
|
|
462
|
+
else:
|
|
463
|
+
return ""
|
|
464
|
+
|
|
465
|
+
if type(timestamp2) is int:
|
|
466
|
+
dt2 = datetime.fromtimestamp(int(ts2))
|
|
467
|
+
elif type(timestamp2) is float:
|
|
468
|
+
ts2 = int(round(ts2))
|
|
469
|
+
dt2 = datetime.fromtimestamp(ts2)
|
|
470
|
+
elif type(timestamp2) is datetime:
|
|
471
|
+
dt2 = timestamp2
|
|
472
|
+
ts2 = int(round(dt2.timestamp()))
|
|
473
|
+
else:
|
|
474
|
+
return ""
|
|
475
|
+
|
|
476
|
+
if ts1 >= ts2:
|
|
477
|
+
ts_diff = ts1 - ts2
|
|
478
|
+
else:
|
|
479
|
+
ts_diff = ts2 - ts1
|
|
480
|
+
dt1, dt2 = dt2, dt1
|
|
481
|
+
|
|
482
|
+
if ts_diff > 0:
|
|
483
|
+
date_diff = relativedelta.relativedelta(dt1, dt2)
|
|
484
|
+
years = date_diff.years
|
|
485
|
+
months = date_diff.months
|
|
486
|
+
weeks = date_diff.weeks
|
|
487
|
+
if not show_weeks:
|
|
488
|
+
weeks = 0
|
|
489
|
+
days = date_diff.days
|
|
490
|
+
if weeks > 0:
|
|
491
|
+
days = days - (weeks * 7)
|
|
492
|
+
hours = date_diff.hours
|
|
493
|
+
if (not show_hours and ts_diff > 86400):
|
|
494
|
+
hours = 0
|
|
495
|
+
minutes = date_diff.minutes
|
|
496
|
+
if (not show_minutes and ts_diff > 3600):
|
|
497
|
+
minutes = 0
|
|
498
|
+
seconds = date_diff.seconds
|
|
499
|
+
if (not show_seconds and ts_diff > 60):
|
|
500
|
+
seconds = 0
|
|
501
|
+
date_list = [years, months, weeks, days, hours, minutes, seconds]
|
|
502
|
+
|
|
503
|
+
for index, interval in enumerate(date_list):
|
|
504
|
+
if interval > 0:
|
|
505
|
+
name = intervals[index]
|
|
506
|
+
if interval == 1:
|
|
507
|
+
name = name.rstrip('s')
|
|
508
|
+
result.append(f"{interval} {name}")
|
|
509
|
+
return ', '.join(result[:granularity])
|
|
510
|
+
else:
|
|
511
|
+
return '0 seconds'
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Sends email notification
|
|
515
|
+
def send_email(subject, body, body_html, use_ssl, smtp_timeout=15):
|
|
516
|
+
fqdn_re = re.compile(r'(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}\.?$)')
|
|
517
|
+
email_re = re.compile(r'[^@]+@[^@]+\.[^@]+')
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
ipaddress.ip_address(str(SMTP_HOST))
|
|
521
|
+
except ValueError:
|
|
522
|
+
if not fqdn_re.search(str(SMTP_HOST)):
|
|
523
|
+
print("Error sending email - SMTP settings are incorrect (invalid IP address/FQDN in SMTP_HOST)")
|
|
524
|
+
return 1
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
port = int(SMTP_PORT)
|
|
528
|
+
if not (1 <= port <= 65535):
|
|
529
|
+
raise ValueError
|
|
530
|
+
except ValueError:
|
|
531
|
+
print("Error sending email - SMTP settings are incorrect (invalid port number in SMTP_PORT)")
|
|
532
|
+
return 1
|
|
533
|
+
|
|
534
|
+
if not email_re.search(str(SENDER_EMAIL)) or not email_re.search(str(RECEIVER_EMAIL)):
|
|
535
|
+
print("Error sending email - SMTP settings are incorrect (invalid email in SENDER_EMAIL or RECEIVER_EMAIL)")
|
|
536
|
+
return 1
|
|
537
|
+
|
|
538
|
+
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)")
|
|
540
|
+
return 1
|
|
541
|
+
|
|
542
|
+
if not subject or not isinstance(subject, str):
|
|
543
|
+
print("Error sending email - SMTP settings are incorrect (subject is not a string or is empty)")
|
|
544
|
+
return 1
|
|
545
|
+
|
|
546
|
+
if not body and not body_html:
|
|
547
|
+
print("Error sending email - SMTP settings are incorrect (body and body_html cannot be empty at the same time)")
|
|
548
|
+
return 1
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
if use_ssl:
|
|
552
|
+
ssl_context = ssl.create_default_context()
|
|
553
|
+
smtpObj = smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=smtp_timeout)
|
|
554
|
+
smtpObj.starttls(context=ssl_context)
|
|
555
|
+
else:
|
|
556
|
+
smtpObj = smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=smtp_timeout)
|
|
557
|
+
smtpObj.login(SMTP_USER, SMTP_PASSWORD)
|
|
558
|
+
email_msg = MIMEMultipart('alternative')
|
|
559
|
+
email_msg["From"] = SENDER_EMAIL
|
|
560
|
+
email_msg["To"] = RECEIVER_EMAIL
|
|
561
|
+
email_msg["Subject"] = str(Header(subject, 'utf-8'))
|
|
562
|
+
|
|
563
|
+
if body:
|
|
564
|
+
part1 = MIMEText(body, 'plain')
|
|
565
|
+
part1 = MIMEText(body.encode('utf-8'), 'plain', _charset='utf-8')
|
|
566
|
+
email_msg.attach(part1)
|
|
567
|
+
|
|
568
|
+
if body_html:
|
|
569
|
+
part2 = MIMEText(body_html, 'html')
|
|
570
|
+
part2 = MIMEText(body_html.encode('utf-8'), 'html', _charset='utf-8')
|
|
571
|
+
email_msg.attach(part2)
|
|
572
|
+
|
|
573
|
+
smtpObj.sendmail(SENDER_EMAIL, RECEIVER_EMAIL, email_msg.as_string())
|
|
574
|
+
smtpObj.quit()
|
|
575
|
+
except Exception as e:
|
|
576
|
+
print(f"Error sending email: {e}")
|
|
577
|
+
return 1
|
|
578
|
+
return 0
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# Initializes the CSV file
|
|
582
|
+
def init_csv_file(csv_file_name):
|
|
583
|
+
try:
|
|
584
|
+
if not os.path.isfile(csv_file_name) or os.path.getsize(csv_file_name) == 0:
|
|
585
|
+
with open(csv_file_name, 'a', newline='', buffering=1, encoding="utf-8") as f:
|
|
586
|
+
writer = csv.DictWriter(f, fieldnames=csvfieldnames, quoting=csv.QUOTE_NONNUMERIC)
|
|
587
|
+
writer.writeheader()
|
|
588
|
+
except Exception as e:
|
|
589
|
+
raise RuntimeError(f"Could not initialize CSV file '{csv_file_name}': {e}")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# Writes CSV entry
|
|
593
|
+
def write_csv_entry(csv_file_name, timestamp, artist, track, playlist, album, last_activity_ts):
|
|
594
|
+
try:
|
|
595
|
+
|
|
596
|
+
with open(csv_file_name, 'a', newline='', buffering=1, encoding="utf-8") as csv_file:
|
|
597
|
+
csvwriter = csv.DictWriter(csv_file, fieldnames=csvfieldnames, quoting=csv.QUOTE_NONNUMERIC)
|
|
598
|
+
csvwriter.writerow({'Date': timestamp, 'Artist': artist, 'Track': track, 'Playlist': playlist, 'Album': album, 'Last activity': last_activity_ts})
|
|
599
|
+
|
|
600
|
+
except Exception as e:
|
|
601
|
+
raise RuntimeError(f"Failed to write to CSV file '{csv_file_name}': {e}")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# Returns the current date/time in human readable format; eg. Sun 21 Apr 2024, 15:08:45
|
|
605
|
+
def get_cur_ts(ts_str=""):
|
|
606
|
+
return (f'{ts_str}{calendar.day_abbr[(datetime.fromtimestamp(int(time.time()))).weekday()]}, {datetime.fromtimestamp(int(time.time())).strftime("%d %b %Y, %H:%M:%S")}')
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# Prints the current date/time in human readable format with separator; eg. Sun 21 Apr 2024, 15:08:45
|
|
610
|
+
def print_cur_ts(ts_str=""):
|
|
611
|
+
print(get_cur_ts(str(ts_str)))
|
|
612
|
+
print("─" * HORIZONTAL_LINE)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# Returns the timestamp/datetime object in human readable format (long version); eg. Sun 21 Apr 2024, 15:08:45
|
|
616
|
+
def get_date_from_ts(ts):
|
|
617
|
+
if type(ts) is datetime:
|
|
618
|
+
ts_new = int(round(ts.timestamp()))
|
|
619
|
+
elif type(ts) is int:
|
|
620
|
+
ts_new = ts
|
|
621
|
+
elif type(ts) is float:
|
|
622
|
+
ts_new = int(round(ts))
|
|
623
|
+
else:
|
|
624
|
+
return ""
|
|
625
|
+
|
|
626
|
+
return (f'{calendar.day_abbr[(datetime.fromtimestamp(ts_new)).weekday()]} {datetime.fromtimestamp(ts_new).strftime("%d %b %Y, %H:%M:%S")}')
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# Returns the timestamp/datetime object in human readable format (short version); eg.
|
|
630
|
+
# Sun 21 Apr 15:08
|
|
631
|
+
# Sun 21 Apr 24, 15:08 (if show_year == True and current year is different)
|
|
632
|
+
# Sun 21 Apr (if show_hour == False)
|
|
633
|
+
def get_short_date_from_ts(ts, show_year=False, show_hour=True):
|
|
634
|
+
if type(ts) is datetime:
|
|
635
|
+
ts_new = int(round(ts.timestamp()))
|
|
636
|
+
elif type(ts) is int:
|
|
637
|
+
ts_new = ts
|
|
638
|
+
elif type(ts) is float:
|
|
639
|
+
ts_new = int(round(ts))
|
|
640
|
+
else:
|
|
641
|
+
return ""
|
|
642
|
+
|
|
643
|
+
if show_hour:
|
|
644
|
+
hour_strftime = " %H:%M"
|
|
645
|
+
else:
|
|
646
|
+
hour_strftime = ""
|
|
647
|
+
|
|
648
|
+
if show_year and int(datetime.fromtimestamp(ts_new).strftime("%Y")) != int(datetime.now().strftime("%Y")):
|
|
649
|
+
if show_hour:
|
|
650
|
+
hour_prefix = ","
|
|
651
|
+
else:
|
|
652
|
+
hour_prefix = ""
|
|
653
|
+
return (f'{calendar.day_abbr[(datetime.fromtimestamp(ts_new)).weekday()]} {datetime.fromtimestamp(ts_new).strftime(f"%d %b %y{hour_prefix}{hour_strftime}")}')
|
|
654
|
+
else:
|
|
655
|
+
return (f'{calendar.day_abbr[(datetime.fromtimestamp(ts_new)).weekday()]} {datetime.fromtimestamp(ts_new).strftime(f"%d %b{hour_strftime}")}')
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# Returns the timestamp/datetime object in human readable format (only hour, minutes and optionally seconds): eg. 15:08:12
|
|
659
|
+
def get_hour_min_from_ts(ts, show_seconds=False):
|
|
660
|
+
if type(ts) is datetime:
|
|
661
|
+
ts_new = int(round(ts.timestamp()))
|
|
662
|
+
elif type(ts) is int:
|
|
663
|
+
ts_new = ts
|
|
664
|
+
elif type(ts) is float:
|
|
665
|
+
ts_new = int(round(ts))
|
|
666
|
+
else:
|
|
667
|
+
return ""
|
|
668
|
+
|
|
669
|
+
if show_seconds:
|
|
670
|
+
out_strf = "%H:%M:%S"
|
|
671
|
+
else:
|
|
672
|
+
out_strf = "%H:%M"
|
|
673
|
+
return (str(datetime.fromtimestamp(ts_new).strftime(out_strf)))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# Returns the range between two timestamps/datetime objects; eg. Sun 21 Apr 14:09 - 14:15
|
|
677
|
+
def get_range_of_dates_from_tss(ts1, ts2, between_sep=" - ", short=False):
|
|
678
|
+
if type(ts1) is datetime:
|
|
679
|
+
ts1_new = int(round(ts1.timestamp()))
|
|
680
|
+
elif type(ts1) is int:
|
|
681
|
+
ts1_new = ts1
|
|
682
|
+
elif type(ts1) is float:
|
|
683
|
+
ts1_new = int(round(ts1))
|
|
684
|
+
else:
|
|
685
|
+
return ""
|
|
686
|
+
|
|
687
|
+
if type(ts2) is datetime:
|
|
688
|
+
ts2_new = int(round(ts2.timestamp()))
|
|
689
|
+
elif type(ts2) is int:
|
|
690
|
+
ts2_new = ts2
|
|
691
|
+
elif type(ts2) is float:
|
|
692
|
+
ts2_new = int(round(ts2))
|
|
693
|
+
else:
|
|
694
|
+
return ""
|
|
695
|
+
|
|
696
|
+
ts1_strf = datetime.fromtimestamp(ts1_new).strftime("%Y%m%d")
|
|
697
|
+
ts2_strf = datetime.fromtimestamp(ts2_new).strftime("%Y%m%d")
|
|
698
|
+
|
|
699
|
+
if ts1_strf == ts2_strf:
|
|
700
|
+
if short:
|
|
701
|
+
out_str = f"{get_short_date_from_ts(ts1_new)}{between_sep}{get_hour_min_from_ts(ts2_new)}"
|
|
702
|
+
else:
|
|
703
|
+
out_str = f"{get_date_from_ts(ts1_new)}{between_sep}{get_hour_min_from_ts(ts2_new, show_seconds=True)}"
|
|
704
|
+
else:
|
|
705
|
+
if short:
|
|
706
|
+
out_str = f"{get_short_date_from_ts(ts1_new)}{between_sep}{get_short_date_from_ts(ts2_new)}"
|
|
707
|
+
else:
|
|
708
|
+
out_str = f"{get_date_from_ts(ts1_new)}{between_sep}{get_date_from_ts(ts2_new)}"
|
|
709
|
+
return (str(out_str))
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# Signal handler for SIGUSR1 allowing to switch active/inactive email notifications
|
|
713
|
+
def toggle_active_inactive_notifications_signal_handler(sig, frame):
|
|
714
|
+
global ACTIVE_NOTIFICATION
|
|
715
|
+
global INACTIVE_NOTIFICATION
|
|
716
|
+
ACTIVE_NOTIFICATION = not ACTIVE_NOTIFICATION
|
|
717
|
+
INACTIVE_NOTIFICATION = not INACTIVE_NOTIFICATION
|
|
718
|
+
sig_name = signal.Signals(sig).name
|
|
719
|
+
print(f"* Signal {sig_name} received")
|
|
720
|
+
print(f"* Email notifications: [active = {ACTIVE_NOTIFICATION}] [inactive = {INACTIVE_NOTIFICATION}]")
|
|
721
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# Signal handler for SIGUSR2 allowing to switch every song email notifications
|
|
725
|
+
def toggle_song_notifications_signal_handler(sig, frame):
|
|
726
|
+
global SONG_NOTIFICATION
|
|
727
|
+
SONG_NOTIFICATION = not SONG_NOTIFICATION
|
|
728
|
+
sig_name = signal.Signals(sig).name
|
|
729
|
+
print(f"* Signal {sig_name} received")
|
|
730
|
+
print(f"* Email notifications: [every song = {SONG_NOTIFICATION}]")
|
|
731
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# Signal handler for SIGCONT allowing to switch tracked songs email notifications
|
|
735
|
+
def toggle_track_notifications_signal_handler(sig, frame):
|
|
736
|
+
global TRACK_NOTIFICATION
|
|
737
|
+
TRACK_NOTIFICATION = not TRACK_NOTIFICATION
|
|
738
|
+
sig_name = signal.Signals(sig).name
|
|
739
|
+
print(f"* Signal {sig_name} received")
|
|
740
|
+
print(f"* Email notifications: [tracked = {TRACK_NOTIFICATION}]")
|
|
741
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# Signal handler for SIGPIPE allowing to switch songs on loop email notifications
|
|
745
|
+
def toggle_songs_on_loop_notifications_signal_handler(sig, frame):
|
|
746
|
+
global SONG_ON_LOOP_NOTIFICATION
|
|
747
|
+
SONG_ON_LOOP_NOTIFICATION = not SONG_ON_LOOP_NOTIFICATION
|
|
748
|
+
sig_name = signal.Signals(sig).name
|
|
749
|
+
print(f"* Signal {sig_name} received")
|
|
750
|
+
print(f"* Email notifications: [songs on loop = {SONG_ON_LOOP_NOTIFICATION}]")
|
|
751
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# Signal handler for SIGTRAP allowing to increase inactivity check timer by SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE seconds
|
|
755
|
+
def increase_inactivity_check_signal_handler(sig, frame):
|
|
756
|
+
global SPOTIFY_INACTIVITY_CHECK
|
|
757
|
+
SPOTIFY_INACTIVITY_CHECK = SPOTIFY_INACTIVITY_CHECK + SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE
|
|
758
|
+
sig_name = signal.Signals(sig).name
|
|
759
|
+
print(f"* Signal {sig_name} received")
|
|
760
|
+
print(f"* Spotify timers: [inactivity: {display_time(SPOTIFY_INACTIVITY_CHECK)}]")
|
|
761
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# Signal handler for SIGABRT allowing to decrease inactivity check timer by SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE seconds
|
|
765
|
+
def decrease_inactivity_check_signal_handler(sig, frame):
|
|
766
|
+
global SPOTIFY_INACTIVITY_CHECK
|
|
767
|
+
if SPOTIFY_INACTIVITY_CHECK - SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE > 0:
|
|
768
|
+
SPOTIFY_INACTIVITY_CHECK = SPOTIFY_INACTIVITY_CHECK - SPOTIFY_INACTIVITY_CHECK_SIGNAL_VALUE
|
|
769
|
+
sig_name = signal.Signals(sig).name
|
|
770
|
+
print(f"* Signal {sig_name} received")
|
|
771
|
+
print(f"* Spotify timers: [inactivity: {display_time(SPOTIFY_INACTIVITY_CHECK)}]")
|
|
772
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# Signal handler for SIGHUP allowing to reload secrets from .env
|
|
776
|
+
def reload_secrets_signal_handler(sig, frame):
|
|
777
|
+
sig_name = signal.Signals(sig).name
|
|
778
|
+
print(f"* Signal {sig_name} received")
|
|
779
|
+
|
|
780
|
+
# disable autoscan if DOTENV_FILE set to none
|
|
781
|
+
if DOTENV_FILE and DOTENV_FILE.lower() == 'none':
|
|
782
|
+
env_path = None
|
|
783
|
+
else:
|
|
784
|
+
# reload .env if python-dotenv is installed
|
|
785
|
+
try:
|
|
786
|
+
from dotenv import load_dotenv, find_dotenv
|
|
787
|
+
if DOTENV_FILE:
|
|
788
|
+
env_path = DOTENV_FILE
|
|
789
|
+
else:
|
|
790
|
+
env_path = find_dotenv()
|
|
791
|
+
if env_path:
|
|
792
|
+
load_dotenv(env_path, override=True)
|
|
793
|
+
else:
|
|
794
|
+
print("* No .env file found, skipping env-var reload")
|
|
795
|
+
except ImportError:
|
|
796
|
+
env_path = None
|
|
797
|
+
print("* python-dotenv not installed, skipping env-var reload")
|
|
798
|
+
|
|
799
|
+
if env_path:
|
|
800
|
+
for secret in SECRET_KEYS:
|
|
801
|
+
old_val = globals().get(secret)
|
|
802
|
+
val = os.getenv(secret)
|
|
803
|
+
if val is not None and val != old_val:
|
|
804
|
+
globals()[secret] = val
|
|
805
|
+
print(f"* Reloaded {secret} from {env_path}")
|
|
806
|
+
|
|
807
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# Prepares Apple & Genius search URLs for specified track
|
|
811
|
+
def get_apple_genius_search_urls(artist, track):
|
|
812
|
+
genius_search_string = f"{artist} {track}"
|
|
813
|
+
youtube_music_search_string = quote_plus(f"{artist} {track}")
|
|
814
|
+
if re.search(re_search_str, genius_search_string, re.IGNORECASE):
|
|
815
|
+
genius_search_string = re.sub(re_replace_str, '', genius_search_string, flags=re.IGNORECASE)
|
|
816
|
+
apple_search_string = quote(f"{artist} {track}")
|
|
817
|
+
apple_search_url = f"https://music.apple.com/pl/search?term={apple_search_string}"
|
|
818
|
+
genius_search_url = f"https://genius.com/search?q={quote_plus(genius_search_string)}"
|
|
819
|
+
youtube_music_search_url = f"https://music.youtube.com/search?q={youtube_music_search_string}"
|
|
820
|
+
return apple_search_url, genius_search_url, youtube_music_search_url
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# Returns random user agent string
|
|
824
|
+
def get_random_user_agent() -> str:
|
|
825
|
+
browser = random.choice(['chrome', 'firefox', 'edge', 'safari'])
|
|
826
|
+
|
|
827
|
+
if browser == 'chrome':
|
|
828
|
+
os_choice = random.choice(['mac', 'windows'])
|
|
829
|
+
if os_choice == 'mac':
|
|
830
|
+
return (
|
|
831
|
+
f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{random.randrange(11, 15)}_{random.randrange(4, 9)}) "
|
|
832
|
+
f"AppleWebKit/{random.randrange(530, 537)}.{random.randrange(30, 37)} (KHTML, like Gecko) "
|
|
833
|
+
f"Chrome/{random.randrange(80, 105)}.0.{random.randrange(3000, 4500)}.{random.randrange(60, 125)} "
|
|
834
|
+
f"Safari/{random.randrange(530, 537)}.{random.randrange(30, 36)}"
|
|
835
|
+
)
|
|
836
|
+
else:
|
|
837
|
+
chrome_version = random.randint(80, 105)
|
|
838
|
+
build = random.randint(3000, 4500)
|
|
839
|
+
patch = random.randint(60, 125)
|
|
840
|
+
return (
|
|
841
|
+
f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
842
|
+
f"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
843
|
+
f"Chrome/{chrome_version}.0.{build}.{patch} Safari/537.36"
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
elif browser == 'firefox':
|
|
847
|
+
os_choice = random.choice(['windows', 'mac', 'linux'])
|
|
848
|
+
version = random.randint(90, 110)
|
|
849
|
+
if os_choice == 'windows':
|
|
850
|
+
return (
|
|
851
|
+
f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{version}.0) "
|
|
852
|
+
f"Gecko/20100101 Firefox/{version}.0"
|
|
853
|
+
)
|
|
854
|
+
elif os_choice == 'mac':
|
|
855
|
+
return (
|
|
856
|
+
f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{random.randrange(11, 15)}_{random.randrange(0, 10)}; rv:{version}.0) "
|
|
857
|
+
f"Gecko/20100101 Firefox/{version}.0"
|
|
858
|
+
)
|
|
859
|
+
else:
|
|
860
|
+
return (
|
|
861
|
+
f"Mozilla/5.0 (X11; Linux x86_64; rv:{version}.0) "
|
|
862
|
+
f"Gecko/20100101 Firefox/{version}.0"
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
elif browser == 'edge':
|
|
866
|
+
os_choice = random.choice(['windows', 'mac'])
|
|
867
|
+
chrome_version = random.randint(80, 105)
|
|
868
|
+
build = random.randint(3000, 4500)
|
|
869
|
+
patch = random.randint(60, 125)
|
|
870
|
+
version_str = f"{chrome_version}.0.{build}.{patch}"
|
|
871
|
+
if os_choice == 'windows':
|
|
872
|
+
return (
|
|
873
|
+
f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
874
|
+
f"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
875
|
+
f"Chrome/{version_str} Safari/537.36 Edg/{version_str}"
|
|
876
|
+
)
|
|
877
|
+
else:
|
|
878
|
+
return (
|
|
879
|
+
f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{random.randrange(11, 15)}_{random.randrange(0, 10)}) "
|
|
880
|
+
f"AppleWebKit/605.1.15 (KHTML, like Gecko) "
|
|
881
|
+
f"Version/{random.randint(13, 16)}.0 Safari/605.1.15 Edg/{version_str}"
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
elif browser == 'safari':
|
|
885
|
+
os_choice = 'mac'
|
|
886
|
+
if os_choice == 'mac':
|
|
887
|
+
mac_major = random.randrange(11, 16)
|
|
888
|
+
mac_minor = random.randrange(0, 10)
|
|
889
|
+
webkit_major = random.randint(600, 610)
|
|
890
|
+
webkit_minor = random.randint(1, 20)
|
|
891
|
+
webkit_patch = random.randint(1, 20)
|
|
892
|
+
safari_version = random.randint(13, 16)
|
|
893
|
+
return (
|
|
894
|
+
f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{mac_major}_{mac_minor}) "
|
|
895
|
+
f"AppleWebKit/{webkit_major}.{webkit_minor}.{webkit_patch} (KHTML, like Gecko) "
|
|
896
|
+
f"Version/{safari_version}.0 Safari/{webkit_major}.{webkit_minor}.{webkit_patch}"
|
|
897
|
+
)
|
|
898
|
+
else:
|
|
899
|
+
return ""
|
|
900
|
+
else:
|
|
901
|
+
return ""
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
# Removes spaces from a hex string and converts it into a corresponding bytes object
|
|
905
|
+
def hex_to_bytes(data: str) -> bytes:
|
|
906
|
+
data = data.replace(" ", "")
|
|
907
|
+
return bytes.fromhex(data)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
# Creates a TOTP object using a secret derived from transformed cipher bytes
|
|
911
|
+
def generate_totp(ua: str):
|
|
912
|
+
secret_cipher_bytes = [
|
|
913
|
+
12, 56, 76, 33, 88, 44, 88, 33,
|
|
914
|
+
78, 78, 11, 66, 22, 22, 55, 69, 54,
|
|
915
|
+
]
|
|
916
|
+
|
|
917
|
+
transformed = [e ^ ((t % 33) + 9) for t, e in enumerate(secret_cipher_bytes)]
|
|
918
|
+
joined = "".join(str(num) for num in transformed)
|
|
919
|
+
utf8_bytes = joined.encode("utf-8")
|
|
920
|
+
hex_str = "".join(format(b, 'x') for b in utf8_bytes)
|
|
921
|
+
secret_bytes = hex_to_bytes(hex_str)
|
|
922
|
+
secret = base64.b32encode(secret_bytes).decode().rstrip('=')
|
|
923
|
+
|
|
924
|
+
headers = {
|
|
925
|
+
"Host": "open.spotify.com",
|
|
926
|
+
"User-Agent": ua,
|
|
927
|
+
"Accept": "*/*",
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
try:
|
|
931
|
+
if platform.system() != 'Windows':
|
|
932
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
933
|
+
signal.alarm(FUNCTION_TIMEOUT + 2)
|
|
934
|
+
resp = req.get(SERVER_TIME_URL, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
935
|
+
except (req.RequestException, TimeoutException) as e:
|
|
936
|
+
raise Exception(f"generate_totp() network request timeout after {display_time(FUNCTION_TIMEOUT + 2)}: {e}")
|
|
937
|
+
finally:
|
|
938
|
+
if platform.system() != 'Windows':
|
|
939
|
+
signal.alarm(0)
|
|
940
|
+
|
|
941
|
+
resp.raise_for_status()
|
|
942
|
+
|
|
943
|
+
json_data = resp.json()
|
|
944
|
+
server_time = json_data.get("serverTime")
|
|
945
|
+
|
|
946
|
+
if server_time is None:
|
|
947
|
+
raise Exception("Failed to get server time")
|
|
948
|
+
|
|
949
|
+
totp_obj = pyotp.TOTP(secret, digits=6, interval=30)
|
|
950
|
+
|
|
951
|
+
return totp_obj, server_time
|
|
952
|
+
|
|
953
|
+
|
|
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
|
+
# Retrieves a new Spotify access token using the sp_dc cookie, tries first with mode "transport" and if needed with "init"
|
|
978
|
+
def refresh_token(sp_dc: str) -> dict:
|
|
979
|
+
transport = True
|
|
980
|
+
init = True
|
|
981
|
+
session = req.Session()
|
|
982
|
+
session.cookies.set("sp_dc", sp_dc)
|
|
983
|
+
data: dict = {}
|
|
984
|
+
token = ""
|
|
985
|
+
|
|
986
|
+
ua = get_random_user_agent()
|
|
987
|
+
totp_obj, server_time = generate_totp(ua)
|
|
988
|
+
client_time = int(time_ns() / 1000 / 1000)
|
|
989
|
+
otp_value = totp_obj.at(server_time)
|
|
990
|
+
|
|
991
|
+
params = {
|
|
992
|
+
"reason": "transport",
|
|
993
|
+
"productType": "web-player",
|
|
994
|
+
"totp": otp_value,
|
|
995
|
+
"totpServer": otp_value,
|
|
996
|
+
"totpVer": 5,
|
|
997
|
+
"sTime": server_time,
|
|
998
|
+
"cTime": client_time,
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
headers = {
|
|
1002
|
+
"User-Agent": ua,
|
|
1003
|
+
"Accept": "application/json",
|
|
1004
|
+
"Cookie": f"sp_dc={sp_dc}",
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
try:
|
|
1008
|
+
if platform.system() != "Windows":
|
|
1009
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1010
|
+
signal.alarm(FUNCTION_TIMEOUT + 2)
|
|
1011
|
+
|
|
1012
|
+
response = session.get(TOKEN_URL, params=params, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1013
|
+
response.raise_for_status()
|
|
1014
|
+
data = response.json()
|
|
1015
|
+
token = data.get("accessToken", "")
|
|
1016
|
+
|
|
1017
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1018
|
+
transport = False
|
|
1019
|
+
finally:
|
|
1020
|
+
if platform.system() != "Windows":
|
|
1021
|
+
signal.alarm(0)
|
|
1022
|
+
|
|
1023
|
+
if not transport or (transport and not check_token_validity(token, data.get("clientId", ""), ua)):
|
|
1024
|
+
params["reason"] = "init"
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
if platform.system() != "Windows":
|
|
1028
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1029
|
+
signal.alarm(FUNCTION_TIMEOUT + 2)
|
|
1030
|
+
|
|
1031
|
+
response = session.get(TOKEN_URL, params=params, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1032
|
+
response.raise_for_status()
|
|
1033
|
+
data = response.json()
|
|
1034
|
+
token = data.get("accessToken", "")
|
|
1035
|
+
|
|
1036
|
+
except (req.RequestException, TimeoutException, req.HTTPError, ValueError):
|
|
1037
|
+
init = False
|
|
1038
|
+
finally:
|
|
1039
|
+
if platform.system() != "Windows":
|
|
1040
|
+
signal.alarm(0)
|
|
1041
|
+
|
|
1042
|
+
if not init or not data or "accessToken" not in data:
|
|
1043
|
+
raise Exception("refresh_token(): Unsuccessful token request")
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
"access_token": token,
|
|
1047
|
+
"expires_at": data["accessTokenExpirationTimestampMs"] // 1000,
|
|
1048
|
+
"client_id": data.get("clientId", ""),
|
|
1049
|
+
"user_agent": ua,
|
|
1050
|
+
"length": len(token)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
# 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
|
|
1057
|
+
|
|
1058
|
+
now = time.time()
|
|
1059
|
+
|
|
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):
|
|
1061
|
+
return SP_CACHED_ACCESS_TOKEN
|
|
1062
|
+
|
|
1063
|
+
max_retries = TOKEN_MAX_RETRIES
|
|
1064
|
+
retry = 0
|
|
1065
|
+
|
|
1066
|
+
while retry < max_retries:
|
|
1067
|
+
token_data = refresh_token(sp_dc)
|
|
1068
|
+
token = token_data["access_token"]
|
|
1069
|
+
client_id = token_data.get("client_id", "")
|
|
1070
|
+
user_agent = token_data.get("user_agent", get_random_user_agent())
|
|
1071
|
+
length = token_data["length"]
|
|
1072
|
+
|
|
1073
|
+
SP_CACHED_ACCESS_TOKEN = token
|
|
1074
|
+
SP_TOKEN_EXPIRES_AT = token_data["expires_at"]
|
|
1075
|
+
SP_CACHED_CLIENT_ID = client_id
|
|
1076
|
+
SP_CACHED_USER_AGENT = user_agent
|
|
1077
|
+
|
|
1078
|
+
if SP_CACHED_ACCESS_TOKEN is None or not check_token_validity(SP_CACHED_ACCESS_TOKEN, SP_CACHED_CLIENT_ID, SP_CACHED_USER_AGENT):
|
|
1079
|
+
retry += 1
|
|
1080
|
+
time.sleep(TOKEN_RETRY_TIMEOUT)
|
|
1081
|
+
else:
|
|
1082
|
+
# print("* Token is valid")
|
|
1083
|
+
break
|
|
1084
|
+
|
|
1085
|
+
# print("Spotify Access Token:", SP_CACHED_ACCESS_TOKEN)
|
|
1086
|
+
# print("Token expires at:", time.ctime(SP_TOKEN_EXPIRES_AT))
|
|
1087
|
+
|
|
1088
|
+
if retry == max_retries:
|
|
1089
|
+
if SP_CACHED_ACCESS_TOKEN is not None:
|
|
1090
|
+
print(f"* Token appears to be still invalid after {max_retries} attempts, returning token anyway")
|
|
1091
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1092
|
+
return SP_CACHED_ACCESS_TOKEN
|
|
1093
|
+
else:
|
|
1094
|
+
raise RuntimeError(f"Failed to obtain a valid Spotify access token after {max_retries} attempts")
|
|
1095
|
+
|
|
1096
|
+
return SP_CACHED_ACCESS_TOKEN
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
# Fetches list of Spotify friends
|
|
1100
|
+
def spotify_get_friends_json(access_token):
|
|
1101
|
+
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
|
+
}
|
|
1107
|
+
|
|
1108
|
+
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1109
|
+
if response.status_code == 401:
|
|
1110
|
+
raise Exception("401 Unauthorized for url: " + url)
|
|
1111
|
+
response.raise_for_status()
|
|
1112
|
+
friends_json = response.json()
|
|
1113
|
+
error_str = friends_json.get("error")
|
|
1114
|
+
if error_str:
|
|
1115
|
+
raise ValueError(error_str)
|
|
1116
|
+
|
|
1117
|
+
return friends_json
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
# Converts Spotify URI (e.g. spotify:user:username) to URL (e.g. https://open.spotify.com/user/username)
|
|
1121
|
+
def spotify_convert_uri_to_url(uri):
|
|
1122
|
+
# add si parameter so link opens in native Spotify app after clicking
|
|
1123
|
+
si = "?si=1"
|
|
1124
|
+
# si=""
|
|
1125
|
+
|
|
1126
|
+
url = ""
|
|
1127
|
+
if "spotify:user:" in uri:
|
|
1128
|
+
s_id = uri.split(':', 2)[2]
|
|
1129
|
+
url = f"https://open.spotify.com/user/{s_id}{si}"
|
|
1130
|
+
elif "spotify:artist:" in uri:
|
|
1131
|
+
s_id = uri.split(':', 2)[2]
|
|
1132
|
+
url = f"https://open.spotify.com/artist/{s_id}{si}"
|
|
1133
|
+
elif "spotify:track:" in uri:
|
|
1134
|
+
s_id = uri.split(':', 2)[2]
|
|
1135
|
+
url = f"https://open.spotify.com/track/{s_id}{si}"
|
|
1136
|
+
elif "spotify:album:" in uri:
|
|
1137
|
+
s_id = uri.split(':', 2)[2]
|
|
1138
|
+
url = f"https://open.spotify.com/album/{s_id}{si}"
|
|
1139
|
+
elif "spotify:playlist:" in uri:
|
|
1140
|
+
s_id = uri.split(':', 2)[2]
|
|
1141
|
+
url = f"https://open.spotify.com/playlist/{s_id}{si}"
|
|
1142
|
+
|
|
1143
|
+
return url
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
# Prints the list of Spotify friends with the last listened track (-l flag)
|
|
1147
|
+
def spotify_list_friends(friend_activity):
|
|
1148
|
+
|
|
1149
|
+
print(f"Number of friends:\t\t{len(friend_activity['friends'])}\n")
|
|
1150
|
+
|
|
1151
|
+
for index, friend in enumerate(friend_activity["friends"]):
|
|
1152
|
+
sp_uri = friend["user"].get("uri").split("spotify:user:", 1)[1]
|
|
1153
|
+
sp_username = friend["user"].get("name")
|
|
1154
|
+
sp_artist = friend["track"]["artist"].get("name")
|
|
1155
|
+
sp_album = friend["track"]["album"].get("name")
|
|
1156
|
+
sp_playlist = friend["track"]["context"].get("name")
|
|
1157
|
+
sp_track = friend["track"].get("name")
|
|
1158
|
+
sp_ts = friend.get("timestamp")
|
|
1159
|
+
sp_album_uri = friend["track"]["album"].get("uri")
|
|
1160
|
+
sp_playlist_uri = friend["track"]["context"].get("uri")
|
|
1161
|
+
sp_track_uri = friend["track"].get("uri")
|
|
1162
|
+
|
|
1163
|
+
# if index > 0:
|
|
1164
|
+
# print("─" * HORIZONTAL_LINE)
|
|
1165
|
+
print("─" * HORIZONTAL_LINE)
|
|
1166
|
+
print(f"Username:\t\t\t{sp_username}")
|
|
1167
|
+
print(f"User URI ID:\t\t\t{sp_uri}")
|
|
1168
|
+
print(f"User URL:\t\t\t{spotify_convert_uri_to_url('spotify:user:' + sp_uri)}")
|
|
1169
|
+
print(f"\nLast played:\t\t\t{sp_artist} - {sp_track}\n")
|
|
1170
|
+
if 'spotify:playlist:' in sp_playlist_uri:
|
|
1171
|
+
print(f"Playlist:\t\t\t{sp_playlist}")
|
|
1172
|
+
print(f"Album:\t\t\t\t{sp_album}")
|
|
1173
|
+
|
|
1174
|
+
if 'spotify:album:' in sp_playlist_uri and sp_playlist != sp_album:
|
|
1175
|
+
print(f"\nContext (Album):\t\t{sp_playlist}")
|
|
1176
|
+
|
|
1177
|
+
if 'spotify:artist:' in sp_playlist_uri:
|
|
1178
|
+
print(f"\nContext (Artist):\t\t{sp_playlist}")
|
|
1179
|
+
|
|
1180
|
+
print(f"\nTrack URL:\t\t\t{spotify_convert_uri_to_url(sp_track_uri)}")
|
|
1181
|
+
if 'spotify:playlist:' in sp_playlist_uri:
|
|
1182
|
+
print(f"Playlist URL:\t\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1183
|
+
print(f"Album URL:\t\t\t{spotify_convert_uri_to_url(sp_album_uri)}")
|
|
1184
|
+
|
|
1185
|
+
if 'spotify:album:' in sp_playlist_uri and sp_playlist != sp_album:
|
|
1186
|
+
print(f"Context (Album) URL:\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1187
|
+
|
|
1188
|
+
if 'spotify:artist:' in sp_playlist_uri:
|
|
1189
|
+
print(f"Context (Artist) URL:\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1190
|
+
|
|
1191
|
+
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
1192
|
+
|
|
1193
|
+
print(f"Apple search URL:\t\t{apple_search_url}")
|
|
1194
|
+
print(f"YouTube Music search URL:\t{youtube_music_search_url}")
|
|
1195
|
+
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
1196
|
+
|
|
1197
|
+
print(f"\nLast activity:\t\t\t{get_date_from_ts(float(str(sp_ts)[0:-3]))} ({calculate_timespan(int(time.time()), datetime.fromtimestamp(float(str(sp_ts)[0:-3])))} ago)")
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
# Returns information for specific Spotify friend's user URI id
|
|
1201
|
+
def spotify_get_friend_info(friend_activity, uri):
|
|
1202
|
+
for friend in friend_activity["friends"]:
|
|
1203
|
+
sp_uri = friend["user"]["uri"].split("spotify:user:", 1)[1]
|
|
1204
|
+
if sp_uri == uri:
|
|
1205
|
+
sp_username = friend["user"].get("name")
|
|
1206
|
+
sp_artist = friend["track"]["artist"].get("name")
|
|
1207
|
+
sp_album = friend["track"]["album"].get("name")
|
|
1208
|
+
sp_album_uri = friend["track"]["album"].get("uri")
|
|
1209
|
+
sp_playlist = friend["track"]["context"].get("name")
|
|
1210
|
+
sp_playlist_uri = friend["track"]["context"].get("uri")
|
|
1211
|
+
sp_track = friend["track"].get("name")
|
|
1212
|
+
sp_track_uri = str(friend["track"].get("uri"))
|
|
1213
|
+
if "spotify:track:" in sp_track_uri:
|
|
1214
|
+
sp_track_uri_id = sp_track_uri.split(':', 2)[2]
|
|
1215
|
+
else:
|
|
1216
|
+
sp_track_uri_id = ""
|
|
1217
|
+
sp_ts = int(str(friend.get("timestamp"))[0:-3])
|
|
1218
|
+
return True, {"sp_uri": sp_uri, "sp_username": sp_username, "sp_artist": sp_artist, "sp_track": sp_track, "sp_track_uri": sp_track_uri, "sp_track_uri_id": sp_track_uri_id, "sp_album": sp_album, "sp_album_uri": sp_album_uri, "sp_playlist": sp_playlist, "sp_playlist_uri": sp_playlist_uri, "sp_ts": sp_ts}
|
|
1219
|
+
return False, {}
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
# Returns information for specific Spotify track URI
|
|
1223
|
+
def spotify_get_track_info(access_token, track_uri):
|
|
1224
|
+
track_id = track_uri.split(':', 2)[2]
|
|
1225
|
+
url = "https://api.spotify.com/v1/tracks/" + track_id
|
|
1226
|
+
headers = {
|
|
1227
|
+
"Authorization": f"Bearer {access_token}",
|
|
1228
|
+
"Client-Id": SP_CACHED_CLIENT_ID,
|
|
1229
|
+
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1230
|
+
}
|
|
1231
|
+
# add si parameter so link opens in native Spotify app after clicking
|
|
1232
|
+
si = "?si=1"
|
|
1233
|
+
|
|
1234
|
+
try:
|
|
1235
|
+
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1236
|
+
response.raise_for_status()
|
|
1237
|
+
json_response = response.json()
|
|
1238
|
+
sp_track_duration = int(json_response.get("duration_ms") / 1000)
|
|
1239
|
+
sp_track_url = json_response["external_urls"].get("spotify") + si
|
|
1240
|
+
sp_track_name = json_response.get("name")
|
|
1241
|
+
sp_artist_url = json_response["artists"][0]["external_urls"].get("spotify") + si
|
|
1242
|
+
sp_artist_name = json_response["artists"][0].get("name")
|
|
1243
|
+
sp_album_url = json_response["album"]["external_urls"].get("spotify") + si
|
|
1244
|
+
sp_album_name = json_response["album"].get("name")
|
|
1245
|
+
return {"sp_track_duration": sp_track_duration, "sp_track_url": sp_track_url, "sp_artist_url": sp_artist_url, "sp_album_url": sp_album_url, "sp_track_name": sp_track_name, "sp_artist_name": sp_artist_name, "sp_album_name": sp_album_name}
|
|
1246
|
+
except Exception:
|
|
1247
|
+
raise
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
# Returns information for specific Spotify playlist URI
|
|
1251
|
+
def spotify_get_playlist_info(access_token, playlist_uri):
|
|
1252
|
+
playlist_id = playlist_uri.split(':', 2)[2]
|
|
1253
|
+
url = f"https://api.spotify.com/v1/playlists/{playlist_id}?fields=name,owner,followers,external_urls"
|
|
1254
|
+
headers = {
|
|
1255
|
+
"Authorization": f"Bearer {access_token}",
|
|
1256
|
+
"Client-Id": SP_CACHED_CLIENT_ID,
|
|
1257
|
+
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1258
|
+
}
|
|
1259
|
+
# add si parameter so link opens in native Spotify app after clicking
|
|
1260
|
+
si = "?si=1"
|
|
1261
|
+
|
|
1262
|
+
try:
|
|
1263
|
+
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1264
|
+
response.raise_for_status()
|
|
1265
|
+
json_response = response.json()
|
|
1266
|
+
sp_playlist_name = json_response.get("name")
|
|
1267
|
+
sp_playlist_owner = json_response["owner"].get("display_name")
|
|
1268
|
+
sp_playlist_owner_url = json_response["owner"]["external_urls"].get("spotify")
|
|
1269
|
+
sp_playlist_followers = int(json_response["followers"].get("total"))
|
|
1270
|
+
sp_playlist_url = json_response["external_urls"].get("spotify") + si
|
|
1271
|
+
return {"sp_playlist_name": sp_playlist_name, "sp_playlist_owner": sp_playlist_owner, "sp_playlist_owner_url": sp_playlist_owner_url, "sp_playlist_followers": sp_playlist_followers, "sp_playlist_url": sp_playlist_url}
|
|
1272
|
+
except Exception:
|
|
1273
|
+
raise
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
# Gets basic information about access token owner
|
|
1277
|
+
def spotify_get_current_user(access_token) -> dict | None:
|
|
1278
|
+
url = "https://api.spotify.com/v1/me"
|
|
1279
|
+
headers = {
|
|
1280
|
+
"Authorization": f"Bearer {access_token}",
|
|
1281
|
+
"Client-Id": SP_CACHED_CLIENT_ID,
|
|
1282
|
+
"User-Agent": SP_CACHED_USER_AGENT,
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if platform.system() != 'Windows':
|
|
1286
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1287
|
+
signal.alarm(ALARM_TIMEOUT + 2)
|
|
1288
|
+
try:
|
|
1289
|
+
response = SESSION.get(url, headers=headers, timeout=FUNCTION_TIMEOUT, verify=VERIFY_SSL)
|
|
1290
|
+
response.raise_for_status()
|
|
1291
|
+
data = response.json()
|
|
1292
|
+
|
|
1293
|
+
user_info = {
|
|
1294
|
+
"display_name": data.get("display_name"),
|
|
1295
|
+
"uri": data.get("uri"),
|
|
1296
|
+
"is_premium": data.get("product") == "premium",
|
|
1297
|
+
"country": data.get("country"),
|
|
1298
|
+
"email": data.get("email"),
|
|
1299
|
+
"spotify_url": data.get("external_urls", {}).get("spotify") + "?si=1" if data.get("external_urls", {}).get("spotify") else None
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return user_info
|
|
1303
|
+
except Exception as e:
|
|
1304
|
+
print(f"* Error: {e}")
|
|
1305
|
+
return None
|
|
1306
|
+
finally:
|
|
1307
|
+
if platform.system() != 'Windows':
|
|
1308
|
+
signal.alarm(0)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def spotify_macos_play_song(sp_track_uri_id, method=SPOTIFY_MACOS_PLAYING_METHOD):
|
|
1312
|
+
if method == "apple-script": # apple-script
|
|
1313
|
+
script = f'tell app "Spotify" to play track "spotify:track:{sp_track_uri_id}"'
|
|
1314
|
+
proc = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
|
1315
|
+
stdout, stderr = proc.communicate(script)
|
|
1316
|
+
else: # trigger-url - just trigger track URL in the client
|
|
1317
|
+
subprocess.call(('open', spotify_convert_uri_to_url(f"spotify:track:{sp_track_uri_id}")))
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def spotify_macos_play_pause(action, method=SPOTIFY_MACOS_PLAYING_METHOD):
|
|
1321
|
+
if method == "apple-script": # apple-script
|
|
1322
|
+
if str(action).lower() == "pause":
|
|
1323
|
+
script = 'tell app "Spotify" to pause'
|
|
1324
|
+
proc = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
|
1325
|
+
stdout, stderr = proc.communicate(script)
|
|
1326
|
+
elif str(action).lower() == "play":
|
|
1327
|
+
script = 'tell app "Spotify" to play'
|
|
1328
|
+
proc = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
|
1329
|
+
stdout, stderr = proc.communicate(script)
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def spotify_linux_play_song(sp_track_uri_id, method=SPOTIFY_LINUX_PLAYING_METHOD):
|
|
1333
|
+
if method == "dbus-send": # dbus-send
|
|
1334
|
+
subprocess.call((f"dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.OpenUri string:'spotify:track:{sp_track_uri_id}'"), shell=True)
|
|
1335
|
+
elif method == "qdbus": # qdbus
|
|
1336
|
+
subprocess.call((f"qdbus org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.OpenUri spotify:track:{sp_track_uri_id}"), shell=True)
|
|
1337
|
+
else: # trigger-url - just trigger track URL in the client
|
|
1338
|
+
subprocess.call(('xdg-open', spotify_convert_uri_to_url(f"spotify:track:{sp_track_uri_id}")), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def spotify_linux_play_pause(action, method=SPOTIFY_LINUX_PLAYING_METHOD):
|
|
1342
|
+
if method == "dbus-send": # dbus-send
|
|
1343
|
+
if str(action).lower() == "pause":
|
|
1344
|
+
subprocess.call((f"dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause"), shell=True)
|
|
1345
|
+
elif str(action).lower() == "play":
|
|
1346
|
+
subprocess.call((f"dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play"), shell=True)
|
|
1347
|
+
elif method == "qdbus": # qdbus
|
|
1348
|
+
if str(action).lower() == "pause":
|
|
1349
|
+
subprocess.call((f"qdbus org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause"), shell=True)
|
|
1350
|
+
elif str(action).lower() == "play":
|
|
1351
|
+
subprocess.call((f"qdbus org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play"), shell=True)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def spotify_win_play_song(sp_track_uri_id, method=SPOTIFY_WINDOWS_PLAYING_METHOD):
|
|
1355
|
+
WIN_SPOTIFY_APP_PATH = r'%APPDATA%\Spotify\Spotify.exe'
|
|
1356
|
+
|
|
1357
|
+
if method == "start-uri": # start-uri
|
|
1358
|
+
subprocess.call((f"start spotify:track:{sp_track_uri_id}"), shell=True)
|
|
1359
|
+
elif method == "spotify-cmd": # spotify-cmd
|
|
1360
|
+
subprocess.call((f"{WIN_SPOTIFY_APP_PATH} --uri=spotify:track:{sp_track_uri_id}"), shell=True)
|
|
1361
|
+
else: # trigger-url - just trigger track URL in the client
|
|
1362
|
+
os.startfile(spotify_convert_uri_to_url(f"spotify:track:{sp_track_uri_id}"))
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
# Finds an optional config file
|
|
1366
|
+
def find_config_file(cli_path=None):
|
|
1367
|
+
"""
|
|
1368
|
+
Search for an optional config file in:
|
|
1369
|
+
1) CLI-provided path (must exist if given)
|
|
1370
|
+
2) ./{DEFAULT_CONFIG_FILENAME}
|
|
1371
|
+
3) ~/.{DEFAULT_CONFIG_FILENAME}
|
|
1372
|
+
4) script-directory/{DEFAULT_CONFIG_FILENAME}
|
|
1373
|
+
"""
|
|
1374
|
+
|
|
1375
|
+
if cli_path:
|
|
1376
|
+
p = Path(os.path.expanduser(cli_path))
|
|
1377
|
+
return str(p) if p.is_file() else None
|
|
1378
|
+
|
|
1379
|
+
candidates = [
|
|
1380
|
+
Path.cwd() / DEFAULT_CONFIG_FILENAME,
|
|
1381
|
+
Path.home() / f".{DEFAULT_CONFIG_FILENAME}",
|
|
1382
|
+
Path(__file__).parent / DEFAULT_CONFIG_FILENAME,
|
|
1383
|
+
]
|
|
1384
|
+
|
|
1385
|
+
for p in candidates:
|
|
1386
|
+
if p.is_file():
|
|
1387
|
+
return str(p)
|
|
1388
|
+
return None
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
# Resolves an executable path by checking if it's a valid file or searching in $PATH
|
|
1392
|
+
def resolve_executable(path):
|
|
1393
|
+
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
1394
|
+
return path
|
|
1395
|
+
|
|
1396
|
+
found = shutil.which(path)
|
|
1397
|
+
if found:
|
|
1398
|
+
return found
|
|
1399
|
+
|
|
1400
|
+
raise FileNotFoundError(f"Could not find executable '{path}'")
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
# Main function that monitors activity of the specified Spotify friend's user URI ID
|
|
1404
|
+
def spotify_monitor_friend_uri(user_uri_id, tracks, csv_file_name):
|
|
1405
|
+
global SP_CACHED_ACCESS_TOKEN
|
|
1406
|
+
sp_active_ts_start = 0
|
|
1407
|
+
sp_active_ts_stop = 0
|
|
1408
|
+
sp_active_ts_start_old = 0
|
|
1409
|
+
user_not_found = False
|
|
1410
|
+
listened_songs = 0
|
|
1411
|
+
listened_songs_old = 0
|
|
1412
|
+
looped_songs = 0
|
|
1413
|
+
looped_songs_old = 0
|
|
1414
|
+
skipped_songs = 0
|
|
1415
|
+
skipped_songs_old = 0
|
|
1416
|
+
sp_artist_old = ""
|
|
1417
|
+
sp_track_old = ""
|
|
1418
|
+
song_on_loop = 0
|
|
1419
|
+
error_500_counter = 0
|
|
1420
|
+
error_500_start_ts = 0
|
|
1421
|
+
error_network_issue_counter = 0
|
|
1422
|
+
error_network_issue_start_ts = 0
|
|
1423
|
+
|
|
1424
|
+
try:
|
|
1425
|
+
if csv_file_name:
|
|
1426
|
+
init_csv_file(csv_file_name)
|
|
1427
|
+
except Exception as e:
|
|
1428
|
+
print(f"* Error: {e}")
|
|
1429
|
+
|
|
1430
|
+
email_sent = False
|
|
1431
|
+
|
|
1432
|
+
out = f"Monitoring user {user_uri_id}"
|
|
1433
|
+
print(out)
|
|
1434
|
+
print("-" * len(out))
|
|
1435
|
+
|
|
1436
|
+
tracks_upper = {t.upper() for t in tracks}
|
|
1437
|
+
|
|
1438
|
+
# Start loop
|
|
1439
|
+
while True:
|
|
1440
|
+
|
|
1441
|
+
# Sometimes Spotify network functions halt even though we specified the timeout
|
|
1442
|
+
# To overcome this we use alarm signal functionality to kill it inevitably, not available on Windows
|
|
1443
|
+
if platform.system() != 'Windows':
|
|
1444
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1445
|
+
signal.alarm(ALARM_TIMEOUT)
|
|
1446
|
+
try:
|
|
1447
|
+
sp_accessToken = spotify_get_access_token(SP_DC_COOKIE)
|
|
1448
|
+
sp_friends = spotify_get_friends_json(sp_accessToken)
|
|
1449
|
+
sp_found, sp_data = spotify_get_friend_info(sp_friends, user_uri_id)
|
|
1450
|
+
email_sent = False
|
|
1451
|
+
if platform.system() != 'Windows':
|
|
1452
|
+
signal.alarm(0)
|
|
1453
|
+
except TimeoutException:
|
|
1454
|
+
if platform.system() != 'Windows':
|
|
1455
|
+
signal.alarm(0)
|
|
1456
|
+
print(f"spotify_*() function timeout after {display_time(ALARM_TIMEOUT)}, retrying in {display_time(ALARM_RETRY)}")
|
|
1457
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1458
|
+
time.sleep(ALARM_RETRY)
|
|
1459
|
+
continue
|
|
1460
|
+
except Exception as e:
|
|
1461
|
+
if platform.system() != 'Windows':
|
|
1462
|
+
signal.alarm(0)
|
|
1463
|
+
|
|
1464
|
+
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: {e}")
|
|
1465
|
+
|
|
1466
|
+
if "401" in str(e):
|
|
1467
|
+
SP_CACHED_ACCESS_TOKEN = None
|
|
1468
|
+
|
|
1469
|
+
if ('access token' in str(e)) or ('Unsuccessful token request' in str(e)):
|
|
1470
|
+
print(f"* Error: sp_dc might have expired!")
|
|
1471
|
+
if ERROR_NOTIFICATION and not email_sent:
|
|
1472
|
+
m_subject = f"spotify_monitor: sp_dc might have expired! (uri: {user_uri_id})"
|
|
1473
|
+
m_body = f"sp_dc might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
1474
|
+
m_body_html = f"<html><head></head><body>sp_dc might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
1475
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1476
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1477
|
+
email_sent = True
|
|
1478
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1479
|
+
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
1480
|
+
continue
|
|
1481
|
+
|
|
1482
|
+
playlist_m_body = ""
|
|
1483
|
+
playlist_m_body_html = ""
|
|
1484
|
+
played_for_m_body = ""
|
|
1485
|
+
played_for_m_body_html = ""
|
|
1486
|
+
is_playlist = False
|
|
1487
|
+
|
|
1488
|
+
# User is found in the Spotify's friend list just after starting the tool
|
|
1489
|
+
if sp_found:
|
|
1490
|
+
user_not_found = False
|
|
1491
|
+
|
|
1492
|
+
user_info = spotify_get_current_user(sp_accessToken)
|
|
1493
|
+
if user_info:
|
|
1494
|
+
print(f"Token belongs to:\t\t{user_info.get('display_name', '')}\n\t\t\t\t[ {user_info.get('spotify_url')} ]")
|
|
1495
|
+
|
|
1496
|
+
sp_track_uri = sp_data["sp_track_uri"]
|
|
1497
|
+
sp_track_uri_id = sp_data["sp_track_uri_id"]
|
|
1498
|
+
sp_album_uri = sp_data["sp_album_uri"]
|
|
1499
|
+
sp_playlist_uri = sp_data["sp_playlist_uri"]
|
|
1500
|
+
|
|
1501
|
+
sp_playlist_data = {}
|
|
1502
|
+
try:
|
|
1503
|
+
sp_track_data = spotify_get_track_info(sp_accessToken, sp_track_uri)
|
|
1504
|
+
if 'spotify:playlist:' in sp_playlist_uri:
|
|
1505
|
+
is_playlist = True
|
|
1506
|
+
sp_playlist_data = spotify_get_playlist_info(sp_accessToken, sp_playlist_uri)
|
|
1507
|
+
if not sp_playlist_data:
|
|
1508
|
+
is_playlist = False
|
|
1509
|
+
else:
|
|
1510
|
+
is_playlist = False
|
|
1511
|
+
except Exception as e:
|
|
1512
|
+
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: {e}")
|
|
1513
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1514
|
+
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
1515
|
+
continue
|
|
1516
|
+
|
|
1517
|
+
sp_username = sp_data["sp_username"]
|
|
1518
|
+
|
|
1519
|
+
sp_artist = sp_data["sp_artist"]
|
|
1520
|
+
if not sp_artist:
|
|
1521
|
+
sp_artist = sp_track_data["sp_artist_name"]
|
|
1522
|
+
|
|
1523
|
+
sp_track = sp_data["sp_track"]
|
|
1524
|
+
if not sp_track:
|
|
1525
|
+
sp_track = sp_track_data["sp_track_name"]
|
|
1526
|
+
|
|
1527
|
+
sp_playlist = sp_data["sp_playlist"]
|
|
1528
|
+
|
|
1529
|
+
sp_album = sp_data["sp_album"]
|
|
1530
|
+
if not sp_album:
|
|
1531
|
+
sp_album = sp_track_data["sp_album_name"]
|
|
1532
|
+
|
|
1533
|
+
sp_ts = sp_data["sp_ts"]
|
|
1534
|
+
cur_ts = int(time.time())
|
|
1535
|
+
|
|
1536
|
+
sp_track_duration = sp_track_data["sp_track_duration"]
|
|
1537
|
+
sp_track_url = sp_track_data["sp_track_url"]
|
|
1538
|
+
sp_artist_url = sp_track_data["sp_artist_url"]
|
|
1539
|
+
sp_album_url = sp_track_data["sp_album_url"]
|
|
1540
|
+
|
|
1541
|
+
sp_playlist_url = ""
|
|
1542
|
+
if is_playlist:
|
|
1543
|
+
sp_playlist_url = sp_playlist_data.get("sp_playlist_url")
|
|
1544
|
+
playlist_m_body = f"\nPlaylist: {sp_playlist}"
|
|
1545
|
+
playlist_m_body_html = f"<br>Playlist: <a href=\"{sp_playlist_url}\">{escape(sp_playlist)}</a>"
|
|
1546
|
+
|
|
1547
|
+
print(f"\nUsername:\t\t\t{sp_username}")
|
|
1548
|
+
print(f"User URI ID:\t\t\t{sp_data['sp_uri']}")
|
|
1549
|
+
print(f"\nLast played:\t\t\t{sp_artist} - {sp_track}")
|
|
1550
|
+
print(f"Duration:\t\t\t{display_time(sp_track_duration)}\n")
|
|
1551
|
+
if is_playlist:
|
|
1552
|
+
print(f"Playlist:\t\t\t{sp_playlist}")
|
|
1553
|
+
|
|
1554
|
+
print(f"Album:\t\t\t\t{sp_album}")
|
|
1555
|
+
|
|
1556
|
+
context_m_body = ""
|
|
1557
|
+
context_m_body_html = ""
|
|
1558
|
+
|
|
1559
|
+
if 'spotify:album:' in sp_playlist_uri and sp_playlist != sp_album:
|
|
1560
|
+
print(f"\nContext (Album):\t\t{sp_playlist}")
|
|
1561
|
+
context_m_body += f"\nContext (Album): {sp_playlist}"
|
|
1562
|
+
context_m_body_html += f"<br>Context (Album): <a href=\"{spotify_convert_uri_to_url(sp_playlist_uri)}\">{escape(sp_playlist)}</a>"
|
|
1563
|
+
|
|
1564
|
+
if 'spotify:artist:' in sp_playlist_uri:
|
|
1565
|
+
print(f"\nContext (Artist):\t\t{sp_playlist}")
|
|
1566
|
+
context_m_body += f"\nContext (Artist): {sp_playlist}"
|
|
1567
|
+
context_m_body_html += f"<br>Context (Artist): <a href=\"{spotify_convert_uri_to_url(sp_playlist_uri)}\">{escape(sp_playlist)}</a>"
|
|
1568
|
+
|
|
1569
|
+
print(f"\nTrack URL:\t\t\t{sp_track_url}")
|
|
1570
|
+
if is_playlist:
|
|
1571
|
+
print(f"Playlist URL:\t\t\t{sp_playlist_url}")
|
|
1572
|
+
print(f"Album URL:\t\t\t{sp_album_url}")
|
|
1573
|
+
|
|
1574
|
+
if 'spotify:album:' in sp_playlist_uri and sp_playlist != sp_album:
|
|
1575
|
+
print(f"Context (Album) URL:\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1576
|
+
|
|
1577
|
+
if 'spotify:artist:' in sp_playlist_uri:
|
|
1578
|
+
print(f"Context (Artist) URL:\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1579
|
+
|
|
1580
|
+
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
1581
|
+
|
|
1582
|
+
print(f"Apple search URL:\t\t{apple_search_url}")
|
|
1583
|
+
print(f"YouTube Music search URL:\t{youtube_music_search_url}")
|
|
1584
|
+
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
1585
|
+
|
|
1586
|
+
if not is_playlist:
|
|
1587
|
+
sp_playlist = ""
|
|
1588
|
+
|
|
1589
|
+
print(f"\nLast activity:\t\t\t{get_date_from_ts(sp_ts)}")
|
|
1590
|
+
|
|
1591
|
+
# Friend is currently active (listens to music)
|
|
1592
|
+
if (cur_ts - sp_ts) <= SPOTIFY_INACTIVITY_CHECK:
|
|
1593
|
+
sp_active_ts_start = sp_ts - sp_track_duration
|
|
1594
|
+
sp_active_ts_stop = 0
|
|
1595
|
+
listened_songs = 1
|
|
1596
|
+
song_on_loop = 1
|
|
1597
|
+
print("\n*** Friend is currently ACTIVE !")
|
|
1598
|
+
|
|
1599
|
+
if sp_track.upper() in tracks_upper or sp_playlist.upper() in tracks_upper or sp_album.upper() in tracks_upper:
|
|
1600
|
+
print("*** Track/playlist/album matched with the list!")
|
|
1601
|
+
|
|
1602
|
+
try:
|
|
1603
|
+
if csv_file_name:
|
|
1604
|
+
write_csv_entry(csv_file_name, datetime.fromtimestamp(int(cur_ts)), sp_artist, sp_track, sp_playlist, sp_album, datetime.fromtimestamp(int(sp_ts)))
|
|
1605
|
+
except Exception as e:
|
|
1606
|
+
print(f"* Error: {e}")
|
|
1607
|
+
|
|
1608
|
+
if ACTIVE_NOTIFICATION:
|
|
1609
|
+
m_subject = f"Spotify user {sp_username} is active: '{sp_artist} - {sp_track}'"
|
|
1610
|
+
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
|
|
1611
|
+
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
|
|
1612
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1613
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1614
|
+
|
|
1615
|
+
if TRACK_SONGS and sp_track_uri_id:
|
|
1616
|
+
if platform.system() == 'Darwin': # macOS
|
|
1617
|
+
spotify_macos_play_song(sp_track_uri_id)
|
|
1618
|
+
elif platform.system() == 'Windows': # Windows
|
|
1619
|
+
spotify_win_play_song(sp_track_uri_id)
|
|
1620
|
+
else: # Linux variants
|
|
1621
|
+
spotify_linux_play_song(sp_track_uri_id)
|
|
1622
|
+
|
|
1623
|
+
# Friend is currently offline (does not play music)
|
|
1624
|
+
else:
|
|
1625
|
+
sp_active_ts_stop = sp_ts
|
|
1626
|
+
print(f"\n*** Friend is OFFLINE for: {calculate_timespan(int(cur_ts), int(sp_ts))}")
|
|
1627
|
+
|
|
1628
|
+
print(f"\nTracks/playlists/albums to monitor: {tracks}")
|
|
1629
|
+
print_cur_ts("\nTimestamp:\t\t\t")
|
|
1630
|
+
|
|
1631
|
+
sp_ts_old = sp_ts
|
|
1632
|
+
alive_counter = 0
|
|
1633
|
+
|
|
1634
|
+
email_sent = False
|
|
1635
|
+
|
|
1636
|
+
# Main loop
|
|
1637
|
+
while True:
|
|
1638
|
+
|
|
1639
|
+
while True:
|
|
1640
|
+
# Sometimes Spotify network functions halt even though we specified the timeout
|
|
1641
|
+
# To overcome this we use alarm signal functionality to kill it inevitably, not available on Windows
|
|
1642
|
+
if platform.system() != 'Windows':
|
|
1643
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
1644
|
+
signal.alarm(ALARM_TIMEOUT)
|
|
1645
|
+
try:
|
|
1646
|
+
sp_accessToken = spotify_get_access_token(SP_DC_COOKIE)
|
|
1647
|
+
sp_friends = spotify_get_friends_json(sp_accessToken)
|
|
1648
|
+
sp_found, sp_data = spotify_get_friend_info(sp_friends, user_uri_id)
|
|
1649
|
+
email_sent = False
|
|
1650
|
+
if platform.system() != 'Windows':
|
|
1651
|
+
signal.alarm(0)
|
|
1652
|
+
break
|
|
1653
|
+
except TimeoutException:
|
|
1654
|
+
if platform.system() != 'Windows':
|
|
1655
|
+
signal.alarm(0)
|
|
1656
|
+
print(f"spotify_*() function timeout after {display_time(ALARM_TIMEOUT)}, retrying in {display_time(ALARM_RETRY)}")
|
|
1657
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1658
|
+
time.sleep(ALARM_RETRY)
|
|
1659
|
+
except Exception as e:
|
|
1660
|
+
if platform.system() != 'Windows':
|
|
1661
|
+
signal.alarm(0)
|
|
1662
|
+
|
|
1663
|
+
if "401" in str(e):
|
|
1664
|
+
SP_CACHED_ACCESS_TOKEN = None
|
|
1665
|
+
|
|
1666
|
+
str_matches = ["500 server", "504 server", "502 server", "503 server"]
|
|
1667
|
+
if any(x in str(e).lower() for x in str_matches):
|
|
1668
|
+
if not error_500_start_ts:
|
|
1669
|
+
error_500_start_ts = int(time.time())
|
|
1670
|
+
error_500_counter = 1
|
|
1671
|
+
else:
|
|
1672
|
+
error_500_counter += 1
|
|
1673
|
+
|
|
1674
|
+
str_matches = ["timed out", "timeout", "name resolution", "failed to resolve", "family not supported", "429 client", "aborted"]
|
|
1675
|
+
if any(x in str(e).lower() for x in str_matches) or str(e) == '':
|
|
1676
|
+
if not error_network_issue_start_ts:
|
|
1677
|
+
error_network_issue_start_ts = int(time.time())
|
|
1678
|
+
error_network_issue_counter = 1
|
|
1679
|
+
else:
|
|
1680
|
+
error_network_issue_counter += 1
|
|
1681
|
+
|
|
1682
|
+
if error_500_start_ts and (error_500_counter >= ERROR_500_NUMBER_LIMIT and (int(time.time()) - error_500_start_ts) >= ERROR_500_TIME_LIMIT):
|
|
1683
|
+
print(f"* Error 50x ({error_500_counter}x times in the last {display_time((int(time.time()) - error_500_start_ts))}): '{e}'")
|
|
1684
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1685
|
+
error_500_start_ts = 0
|
|
1686
|
+
error_500_counter = 0
|
|
1687
|
+
|
|
1688
|
+
elif error_network_issue_start_ts and (error_network_issue_counter >= ERROR_NETWORK_ISSUES_NUMBER_LIMIT and (int(time.time()) - error_network_issue_start_ts) >= ERROR_NETWORK_ISSUES_TIME_LIMIT):
|
|
1689
|
+
print(f"* Error with network ({error_network_issue_counter}x times in the last {display_time((int(time.time()) - error_network_issue_start_ts))}): '{e}'")
|
|
1690
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1691
|
+
error_network_issue_start_ts = 0
|
|
1692
|
+
error_network_issue_counter = 0
|
|
1693
|
+
|
|
1694
|
+
elif not error_500_start_ts and not error_network_issue_start_ts:
|
|
1695
|
+
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: '{e}'")
|
|
1696
|
+
if ('access token' in str(e)) or ('Unsuccessful token request' in str(e)):
|
|
1697
|
+
print(f"* Error: sp_dc might have expired!")
|
|
1698
|
+
if ERROR_NOTIFICATION and not email_sent:
|
|
1699
|
+
m_subject = f"spotify_monitor: sp_dc might have expired! (uri: {user_uri_id})"
|
|
1700
|
+
m_body = f"sp_dc might have expired!\n{e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
1701
|
+
m_body_html = f"<html><head></head><body>sp_dc might have expired!<br>{escape(str(e))}{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
1702
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1703
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1704
|
+
email_sent = True
|
|
1705
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1706
|
+
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
1707
|
+
|
|
1708
|
+
if sp_found is False:
|
|
1709
|
+
# User disappeared from the Spotify's friend list
|
|
1710
|
+
if user_not_found is False:
|
|
1711
|
+
print(f"Spotify user {user_uri_id} ({sp_username}) disappeared - make sure your friend is followed and has activity sharing enabled. Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals")
|
|
1712
|
+
if ERROR_NOTIFICATION:
|
|
1713
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) disappeared!"
|
|
1714
|
+
m_body = f"Spotify user {user_uri_id} ({sp_username}) disappeared - make sure your friend is followed and has activity sharing enabled\nRetrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
1715
|
+
m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) disappeared - make sure your friend is followed and has activity sharing enabled<br>Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)} intervals{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
1716
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1717
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1718
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1719
|
+
user_not_found = True
|
|
1720
|
+
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
1721
|
+
continue
|
|
1722
|
+
else:
|
|
1723
|
+
# User reappeared in the Spotify's friend list
|
|
1724
|
+
if user_not_found is True:
|
|
1725
|
+
print(f"Spotify user {user_uri_id} ({sp_username}) appeared again!")
|
|
1726
|
+
if ERROR_NOTIFICATION:
|
|
1727
|
+
m_subject = f"Spotify user {user_uri_id} ({sp_username}) appeared!"
|
|
1728
|
+
m_body = f"Spotify user {user_uri_id} ({sp_username}) appeared again!{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
1729
|
+
m_body_html = f"<html><head></head><body>Spotify user {user_uri_id} ({sp_username}) appeared again!{get_cur_ts('<br><br>Timestamp: ')}</body></html>"
|
|
1730
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1731
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1732
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1733
|
+
|
|
1734
|
+
user_not_found = False
|
|
1735
|
+
sp_ts = sp_data["sp_ts"]
|
|
1736
|
+
cur_ts = int(time.time())
|
|
1737
|
+
# Track has changed
|
|
1738
|
+
if sp_ts != sp_ts_old:
|
|
1739
|
+
sp_artist_old = sp_artist
|
|
1740
|
+
sp_track_old = sp_track
|
|
1741
|
+
alive_counter = 0
|
|
1742
|
+
sp_playlist = sp_data["sp_playlist"]
|
|
1743
|
+
sp_track_uri = sp_data["sp_track_uri"]
|
|
1744
|
+
sp_track_uri_id = sp_data["sp_track_uri_id"]
|
|
1745
|
+
sp_album_uri = sp_data["sp_album_uri"]
|
|
1746
|
+
sp_playlist_uri = sp_data["sp_playlist_uri"]
|
|
1747
|
+
try:
|
|
1748
|
+
sp_track_data = spotify_get_track_info(sp_accessToken, sp_track_uri)
|
|
1749
|
+
if 'spotify:playlist:' in sp_playlist_uri:
|
|
1750
|
+
is_playlist = True
|
|
1751
|
+
sp_playlist_data = spotify_get_playlist_info(sp_accessToken, sp_playlist_uri)
|
|
1752
|
+
if not sp_playlist_data:
|
|
1753
|
+
is_playlist = False
|
|
1754
|
+
else:
|
|
1755
|
+
is_playlist = False
|
|
1756
|
+
except Exception as e:
|
|
1757
|
+
print(f"* Error, retrying in {display_time(SPOTIFY_ERROR_INTERVAL)}: {e}")
|
|
1758
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1759
|
+
time.sleep(SPOTIFY_ERROR_INTERVAL)
|
|
1760
|
+
continue
|
|
1761
|
+
|
|
1762
|
+
sp_username = sp_data["sp_username"]
|
|
1763
|
+
|
|
1764
|
+
sp_artist = sp_data["sp_artist"]
|
|
1765
|
+
if not sp_artist:
|
|
1766
|
+
sp_artist = sp_track_data["sp_artist_name"]
|
|
1767
|
+
|
|
1768
|
+
sp_track = sp_data["sp_track"]
|
|
1769
|
+
if not sp_track:
|
|
1770
|
+
sp_track = sp_track_data["sp_track_name"]
|
|
1771
|
+
|
|
1772
|
+
sp_album = sp_data["sp_album"]
|
|
1773
|
+
if not sp_album:
|
|
1774
|
+
sp_album = sp_track_data["sp_album_name"]
|
|
1775
|
+
|
|
1776
|
+
sp_track_duration = sp_track_data["sp_track_duration"]
|
|
1777
|
+
sp_track_url = sp_track_data["sp_track_url"]
|
|
1778
|
+
sp_artist_url = sp_track_data["sp_artist_url"]
|
|
1779
|
+
sp_album_url = sp_track_data["sp_album_url"]
|
|
1780
|
+
|
|
1781
|
+
# If tracking functionality is enabled then play the current song via Spotify client
|
|
1782
|
+
|
|
1783
|
+
if TRACK_SONGS and sp_track_uri_id:
|
|
1784
|
+
if platform.system() == 'Darwin': # macOS
|
|
1785
|
+
spotify_macos_play_song(sp_track_uri_id)
|
|
1786
|
+
elif platform.system() == 'Windows': # Windows
|
|
1787
|
+
spotify_win_play_song(sp_track_uri_id)
|
|
1788
|
+
else: # Linux variants
|
|
1789
|
+
spotify_linux_play_song(sp_track_uri_id)
|
|
1790
|
+
|
|
1791
|
+
if is_playlist:
|
|
1792
|
+
sp_playlist_url = sp_playlist_data.get("sp_playlist_url")
|
|
1793
|
+
playlist_m_body = f"\nPlaylist: {sp_playlist}"
|
|
1794
|
+
playlist_m_body_html = f"<br>Playlist: <a href=\"{sp_playlist_url}\">{escape(sp_playlist)}</a>"
|
|
1795
|
+
else:
|
|
1796
|
+
playlist_m_body = ""
|
|
1797
|
+
playlist_m_body_html = ""
|
|
1798
|
+
|
|
1799
|
+
if sp_artist == sp_artist_old and sp_track == sp_track_old:
|
|
1800
|
+
song_on_loop += 1
|
|
1801
|
+
if song_on_loop == SONG_ON_LOOP_VALUE:
|
|
1802
|
+
looped_songs += 1
|
|
1803
|
+
else:
|
|
1804
|
+
song_on_loop = 1
|
|
1805
|
+
|
|
1806
|
+
print(f"Spotify user:\t\t\t{sp_username}")
|
|
1807
|
+
print(f"\nLast played:\t\t\t{sp_artist} - {sp_track}")
|
|
1808
|
+
print(f"Duration:\t\t\t{display_time(sp_track_duration)}")
|
|
1809
|
+
|
|
1810
|
+
listened_songs += 1
|
|
1811
|
+
|
|
1812
|
+
if (sp_ts - sp_ts_old) < (sp_track_duration - 1):
|
|
1813
|
+
played_for_time = sp_ts - sp_ts_old
|
|
1814
|
+
listened_percentage = (played_for_time) / (sp_track_duration - 1)
|
|
1815
|
+
played_for = display_time(played_for_time)
|
|
1816
|
+
if listened_percentage <= SKIPPED_SONG_THRESHOLD:
|
|
1817
|
+
played_for += f" - SKIPPED ({int(listened_percentage * 100)}%)"
|
|
1818
|
+
skipped_songs += 1
|
|
1819
|
+
else:
|
|
1820
|
+
played_for += f" ({int(listened_percentage * 100)}%)"
|
|
1821
|
+
print(f"Played for:\t\t\t{played_for}")
|
|
1822
|
+
played_for_m_body = f"\nPlayed for: {played_for}"
|
|
1823
|
+
played_for_m_body_html = f"<br>Played for: {played_for}"
|
|
1824
|
+
else:
|
|
1825
|
+
played_for_m_body = ""
|
|
1826
|
+
played_for_m_body_html = ""
|
|
1827
|
+
|
|
1828
|
+
if is_playlist:
|
|
1829
|
+
print(f"Playlist:\t\t\t{sp_playlist}")
|
|
1830
|
+
|
|
1831
|
+
print(f"Album:\t\t\t\t{sp_album}")
|
|
1832
|
+
|
|
1833
|
+
context_m_body = ""
|
|
1834
|
+
context_m_body_html = ""
|
|
1835
|
+
|
|
1836
|
+
if 'spotify:album:' in sp_playlist_uri and sp_playlist != sp_album:
|
|
1837
|
+
print(f"\nContext (Album):\t\t{sp_playlist}")
|
|
1838
|
+
context_m_body += f"\nContext (Album): {sp_playlist}"
|
|
1839
|
+
context_m_body_html += f"<br>Context (Album): <a href=\"{spotify_convert_uri_to_url(sp_playlist_uri)}\">{escape(sp_playlist)}</a>"
|
|
1840
|
+
|
|
1841
|
+
if 'spotify:artist:' in sp_playlist_uri:
|
|
1842
|
+
print(f"\nContext (Artist):\t\t{sp_playlist}")
|
|
1843
|
+
context_m_body += f"\nContext (Artist): {sp_playlist}"
|
|
1844
|
+
context_m_body_html += f"<br>Context (Artist): <a href=\"{spotify_convert_uri_to_url(sp_playlist_uri)}\">{escape(sp_playlist)}</a>"
|
|
1845
|
+
|
|
1846
|
+
print(f"Last activity:\t\t\t{get_date_from_ts(sp_ts)}")
|
|
1847
|
+
|
|
1848
|
+
print(f"\nTrack URL:\t\t\t{sp_track_url}")
|
|
1849
|
+
if is_playlist:
|
|
1850
|
+
print(f"Playlist URL:\t\t\t{sp_playlist_url}")
|
|
1851
|
+
print(f"Album URL:\t\t\t{sp_album_url}")
|
|
1852
|
+
|
|
1853
|
+
if 'spotify:album:' in sp_playlist_uri and sp_playlist != sp_album:
|
|
1854
|
+
print(f"Context (Album) URL:\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1855
|
+
|
|
1856
|
+
if 'spotify:artist:' in sp_playlist_uri:
|
|
1857
|
+
print(f"Context (Artist) URL:\t\t{spotify_convert_uri_to_url(sp_playlist_uri)}")
|
|
1858
|
+
|
|
1859
|
+
apple_search_url, genius_search_url, youtube_music_search_url = get_apple_genius_search_urls(str(sp_artist), str(sp_track))
|
|
1860
|
+
|
|
1861
|
+
print(f"Apple search URL:\t\t{apple_search_url}")
|
|
1862
|
+
print(f"YouTube Music search URL:\t{youtube_music_search_url}")
|
|
1863
|
+
print(f"Genius lyrics URL:\t\t{genius_search_url}")
|
|
1864
|
+
|
|
1865
|
+
if not is_playlist:
|
|
1866
|
+
sp_playlist = ""
|
|
1867
|
+
|
|
1868
|
+
if song_on_loop == SONG_ON_LOOP_VALUE:
|
|
1869
|
+
print("─" * HORIZONTAL_LINE)
|
|
1870
|
+
print(f"User plays song on LOOP ({song_on_loop} times)")
|
|
1871
|
+
print("─" * HORIZONTAL_LINE)
|
|
1872
|
+
|
|
1873
|
+
# Friend got active after being offline
|
|
1874
|
+
if (cur_ts - sp_ts_old) > SPOTIFY_INACTIVITY_CHECK and sp_active_ts_stop > 0:
|
|
1875
|
+
|
|
1876
|
+
sp_active_ts_start = sp_ts - sp_track_duration
|
|
1877
|
+
|
|
1878
|
+
listened_songs = 1
|
|
1879
|
+
skipped_songs = 0
|
|
1880
|
+
looped_songs = 0
|
|
1881
|
+
|
|
1882
|
+
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)})")
|
|
1883
|
+
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)})"
|
|
1884
|
+
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)}"
|
|
1885
|
+
friend_active_m_body_html = f"<br><br>Friend got active after being offline for <b>{calculate_timespan(int(sp_active_ts_start), int(sp_active_ts_stop))}</b><br>Last activity (before getting offline): <b>{get_date_from_ts(sp_active_ts_stop)}</b>"
|
|
1886
|
+
if (sp_active_ts_start - sp_active_ts_stop) < 30:
|
|
1887
|
+
listened_songs = listened_songs_old
|
|
1888
|
+
skipped_songs = skipped_songs_old
|
|
1889
|
+
looped_songs = looped_songs_old
|
|
1890
|
+
print(f"*** Inactivity timer ({display_time(SPOTIFY_INACTIVITY_CHECK)}) value might be too low, readjusting session start back to {get_short_date_from_ts(sp_active_ts_start_old)}")
|
|
1891
|
+
friend_active_m_body += f"\nInactivity timer ({display_time(SPOTIFY_INACTIVITY_CHECK)}) value might be too low, readjusting session start back to {get_short_date_from_ts(sp_active_ts_start_old)}"
|
|
1892
|
+
friend_active_m_body_html += f"<br>Inactivity timer (<b>{display_time(SPOTIFY_INACTIVITY_CHECK)}</b>) value might be <b>too low</b>, readjusting session start back to <b>{get_short_date_from_ts(sp_active_ts_start_old)}</b>"
|
|
1893
|
+
if sp_active_ts_start_old > 0:
|
|
1894
|
+
sp_active_ts_start = sp_active_ts_start_old
|
|
1895
|
+
sp_active_ts_stop = 0
|
|
1896
|
+
|
|
1897
|
+
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
|
|
1898
|
+
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
|
|
1899
|
+
|
|
1900
|
+
if ACTIVE_NOTIFICATION:
|
|
1901
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1902
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1903
|
+
email_sent = True
|
|
1904
|
+
|
|
1905
|
+
on_the_list = False
|
|
1906
|
+
if sp_track.upper() in tracks_upper or sp_playlist.upper() in tracks_upper or sp_album.upper() in tracks_upper:
|
|
1907
|
+
print("\n*** Track/playlist/album matched with the list!")
|
|
1908
|
+
on_the_list = True
|
|
1909
|
+
|
|
1910
|
+
if (TRACK_NOTIFICATION and on_the_list and not email_sent) or (SONG_NOTIFICATION and not email_sent):
|
|
1911
|
+
m_subject = f"Spotify user {sp_username}: '{sp_artist} - {sp_track}'"
|
|
1912
|
+
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
|
|
1913
|
+
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
|
|
1914
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1915
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1916
|
+
email_sent = True
|
|
1917
|
+
|
|
1918
|
+
if song_on_loop == SONG_ON_LOOP_VALUE and SONG_ON_LOOP_NOTIFICATION:
|
|
1919
|
+
m_subject = f"Spotify user {sp_username} plays song on loop: '{sp_artist} - {sp_track}'"
|
|
1920
|
+
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 search URL: {apple_search_url}\nYouTube Music search 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: ')}"
|
|
1921
|
+
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>"
|
|
1922
|
+
if not email_sent:
|
|
1923
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1924
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1925
|
+
|
|
1926
|
+
try:
|
|
1927
|
+
if csv_file_name:
|
|
1928
|
+
write_csv_entry(csv_file_name, datetime.fromtimestamp(int(cur_ts)), sp_artist, sp_track, sp_playlist, sp_album, datetime.fromtimestamp(int(sp_ts)))
|
|
1929
|
+
except Exception as e:
|
|
1930
|
+
print(f"* Error: {e}")
|
|
1931
|
+
|
|
1932
|
+
print_cur_ts("\nTimestamp:\t\t\t")
|
|
1933
|
+
sp_ts_old = sp_ts
|
|
1934
|
+
# Track has not changed
|
|
1935
|
+
else:
|
|
1936
|
+
alive_counter += 1
|
|
1937
|
+
# Friend got inactive
|
|
1938
|
+
if (cur_ts - sp_ts) > SPOTIFY_INACTIVITY_CHECK and sp_active_ts_start > 0:
|
|
1939
|
+
sp_active_ts_stop = sp_ts
|
|
1940
|
+
print(f"*** Friend got INACTIVE after listening to music for {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}")
|
|
1941
|
+
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 ')}")
|
|
1942
|
+
|
|
1943
|
+
listened_songs_text = f"*** User played {listened_songs} songs"
|
|
1944
|
+
listened_songs_mbody = f"\n\nUser played {listened_songs} songs"
|
|
1945
|
+
listened_songs_mbody_html = f"<br><br>User played <b>{listened_songs}</b> songs"
|
|
1946
|
+
|
|
1947
|
+
if skipped_songs > 0:
|
|
1948
|
+
skipped_songs_text = f", skipped {skipped_songs} songs ({int((skipped_songs / listened_songs) * 100)}%)"
|
|
1949
|
+
listened_songs_text += skipped_songs_text
|
|
1950
|
+
listened_songs_mbody += skipped_songs_text
|
|
1951
|
+
listened_songs_mbody_html += f", skipped <b>{skipped_songs}</b> songs <b>({int((skipped_songs / listened_songs) * 100)}%)</b>"
|
|
1952
|
+
|
|
1953
|
+
if looped_songs > 0:
|
|
1954
|
+
looped_songs_text = f"\n*** User played {looped_songs} songs on loop"
|
|
1955
|
+
looped_songs_mbody = f"\nUser played {looped_songs} songs on loop"
|
|
1956
|
+
looped_songs_mbody_html = f"<br>User played <b>{looped_songs}</b> songs on loop"
|
|
1957
|
+
listened_songs_text += looped_songs_text
|
|
1958
|
+
listened_songs_mbody += looped_songs_mbody
|
|
1959
|
+
listened_songs_mbody_html += looped_songs_mbody_html
|
|
1960
|
+
|
|
1961
|
+
print(listened_songs_text)
|
|
1962
|
+
|
|
1963
|
+
print(f"*** Last activity:\t\t{get_date_from_ts(sp_active_ts_stop)} (inactive timer: {display_time(SPOTIFY_INACTIVITY_CHECK)})")
|
|
1964
|
+
# If tracking functionality is enabled then either pause the current song via Spotify client or play the indicated SP_USER_GOT_OFFLINE_TRACK_ID "finishing" song
|
|
1965
|
+
if TRACK_SONGS:
|
|
1966
|
+
if SP_USER_GOT_OFFLINE_TRACK_ID:
|
|
1967
|
+
if platform.system() == 'Darwin': # macOS
|
|
1968
|
+
spotify_macos_play_song(SP_USER_GOT_OFFLINE_TRACK_ID)
|
|
1969
|
+
if SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE > 0:
|
|
1970
|
+
time.sleep(SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE)
|
|
1971
|
+
spotify_macos_play_pause("pause")
|
|
1972
|
+
elif platform.system() == 'Windows': # Windows
|
|
1973
|
+
pass
|
|
1974
|
+
else: # Linux variants
|
|
1975
|
+
spotify_linux_play_song(SP_USER_GOT_OFFLINE_TRACK_ID)
|
|
1976
|
+
if SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE > 0:
|
|
1977
|
+
time.sleep(SP_USER_GOT_OFFLINE_DELAY_BEFORE_PAUSE)
|
|
1978
|
+
spotify_linux_play_pause("pause")
|
|
1979
|
+
else:
|
|
1980
|
+
if platform.system() == 'Darwin': # macOS
|
|
1981
|
+
spotify_macos_play_pause("pause")
|
|
1982
|
+
elif platform.system() == 'Windows': # Windows
|
|
1983
|
+
pass
|
|
1984
|
+
else: # Linux variants
|
|
1985
|
+
spotify_linux_play_pause("pause")
|
|
1986
|
+
if INACTIVE_NOTIFICATION:
|
|
1987
|
+
m_subject = f"Spotify user {sp_username} is inactive: '{sp_artist} - {sp_track}' (after {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start), show_seconds=False)}: {get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True)})"
|
|
1988
|
+
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 search URL: {apple_search_url}\nYouTube Music search URL:{youtube_music_search_url}\nGenius lyrics URL: {genius_search_url}\n\nFriend got inactive after listening to music for {calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}\nFriend played music from {get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True, between_sep=' to ')}{listened_songs_mbody}\n\nLast activity: {get_date_from_ts(sp_active_ts_stop)}\nInactivity timer: {display_time(SPOTIFY_INACTIVITY_CHECK)}{get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
1989
|
+
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 search URL: <a href=\"{apple_search_url}\">{escape(sp_artist)} - {escape(sp_track)}</a><br>YouTube Music search 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>Friend got inactive after listening to music for <b>{calculate_timespan(int(sp_active_ts_stop), int(sp_active_ts_start))}</b><br>Friend played music from <b>{get_range_of_dates_from_tss(sp_active_ts_start, sp_active_ts_stop, short=True, between_sep='</b> to <b>')}</b>{listened_songs_mbody_html}<br><br>Last activity: <b>{get_date_from_ts(sp_active_ts_stop)}</b><br>Inactivity timer: {display_time(SPOTIFY_INACTIVITY_CHECK)}{get_cur_ts('<br>Timestamp: ')}</body></html>"
|
|
1990
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1991
|
+
send_email(m_subject, m_body, m_body_html, SMTP_SSL)
|
|
1992
|
+
email_sent = True
|
|
1993
|
+
sp_active_ts_start_old = sp_active_ts_start
|
|
1994
|
+
sp_active_ts_start = 0
|
|
1995
|
+
listened_songs_old = listened_songs
|
|
1996
|
+
skipped_songs_old = skipped_songs
|
|
1997
|
+
looped_songs_old = looped_songs
|
|
1998
|
+
listened_songs = 0
|
|
1999
|
+
looped_songs = 0
|
|
2000
|
+
skipped_songs = 0
|
|
2001
|
+
print_cur_ts("\nTimestamp:\t\t\t")
|
|
2002
|
+
|
|
2003
|
+
if LIVENESS_CHECK_COUNTER and alive_counter >= LIVENESS_CHECK_COUNTER:
|
|
2004
|
+
print_cur_ts("Liveness check, timestamp:\t")
|
|
2005
|
+
alive_counter = 0
|
|
2006
|
+
|
|
2007
|
+
time.sleep(SPOTIFY_CHECK_INTERVAL)
|
|
2008
|
+
|
|
2009
|
+
ERROR_500_ZERO_TIME_LIMIT = ERROR_500_TIME_LIMIT + SPOTIFY_CHECK_INTERVAL
|
|
2010
|
+
if SPOTIFY_CHECK_INTERVAL * ERROR_500_NUMBER_LIMIT > ERROR_500_ZERO_TIME_LIMIT:
|
|
2011
|
+
ERROR_500_ZERO_TIME_LIMIT = SPOTIFY_CHECK_INTERVAL * (ERROR_500_NUMBER_LIMIT + 1)
|
|
2012
|
+
|
|
2013
|
+
if error_500_start_ts and ((int(time.time()) - error_500_start_ts) >= ERROR_500_ZERO_TIME_LIMIT):
|
|
2014
|
+
error_500_start_ts = 0
|
|
2015
|
+
error_500_counter = 0
|
|
2016
|
+
|
|
2017
|
+
ERROR_NETWORK_ZERO_TIME_LIMIT = ERROR_NETWORK_ISSUES_TIME_LIMIT + SPOTIFY_CHECK_INTERVAL
|
|
2018
|
+
if SPOTIFY_CHECK_INTERVAL * ERROR_NETWORK_ISSUES_NUMBER_LIMIT > ERROR_NETWORK_ZERO_TIME_LIMIT:
|
|
2019
|
+
ERROR_NETWORK_ZERO_TIME_LIMIT = SPOTIFY_CHECK_INTERVAL * (ERROR_NETWORK_ISSUES_NUMBER_LIMIT + 1)
|
|
2020
|
+
|
|
2021
|
+
if error_network_issue_start_ts and ((int(time.time()) - error_network_issue_start_ts) >= ERROR_NETWORK_ZERO_TIME_LIMIT):
|
|
2022
|
+
error_network_issue_start_ts = 0
|
|
2023
|
+
error_network_issue_counter = 0
|
|
2024
|
+
|
|
2025
|
+
# User is not found in the Spotify's friend list just after starting the tool
|
|
2026
|
+
else:
|
|
2027
|
+
if user_not_found is False:
|
|
2028
|
+
print(f"User {user_uri_id} not found - make sure your friend is followed and has activity sharing enabled. Retrying in {display_time(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)}.")
|
|
2029
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
2030
|
+
user_not_found = True
|
|
2031
|
+
time.sleep(SPOTIFY_DISAPPEARED_CHECK_INTERVAL)
|
|
2032
|
+
continue
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
def main():
|
|
2036
|
+
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
|
|
2037
|
+
|
|
2038
|
+
if "--generate-config" in sys.argv:
|
|
2039
|
+
print(CONFIG_BLOCK.strip("\n"))
|
|
2040
|
+
sys.exit(0)
|
|
2041
|
+
|
|
2042
|
+
if "--version" in sys.argv:
|
|
2043
|
+
print(f"{os.path.basename(sys.argv[0])} v{VERSION}")
|
|
2044
|
+
sys.exit(0)
|
|
2045
|
+
|
|
2046
|
+
stdout_bck = sys.stdout
|
|
2047
|
+
|
|
2048
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
2049
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
2050
|
+
|
|
2051
|
+
clear_screen(CLEAR_SCREEN)
|
|
2052
|
+
|
|
2053
|
+
print(f"Spotify Monitoring Tool v{VERSION}\n")
|
|
2054
|
+
|
|
2055
|
+
parser = argparse.ArgumentParser(
|
|
2056
|
+
prog="spotify_monitor",
|
|
2057
|
+
description=("Monitor a Spotify friend's activity and send customizable email alerts [ https://github.com/misiektoja/spotify_monitor/ ]"), formatter_class=argparse.RawTextHelpFormatter
|
|
2058
|
+
)
|
|
2059
|
+
|
|
2060
|
+
# Positional
|
|
2061
|
+
parser.add_argument(
|
|
2062
|
+
"user_id",
|
|
2063
|
+
nargs="?",
|
|
2064
|
+
metavar="SPOTIFY_USER_URI_ID",
|
|
2065
|
+
help="Spotify user URI ID"
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
# Version, just to list in help, it is handled earlier
|
|
2069
|
+
parser.add_argument(
|
|
2070
|
+
"--version",
|
|
2071
|
+
action="version",
|
|
2072
|
+
version=f"%(prog)s v{VERSION}"
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
# Configuration & dotenv files
|
|
2076
|
+
conf = parser.add_argument_group("Configuration & dotenv files")
|
|
2077
|
+
conf.add_argument(
|
|
2078
|
+
"--config-file",
|
|
2079
|
+
dest="config_file",
|
|
2080
|
+
metavar="PATH",
|
|
2081
|
+
help="Location of the optional config file",
|
|
2082
|
+
)
|
|
2083
|
+
conf.add_argument(
|
|
2084
|
+
"--generate-config",
|
|
2085
|
+
action="store_true",
|
|
2086
|
+
help="Print default config template and exit",
|
|
2087
|
+
)
|
|
2088
|
+
conf.add_argument(
|
|
2089
|
+
"--env-file",
|
|
2090
|
+
dest="env_file",
|
|
2091
|
+
metavar="PATH",
|
|
2092
|
+
help="Path to optional dotenv file (auto-search if not set, disable with 'none')",
|
|
2093
|
+
)
|
|
2094
|
+
|
|
2095
|
+
# API credentials
|
|
2096
|
+
creds = parser.add_argument_group("API credentials")
|
|
2097
|
+
creds.add_argument(
|
|
2098
|
+
"-u", "--spotify-dc-cookie",
|
|
2099
|
+
dest="spotify_dc_cookie",
|
|
2100
|
+
metavar="SP_DC_COOKIE",
|
|
2101
|
+
help="Spotify sp_dc cookie"
|
|
2102
|
+
)
|
|
2103
|
+
|
|
2104
|
+
# Notifications
|
|
2105
|
+
notify = parser.add_argument_group("Notifications")
|
|
2106
|
+
notify.add_argument(
|
|
2107
|
+
"-a", "--notify-active",
|
|
2108
|
+
dest="notify_active",
|
|
2109
|
+
action="store_true",
|
|
2110
|
+
default=None,
|
|
2111
|
+
help="Email when user becomes active"
|
|
2112
|
+
)
|
|
2113
|
+
notify.add_argument(
|
|
2114
|
+
"-i", "--notify-inactive",
|
|
2115
|
+
dest="notify_inactive",
|
|
2116
|
+
action="store_true",
|
|
2117
|
+
default=None,
|
|
2118
|
+
help="Email when user goes inactive"
|
|
2119
|
+
)
|
|
2120
|
+
notify.add_argument(
|
|
2121
|
+
"-t", "--notify-track",
|
|
2122
|
+
dest="notify_track",
|
|
2123
|
+
action="store_true",
|
|
2124
|
+
default=None,
|
|
2125
|
+
help="Email when a monitored track/playlist/album plays"
|
|
2126
|
+
)
|
|
2127
|
+
notify.add_argument(
|
|
2128
|
+
"-j", "--notify-song-changes",
|
|
2129
|
+
dest="notify_song_changes",
|
|
2130
|
+
action="store_true",
|
|
2131
|
+
default=None,
|
|
2132
|
+
help="Email on every song change"
|
|
2133
|
+
)
|
|
2134
|
+
notify.add_argument(
|
|
2135
|
+
"-x", "--notify-loop",
|
|
2136
|
+
dest="notify_loop",
|
|
2137
|
+
action="store_true",
|
|
2138
|
+
default=None,
|
|
2139
|
+
help="Email when user plays a song on loop"
|
|
2140
|
+
)
|
|
2141
|
+
notify.add_argument(
|
|
2142
|
+
"-e", "--no-error-notify",
|
|
2143
|
+
dest="notify_errors",
|
|
2144
|
+
action="store_false",
|
|
2145
|
+
default=None,
|
|
2146
|
+
help="Disable email on errors (e.g. expired sp_dc)"
|
|
2147
|
+
)
|
|
2148
|
+
notify.add_argument(
|
|
2149
|
+
"--send-test-email",
|
|
2150
|
+
dest="send_test_email",
|
|
2151
|
+
action="store_true",
|
|
2152
|
+
help="Send test email to verify SMTP settings"
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
# Intervals & timers
|
|
2156
|
+
times = parser.add_argument_group("Intervals & timers")
|
|
2157
|
+
times.add_argument(
|
|
2158
|
+
"-c", "--check-interval",
|
|
2159
|
+
dest="check_interval",
|
|
2160
|
+
metavar="SECONDS",
|
|
2161
|
+
type=int,
|
|
2162
|
+
help="Time between monitoring checks, in seconds"
|
|
2163
|
+
)
|
|
2164
|
+
times.add_argument(
|
|
2165
|
+
"-o", "--offline-timer",
|
|
2166
|
+
dest="offline_timer",
|
|
2167
|
+
metavar="SECONDS",
|
|
2168
|
+
type=int,
|
|
2169
|
+
help="Time required to mark inactive user as offline, in seconds"
|
|
2170
|
+
)
|
|
2171
|
+
times.add_argument(
|
|
2172
|
+
"-m", "--disappeared-timer",
|
|
2173
|
+
dest="disappeared_timer",
|
|
2174
|
+
metavar="SECONDS",
|
|
2175
|
+
type=int,
|
|
2176
|
+
help="Wait time between checks once the user disappears from friends list, in seconds"
|
|
2177
|
+
)
|
|
2178
|
+
|
|
2179
|
+
# Listing
|
|
2180
|
+
listing = parser.add_argument_group("Listing")
|
|
2181
|
+
listing.add_argument(
|
|
2182
|
+
"-l", "--list-friends",
|
|
2183
|
+
dest="list_friends",
|
|
2184
|
+
action="store_true",
|
|
2185
|
+
help="List Spotify friends with their last listened track"
|
|
2186
|
+
)
|
|
2187
|
+
listing.add_argument(
|
|
2188
|
+
"-v", "--show-user-info",
|
|
2189
|
+
dest="show_user_info",
|
|
2190
|
+
action="store_true",
|
|
2191
|
+
help="Get basic information about access token owner"
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
# Features & output
|
|
2195
|
+
opts = parser.add_argument_group("Features & output")
|
|
2196
|
+
opts.add_argument(
|
|
2197
|
+
"-g", "--track-in-spotify",
|
|
2198
|
+
dest="track_in_spotify",
|
|
2199
|
+
action="store_true",
|
|
2200
|
+
default=None,
|
|
2201
|
+
help="Auto‑play each listened song in your Spotify client"
|
|
2202
|
+
)
|
|
2203
|
+
opts.add_argument(
|
|
2204
|
+
"-b", "--csv-file",
|
|
2205
|
+
dest="csv_file",
|
|
2206
|
+
metavar="CSV_FILE",
|
|
2207
|
+
type=str,
|
|
2208
|
+
help="Write every listened track to CSV file"
|
|
2209
|
+
)
|
|
2210
|
+
opts.add_argument(
|
|
2211
|
+
"-s", "--monitor-list",
|
|
2212
|
+
dest="monitor_list",
|
|
2213
|
+
metavar="TRACKS_FILE",
|
|
2214
|
+
type=str,
|
|
2215
|
+
help="Filename with Spotify tracks/playlists/albums to alert on"
|
|
2216
|
+
)
|
|
2217
|
+
opts.add_argument(
|
|
2218
|
+
"-y", "--file-suffix",
|
|
2219
|
+
dest="file_suffix",
|
|
2220
|
+
metavar="SUFFIX",
|
|
2221
|
+
type=str,
|
|
2222
|
+
help="File suffix to append to output filenames instead of Spotify user URI ID"
|
|
2223
|
+
)
|
|
2224
|
+
opts.add_argument(
|
|
2225
|
+
"-d", "--disable-logging",
|
|
2226
|
+
dest="disable_logging",
|
|
2227
|
+
action="store_true",
|
|
2228
|
+
default=None,
|
|
2229
|
+
help="Disable logging to file spotify_monitor_<user_uri_id/file_suffix>.log"
|
|
2230
|
+
)
|
|
2231
|
+
|
|
2232
|
+
args = parser.parse_args()
|
|
2233
|
+
|
|
2234
|
+
if len(sys.argv) == 1:
|
|
2235
|
+
parser.print_help(sys.stderr)
|
|
2236
|
+
sys.exit(1)
|
|
2237
|
+
|
|
2238
|
+
if args.config_file:
|
|
2239
|
+
CLI_CONFIG_PATH = os.path.expanduser(args.config_file)
|
|
2240
|
+
|
|
2241
|
+
cfg_path = find_config_file(CLI_CONFIG_PATH)
|
|
2242
|
+
|
|
2243
|
+
if not cfg_path and CLI_CONFIG_PATH:
|
|
2244
|
+
print(f"* Error: Config file '{CLI_CONFIG_PATH}' does not exist")
|
|
2245
|
+
sys.exit(1)
|
|
2246
|
+
|
|
2247
|
+
if cfg_path:
|
|
2248
|
+
try:
|
|
2249
|
+
with open(cfg_path, "r") as cf:
|
|
2250
|
+
exec(cf.read(), globals())
|
|
2251
|
+
except Exception as e:
|
|
2252
|
+
print(f"* Error loading config file '{cfg_path}': {e}")
|
|
2253
|
+
sys.exit(1)
|
|
2254
|
+
|
|
2255
|
+
if args.env_file:
|
|
2256
|
+
DOTENV_FILE = os.path.expanduser(args.env_file)
|
|
2257
|
+
else:
|
|
2258
|
+
if DOTENV_FILE:
|
|
2259
|
+
DOTENV_FILE = os.path.expanduser(DOTENV_FILE)
|
|
2260
|
+
|
|
2261
|
+
if DOTENV_FILE and DOTENV_FILE.lower() == 'none':
|
|
2262
|
+
env_path = None
|
|
2263
|
+
else:
|
|
2264
|
+
try:
|
|
2265
|
+
from dotenv import load_dotenv, find_dotenv
|
|
2266
|
+
|
|
2267
|
+
if DOTENV_FILE:
|
|
2268
|
+
env_path = DOTENV_FILE
|
|
2269
|
+
if not os.path.isfile(env_path):
|
|
2270
|
+
print(f"* Warning: dotenv file '{env_path}' does not exist\n")
|
|
2271
|
+
else:
|
|
2272
|
+
load_dotenv(env_path, override=True)
|
|
2273
|
+
else:
|
|
2274
|
+
env_path = find_dotenv() or None
|
|
2275
|
+
if env_path:
|
|
2276
|
+
load_dotenv(env_path, override=True)
|
|
2277
|
+
except ImportError:
|
|
2278
|
+
env_path = DOTENV_FILE if DOTENV_FILE else None
|
|
2279
|
+
if env_path:
|
|
2280
|
+
print(f"* Warning: Cannot load dotenv file '{env_path}' because 'python-dotenv' is not installed\n\nTo install it, run:\n pip3 install python-dotenv\n\nOnce installed, re-run this tool\n")
|
|
2281
|
+
|
|
2282
|
+
if env_path:
|
|
2283
|
+
for secret in SECRET_KEYS:
|
|
2284
|
+
val = os.getenv(secret)
|
|
2285
|
+
if val is not None:
|
|
2286
|
+
globals()[secret] = val
|
|
2287
|
+
|
|
2288
|
+
if not check_internet():
|
|
2289
|
+
sys.exit(1)
|
|
2290
|
+
|
|
2291
|
+
if args.send_test_email:
|
|
2292
|
+
print("* Sending test email notification ...\n")
|
|
2293
|
+
if send_email("spotify_monitor: test email", "This is test email - your SMTP settings seems to be correct !", "", SMTP_SSL, smtp_timeout=5) == 0:
|
|
2294
|
+
print("* Email sent successfully !")
|
|
2295
|
+
else:
|
|
2296
|
+
sys.exit(1)
|
|
2297
|
+
sys.exit(0)
|
|
2298
|
+
|
|
2299
|
+
if args.check_interval:
|
|
2300
|
+
SPOTIFY_CHECK_INTERVAL = args.check_interval
|
|
2301
|
+
LIVENESS_CHECK_COUNTER = LIVENESS_CHECK_INTERVAL / SPOTIFY_CHECK_INTERVAL
|
|
2302
|
+
|
|
2303
|
+
if args.offline_timer:
|
|
2304
|
+
SPOTIFY_INACTIVITY_CHECK = args.offline_timer
|
|
2305
|
+
|
|
2306
|
+
if args.disappeared_timer:
|
|
2307
|
+
SPOTIFY_DISAPPEARED_CHECK_INTERVAL = args.disappeared_timer
|
|
2308
|
+
|
|
2309
|
+
if args.spotify_dc_cookie:
|
|
2310
|
+
SP_DC_COOKIE = args.spotify_dc_cookie
|
|
2311
|
+
|
|
2312
|
+
if not SP_DC_COOKIE or SP_DC_COOKIE == "your_sp_dc_cookie_value":
|
|
2313
|
+
print("* Error: SP_DC_COOKIE (-u / --spotify-dc-cookie) value is empty or incorrect")
|
|
2314
|
+
sys.exit(1)
|
|
2315
|
+
|
|
2316
|
+
if args.show_user_info:
|
|
2317
|
+
print("* Getting basic information about access token owner ...\n")
|
|
2318
|
+
try:
|
|
2319
|
+
accessToken = spotify_get_access_token(SP_DC_COOKIE)
|
|
2320
|
+
user_info = spotify_get_current_user(accessToken)
|
|
2321
|
+
|
|
2322
|
+
if user_info:
|
|
2323
|
+
print(f"Token belongs to:\n")
|
|
2324
|
+
|
|
2325
|
+
print(f"Username:\t\t{user_info.get('display_name', '')}")
|
|
2326
|
+
print(f"User URI ID:\t\t{user_info.get('uri', '').split('spotify:user:', 1)[1]}")
|
|
2327
|
+
print(f"User URL:\t\t{user_info.get('spotify_url', '')}")
|
|
2328
|
+
print(f"User e-mail:\t\t{user_info.get('email', '')}")
|
|
2329
|
+
print(f"User country:\t\t{user_info.get('country', '')}")
|
|
2330
|
+
print(f"Is Premium?:\t\t{user_info.get('is_premium', '')}")
|
|
2331
|
+
else:
|
|
2332
|
+
print("Failed to retrieve user info.")
|
|
2333
|
+
|
|
2334
|
+
print("─" * HORIZONTAL_LINE)
|
|
2335
|
+
except Exception as e:
|
|
2336
|
+
print(f"* Error: {e}")
|
|
2337
|
+
sys.exit(1)
|
|
2338
|
+
sys.exit(0)
|
|
2339
|
+
|
|
2340
|
+
if args.list_friends:
|
|
2341
|
+
print("* Listing Spotify friends ...\n")
|
|
2342
|
+
try:
|
|
2343
|
+
accessToken = spotify_get_access_token(SP_DC_COOKIE)
|
|
2344
|
+
user_info = spotify_get_current_user(accessToken)
|
|
2345
|
+
if user_info:
|
|
2346
|
+
print(f"Token belongs to:\t\t{user_info.get('display_name', '')}\n\t\t\t\t[ {user_info.get('spotify_url')} ]")
|
|
2347
|
+
sp_friends = spotify_get_friends_json(accessToken)
|
|
2348
|
+
spotify_list_friends(sp_friends)
|
|
2349
|
+
print("─" * HORIZONTAL_LINE)
|
|
2350
|
+
except Exception as e:
|
|
2351
|
+
print(f"* Error: {e}")
|
|
2352
|
+
sys.exit(1)
|
|
2353
|
+
sys.exit(0)
|
|
2354
|
+
|
|
2355
|
+
if not args.user_id:
|
|
2356
|
+
print("* Error: SPOTIFY_USER_URI_ID argument is required !")
|
|
2357
|
+
sys.exit(1)
|
|
2358
|
+
|
|
2359
|
+
if args.monitor_list:
|
|
2360
|
+
MONITOR_LIST_FILE = os.path.expanduser(args.monitor_list)
|
|
2361
|
+
else:
|
|
2362
|
+
if MONITOR_LIST_FILE:
|
|
2363
|
+
MONITOR_LIST_FILE = os.path.expanduser(MONITOR_LIST_FILE)
|
|
2364
|
+
|
|
2365
|
+
if MONITOR_LIST_FILE:
|
|
2366
|
+
try:
|
|
2367
|
+
try:
|
|
2368
|
+
with open(MONITOR_LIST_FILE, encoding="utf-8") as file:
|
|
2369
|
+
lines = file.read().splitlines()
|
|
2370
|
+
except UnicodeDecodeError:
|
|
2371
|
+
with open(MONITOR_LIST_FILE, encoding="cp1252") as file:
|
|
2372
|
+
lines = file.read().splitlines()
|
|
2373
|
+
|
|
2374
|
+
sp_tracks = [
|
|
2375
|
+
line.strip()
|
|
2376
|
+
for line in lines
|
|
2377
|
+
if line.strip() and not line.strip().startswith("#")
|
|
2378
|
+
]
|
|
2379
|
+
except Exception as e:
|
|
2380
|
+
print(f"* Error: File with monitored Spotify tracks cannot be opened: {e}")
|
|
2381
|
+
sys.exit(1)
|
|
2382
|
+
else:
|
|
2383
|
+
sp_tracks = []
|
|
2384
|
+
|
|
2385
|
+
if args.csv_file:
|
|
2386
|
+
CSV_FILE = os.path.expanduser(args.csv_file)
|
|
2387
|
+
else:
|
|
2388
|
+
if CSV_FILE:
|
|
2389
|
+
CSV_FILE = os.path.expanduser(CSV_FILE)
|
|
2390
|
+
|
|
2391
|
+
if CSV_FILE:
|
|
2392
|
+
try:
|
|
2393
|
+
with open(CSV_FILE, 'a', newline='', buffering=1, encoding="utf-8") as _:
|
|
2394
|
+
pass
|
|
2395
|
+
except Exception as e:
|
|
2396
|
+
print(f"* Error: CSV file cannot be opened for writing: {e}")
|
|
2397
|
+
sys.exit(1)
|
|
2398
|
+
|
|
2399
|
+
if args.file_suffix:
|
|
2400
|
+
FILE_SUFFIX = str(args.file_suffix)
|
|
2401
|
+
else:
|
|
2402
|
+
if not FILE_SUFFIX:
|
|
2403
|
+
FILE_SUFFIX = str(args.user_id)
|
|
2404
|
+
|
|
2405
|
+
if args.disable_logging is True:
|
|
2406
|
+
DISABLE_LOGGING = True
|
|
2407
|
+
|
|
2408
|
+
if not DISABLE_LOGGING:
|
|
2409
|
+
log_path = Path(os.path.expanduser(SP_LOGFILE))
|
|
2410
|
+
if log_path.parent != Path('.'):
|
|
2411
|
+
if log_path.suffix == "":
|
|
2412
|
+
log_path = log_path.parent / f"{log_path.name}_{FILE_SUFFIX}.log"
|
|
2413
|
+
else:
|
|
2414
|
+
if log_path.suffix == "":
|
|
2415
|
+
log_path = Path(f"{log_path.name}_{FILE_SUFFIX}.log")
|
|
2416
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2417
|
+
FINAL_LOG_PATH = str(log_path)
|
|
2418
|
+
sys.stdout = Logger(FINAL_LOG_PATH)
|
|
2419
|
+
else:
|
|
2420
|
+
FINAL_LOG_PATH = None
|
|
2421
|
+
|
|
2422
|
+
if args.notify_active is True:
|
|
2423
|
+
ACTIVE_NOTIFICATION = True
|
|
2424
|
+
|
|
2425
|
+
if args.notify_inactive is True:
|
|
2426
|
+
INACTIVE_NOTIFICATION = True
|
|
2427
|
+
|
|
2428
|
+
if args.notify_track is True:
|
|
2429
|
+
TRACK_NOTIFICATION = True
|
|
2430
|
+
|
|
2431
|
+
if args.notify_song_changes is True:
|
|
2432
|
+
SONG_NOTIFICATION = True
|
|
2433
|
+
|
|
2434
|
+
if args.notify_loop is True:
|
|
2435
|
+
SONG_ON_LOOP_NOTIFICATION = True
|
|
2436
|
+
|
|
2437
|
+
if args.notify_errors is False:
|
|
2438
|
+
ERROR_NOTIFICATION = False
|
|
2439
|
+
|
|
2440
|
+
if args.track_in_spotify is True:
|
|
2441
|
+
TRACK_SONGS = True
|
|
2442
|
+
|
|
2443
|
+
if SMTP_HOST.startswith("your_smtp_server_"):
|
|
2444
|
+
ACTIVE_NOTIFICATION = False
|
|
2445
|
+
INACTIVE_NOTIFICATION = False
|
|
2446
|
+
TRACK_NOTIFICATION = False
|
|
2447
|
+
SONG_NOTIFICATION = False
|
|
2448
|
+
SONG_ON_LOOP_NOTIFICATION = False
|
|
2449
|
+
ERROR_NOTIFICATION = False
|
|
2450
|
+
|
|
2451
|
+
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)}]")
|
|
2452
|
+
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}]")
|
|
2453
|
+
print(f"* Track listened songs:\t\t{TRACK_SONGS}")
|
|
2454
|
+
print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
|
|
2455
|
+
print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
|
|
2456
|
+
print(f"* Alert on monitored tracks:\t{bool(MONITOR_LIST_FILE)}" + (f" ({MONITOR_LIST_FILE})" if MONITOR_LIST_FILE else ""))
|
|
2457
|
+
print(f"* Output logging enabled:\t{not DISABLE_LOGGING}" + (f" ({FINAL_LOG_PATH})" if not DISABLE_LOGGING else ""))
|
|
2458
|
+
print(f"* Configuration file:\t\t{cfg_path}")
|
|
2459
|
+
print(f"* Dotenv file:\t\t\t{env_path or 'None'}\n")
|
|
2460
|
+
|
|
2461
|
+
# We define signal handlers only for Linux, Unix & MacOS since Windows has limited number of signals supported
|
|
2462
|
+
if platform.system() != 'Windows':
|
|
2463
|
+
signal.signal(signal.SIGUSR1, toggle_active_inactive_notifications_signal_handler)
|
|
2464
|
+
signal.signal(signal.SIGUSR2, toggle_song_notifications_signal_handler)
|
|
2465
|
+
signal.signal(signal.SIGCONT, toggle_track_notifications_signal_handler)
|
|
2466
|
+
signal.signal(signal.SIGPIPE, toggle_songs_on_loop_notifications_signal_handler)
|
|
2467
|
+
signal.signal(signal.SIGTRAP, increase_inactivity_check_signal_handler)
|
|
2468
|
+
signal.signal(signal.SIGABRT, decrease_inactivity_check_signal_handler)
|
|
2469
|
+
signal.signal(signal.SIGHUP, reload_secrets_signal_handler)
|
|
2470
|
+
|
|
2471
|
+
spotify_monitor_friend_uri(args.user_id, sp_tracks, CSV_FILE)
|
|
2472
|
+
|
|
2473
|
+
sys.stdout = stdout_bck
|
|
2474
|
+
sys.exit(0)
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
if __name__ == "__main__":
|
|
2478
|
+
main()
|