psn-monitor 1.5.1__py3-none-any.whl → 1.6__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.
- {psn_monitor-1.5.1.dist-info → psn_monitor-1.6.dist-info}/METADATA +64 -4
- psn_monitor-1.6.dist-info/RECORD +7 -0
- psn_monitor.py +613 -8
- psn_monitor-1.5.1.dist-info/RECORD +0 -7
- {psn_monitor-1.5.1.dist-info → psn_monitor-1.6.dist-info}/WHEEL +0 -0
- {psn_monitor-1.5.1.dist-info → psn_monitor-1.6.dist-info}/entry_points.txt +0 -0
- {psn_monitor-1.5.1.dist-info → psn_monitor-1.6.dist-info}/licenses/LICENSE +0 -0
- {psn_monitor-1.5.1.dist-info → psn_monitor-1.6.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: psn_monitor
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6
|
|
4
4
|
Summary: Tool implementing real-time tracking of Sony PlayStation (PSN) players activities
|
|
5
5
|
Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -32,13 +32,15 @@ psn_monitor is a tool for real-time monitoring of Sony PlayStation (PSN) players
|
|
|
32
32
|
## Features
|
|
33
33
|
|
|
34
34
|
- Real-time tracking of PlayStation users' gaming activity (including detection when a user gets online/offline or plays games)
|
|
35
|
+
- Detailed user information display mode providing comprehensive PlayStation profile insights
|
|
35
36
|
- Basic statistics for user activity (duration in different states, time spent playing a game, overall time and number of games played in a session etc.)
|
|
36
37
|
- Email notifications for various events (player gets online/offline, starts/finishes/changes a game, errors)
|
|
37
38
|
- Saving all user activities with timestamps to a CSV file
|
|
38
39
|
- Possibility to control the running copy of the script via signals
|
|
40
|
+
- Functional, procedural Python (minimal OOP)
|
|
39
41
|
|
|
40
42
|
<p align="center">
|
|
41
|
-
<img src="https://raw.githubusercontent.com/misiektoja/psn_monitor/refs/heads/main/assets/psn_monitor.png" alt="psn_monitor_screenshot" width="
|
|
43
|
+
<img src="https://raw.githubusercontent.com/misiektoja/psn_monitor/refs/heads/main/assets/psn_monitor.png" alt="psn_monitor_screenshot" width="90%"/>
|
|
42
44
|
</p>
|
|
43
45
|
|
|
44
46
|
<a id="table-of-contents"></a>
|
|
@@ -48,6 +50,7 @@ psn_monitor is a tool for real-time monitoring of Sony PlayStation (PSN) players
|
|
|
48
50
|
2. [Installation](#installation)
|
|
49
51
|
* [Install from PyPI](#install-from-pypi)
|
|
50
52
|
* [Manual Installation](#manual-installation)
|
|
53
|
+
* [Upgrading](#upgrading)
|
|
51
54
|
3. [Quick Start](#quick-start)
|
|
52
55
|
4. [Configuration](#configuration)
|
|
53
56
|
* [Configuration File](#configuration-file)
|
|
@@ -57,6 +60,7 @@ psn_monitor is a tool for real-time monitoring of Sony PlayStation (PSN) players
|
|
|
57
60
|
* [SMTP Settings](#smtp-settings)
|
|
58
61
|
* [Storing Secrets](#storing-secrets)
|
|
59
62
|
5. [Usage](#usage)
|
|
63
|
+
* [User Information Display Mode](#user-information-display-mode)
|
|
60
64
|
* [Monitoring Mode](#monitoring-mode)
|
|
61
65
|
* [Email Notifications](#email-notifications)
|
|
62
66
|
* [CSV Export](#csv-export)
|
|
@@ -74,8 +78,8 @@ psn_monitor is a tool for real-time monitoring of Sony PlayStation (PSN) players
|
|
|
74
78
|
|
|
75
79
|
Tested on:
|
|
76
80
|
|
|
77
|
-
* **macOS**: Ventura, Sonoma, Sequoia
|
|
78
|
-
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm), Ubuntu 24, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
81
|
+
* **macOS**: Ventura, Sonoma, Sequoia, Tahoe
|
|
82
|
+
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm, Trixie), Ubuntu 24/25, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
79
83
|
* **Windows**: 10, 11
|
|
80
84
|
|
|
81
85
|
It should work on other versions of macOS, Linux, Unix and Windows as well.
|
|
@@ -107,6 +111,17 @@ Alternatively, from the downloaded *[requirements.txt](https://raw.githubusercon
|
|
|
107
111
|
pip install -r requirements.txt
|
|
108
112
|
```
|
|
109
113
|
|
|
114
|
+
<a id="upgrading"></a>
|
|
115
|
+
### Upgrading
|
|
116
|
+
|
|
117
|
+
To upgrade to the latest version when installed from PyPI:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
pip install psn_monitor -U
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If you installed manually, download the newest *[psn_monitor.py](https://raw.githubusercontent.com/misiektoja/psn_monitor/refs/heads/main/psn_monitor.py)* file to replace your existing installation.
|
|
124
|
+
|
|
110
125
|
<a id="quick-start"></a>
|
|
111
126
|
## Quick Start
|
|
112
127
|
|
|
@@ -241,6 +256,51 @@ As a fallback, you can also store secrets in the configuration file or source co
|
|
|
241
256
|
<a id="usage"></a>
|
|
242
257
|
## Usage
|
|
243
258
|
|
|
259
|
+
<a id="user-information-display-mode"></a>
|
|
260
|
+
### User Information Display Mode
|
|
261
|
+
|
|
262
|
+
The tool provides a detailed user information display mode that shows comprehensive PlayStation profile insights. This mode displays information once and then exits (it does not run continuous monitoring).
|
|
263
|
+
|
|
264
|
+
To get detailed user information, use the `-i` or `--info` flag:
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
psn_monitor <psn_user_id> -i
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
This displays:
|
|
271
|
+
- PlayStation/PSN IDs
|
|
272
|
+
- Online status and availability to play
|
|
273
|
+
- Platform information
|
|
274
|
+
- PS+ subscription status
|
|
275
|
+
- Verification status
|
|
276
|
+
- About me section
|
|
277
|
+
- Languages
|
|
278
|
+
- Friendship relation and mutual friends count
|
|
279
|
+
- Profile URL
|
|
280
|
+
- Recently played games with last played date and total play time
|
|
281
|
+
|
|
282
|
+
To also display trophy summary and list of most recently earned trophies, add the `--trophies` flag:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
psn_monitor <psn_user_id> -i --trophies
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
To disable fetching the recently played games list (faster execution), use the `--no-recent-games` flag:
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
psn_monitor <psn_user_id> -i --no-recent-games
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
You can combine both flags:
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
psn_monitor <psn_user_id> -i --trophies --no-recent-games
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
<p align="center">
|
|
301
|
+
<img src="https://raw.githubusercontent.com/misiektoja/psn_monitor/refs/heads/main/assets/psn_monitor_info.png" alt="psn_monitor_info" width="90%"/>
|
|
302
|
+
</p>
|
|
303
|
+
|
|
244
304
|
<a id="monitoring-mode"></a>
|
|
245
305
|
### Monitoring Mode
|
|
246
306
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
psn_monitor.py,sha256=LUOMdhCCK5V0fx-2bkW0SfqrLjywOYc6-IwON6SNZg8,78993
|
|
2
|
+
psn_monitor-1.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
psn_monitor-1.6.dist-info/METADATA,sha256=yf8X9sbc4ocZF5i85LkmN7Jxn7QBriTkqyFf1wGEFCU,14458
|
|
4
|
+
psn_monitor-1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
psn_monitor-1.6.dist-info/entry_points.txt,sha256=2yXV06LBmzhWnvc4bKuEblezaWSHEWKdB2HClGXZlnk,49
|
|
6
|
+
psn_monitor-1.6.dist-info/top_level.txt,sha256=IG37NL5yiB0wgx_MN-L47SDxKdqRXzjIqBpkU7JiISE,12
|
|
7
|
+
psn_monitor-1.6.dist-info/RECORD,,
|
psn_monitor.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Author: Michal Szymanski <misiektoja-github@rm-rf.ninja>
|
|
4
|
-
v1.
|
|
4
|
+
v1.6
|
|
5
5
|
|
|
6
6
|
Tool implementing real-time tracking of Sony PlayStation (PSN) players activities:
|
|
7
7
|
https://github.com/misiektoja/psn_monitor/
|
|
@@ -16,7 +16,7 @@ tzlocal (optional)
|
|
|
16
16
|
python-dotenv (optional)
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
VERSION = "1.
|
|
19
|
+
VERSION = "1.6"
|
|
20
20
|
|
|
21
21
|
# ---------------------------
|
|
22
22
|
# CONFIGURATION SECTION START
|
|
@@ -779,6 +779,496 @@ def resolve_executable(path):
|
|
|
779
779
|
raise FileNotFoundError(f"Could not find executable '{path}'")
|
|
780
780
|
|
|
781
781
|
|
|
782
|
+
# Normalizes Unicode punctuation, symbols and spacing in a string to plain ASCII
|
|
783
|
+
def normalize_ascii(s):
|
|
784
|
+
if not isinstance(s, str):
|
|
785
|
+
return s
|
|
786
|
+
# punctuation & symbols to ASCII
|
|
787
|
+
s = (s.replace("\u2018", "'").replace("\u2019", "'") # ‘ ’ -> '
|
|
788
|
+
.replace("\u201C", '"').replace("\u201D", '"') # “ ” -> "
|
|
789
|
+
.replace("\u2013", "-") # – -> -
|
|
790
|
+
.replace("\u2026", "...") # … -> ...
|
|
791
|
+
.replace("\u00A0", " ")) # NBSP -> space
|
|
792
|
+
# remove trademark symbols
|
|
793
|
+
for ch in ("\u00AE", "\u2122"): # ® ™
|
|
794
|
+
s = s.replace(ch, "")
|
|
795
|
+
# collapse doubled single quotes that often appear after smart-quote normalization
|
|
796
|
+
s = s.replace("''", "'")
|
|
797
|
+
# collapse multiple spaces
|
|
798
|
+
while " " in s:
|
|
799
|
+
s = s.replace(" ", " ")
|
|
800
|
+
return s.strip()
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
# Prints the last N earned trophies across titles with game, type and earn date
|
|
804
|
+
def print_last_earned_trophies(psn_user, max_items=5, title_limit=15):
|
|
805
|
+
PT = None
|
|
806
|
+
try:
|
|
807
|
+
from psnawp_api.models.trophies import PlatformType as PT # 3.x
|
|
808
|
+
except Exception:
|
|
809
|
+
PT = None # fallback to string platforms later
|
|
810
|
+
|
|
811
|
+
def _get(obj, *names, default=None):
|
|
812
|
+
for n in names:
|
|
813
|
+
if hasattr(obj, n):
|
|
814
|
+
v = getattr(obj, n)
|
|
815
|
+
if v is not None:
|
|
816
|
+
return v
|
|
817
|
+
return default
|
|
818
|
+
|
|
819
|
+
def _platforms_to_try(title):
|
|
820
|
+
raw = getattr(title, "platform", None)
|
|
821
|
+
raw_val = getattr(raw, "value", raw)
|
|
822
|
+
s = (str(raw_val).lower() if raw_val else "")
|
|
823
|
+
if PT:
|
|
824
|
+
if "ps5" in s:
|
|
825
|
+
return [PT.PS5, PT.PS4]
|
|
826
|
+
if "ps4" in s:
|
|
827
|
+
return [PT.PS4, PT.PS5]
|
|
828
|
+
return [PT.PS5, PT.PS4]
|
|
829
|
+
# string fallback
|
|
830
|
+
if "ps5" in s:
|
|
831
|
+
return ["ps5", "ps4"]
|
|
832
|
+
if "ps4" in s:
|
|
833
|
+
return ["ps4", "ps5"]
|
|
834
|
+
return ["ps5", "ps4"]
|
|
835
|
+
|
|
836
|
+
def _earn_dt(tr):
|
|
837
|
+
return _get(tr, "earned_date_time", "earnedDateTime", default=None)
|
|
838
|
+
|
|
839
|
+
def _trophy_type_str(tr):
|
|
840
|
+
raw = _get(tr, "trophy_type", "trophyType", default=None)
|
|
841
|
+
if raw is None:
|
|
842
|
+
return "UNKNOWN"
|
|
843
|
+
if hasattr(raw, "name"):
|
|
844
|
+
return raw.name
|
|
845
|
+
return str(raw).upper()
|
|
846
|
+
|
|
847
|
+
# title-name resolver (cache)
|
|
848
|
+
_title_name_cache = {}
|
|
849
|
+
|
|
850
|
+
def _resolve_title_name(npcomm, platform):
|
|
851
|
+
key = (npcomm, str(platform))
|
|
852
|
+
if key in _title_name_cache:
|
|
853
|
+
return _title_name_cache[key]
|
|
854
|
+
|
|
855
|
+
def _first_name_like(obj):
|
|
856
|
+
# Try common fields first
|
|
857
|
+
for fld in ("trophy_title_name", "trophyTitleName", "title_name", "titleName", "name"):
|
|
858
|
+
if hasattr(obj, fld):
|
|
859
|
+
val = getattr(obj, fld)
|
|
860
|
+
if isinstance(val, str) and val.strip():
|
|
861
|
+
return val.strip()
|
|
862
|
+
# Fallback: scan attributes that look like "*name"
|
|
863
|
+
for attr in dir(obj):
|
|
864
|
+
if attr.startswith("_"):
|
|
865
|
+
continue
|
|
866
|
+
if "name" in attr.lower():
|
|
867
|
+
try:
|
|
868
|
+
val = getattr(obj, attr)
|
|
869
|
+
except Exception:
|
|
870
|
+
continue
|
|
871
|
+
if isinstance(val, str) and val.strip():
|
|
872
|
+
return val.strip()
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
name = None
|
|
876
|
+
|
|
877
|
+
# A) groups often carry the title name
|
|
878
|
+
try:
|
|
879
|
+
for g in psn_user.trophy_groups(np_communication_id=npcomm, platform=platform):
|
|
880
|
+
name = _first_name_like(g)
|
|
881
|
+
if name:
|
|
882
|
+
break
|
|
883
|
+
except Exception:
|
|
884
|
+
pass
|
|
885
|
+
|
|
886
|
+
# B) per-title summary
|
|
887
|
+
if not name:
|
|
888
|
+
try:
|
|
889
|
+
summ = psn_user.trophy_summary(np_communication_id=npcomm, platform=platform)
|
|
890
|
+
name = _first_name_like(summ)
|
|
891
|
+
except Exception:
|
|
892
|
+
pass
|
|
893
|
+
|
|
894
|
+
# C) scan titles
|
|
895
|
+
if not name:
|
|
896
|
+
try:
|
|
897
|
+
for tt in psn_user.trophy_titles(limit=title_limit):
|
|
898
|
+
nc = getattr(tt, "np_communication_id", None) or getattr(tt, "npCommunicationId", None)
|
|
899
|
+
if nc == npcomm:
|
|
900
|
+
name = _first_name_like(tt)
|
|
901
|
+
if name:
|
|
902
|
+
break
|
|
903
|
+
except Exception:
|
|
904
|
+
pass
|
|
905
|
+
|
|
906
|
+
if not name:
|
|
907
|
+
name = npcomm # last resort
|
|
908
|
+
|
|
909
|
+
_title_name_cache[key] = name
|
|
910
|
+
return name
|
|
911
|
+
# -------------------------------------
|
|
912
|
+
|
|
913
|
+
items = []
|
|
914
|
+
|
|
915
|
+
# 1) list titles (no special args for cross-version compat)
|
|
916
|
+
try:
|
|
917
|
+
titles_iter = psn_user.trophy_titles(limit=title_limit)
|
|
918
|
+
except Exception:
|
|
919
|
+
titles_iter = []
|
|
920
|
+
|
|
921
|
+
for tt in titles_iter:
|
|
922
|
+
npcomm = _get(tt, "np_communication_id", "npCommunicationId", default=None)
|
|
923
|
+
if not npcomm:
|
|
924
|
+
continue
|
|
925
|
+
|
|
926
|
+
for plat in _platforms_to_try(tt):
|
|
927
|
+
try:
|
|
928
|
+
it = psn_user.trophies(
|
|
929
|
+
np_communication_id=npcomm,
|
|
930
|
+
platform=plat,
|
|
931
|
+
include_progress=True,
|
|
932
|
+
trophy_group_id="all",
|
|
933
|
+
)
|
|
934
|
+
except Exception:
|
|
935
|
+
continue
|
|
936
|
+
|
|
937
|
+
got_any_for_title = False
|
|
938
|
+
for tr in it:
|
|
939
|
+
got_any_for_title = True
|
|
940
|
+
if not getattr(tr, "earned", False):
|
|
941
|
+
continue
|
|
942
|
+
dt = _earn_dt(tr)
|
|
943
|
+
if not dt:
|
|
944
|
+
continue
|
|
945
|
+
|
|
946
|
+
game_name = normalize_ascii(_resolve_title_name(npcomm, plat))
|
|
947
|
+
ttype = _trophy_type_str(tr)
|
|
948
|
+
tname = _get(tr, "trophy_name", "trophyName", default=None)
|
|
949
|
+
if not tname:
|
|
950
|
+
tname = "(hidden)" if getattr(tr, "hidden", False) else "(unknown)"
|
|
951
|
+
tname = normalize_ascii(tname)
|
|
952
|
+
|
|
953
|
+
items.append((dt, game_name, ttype, tname))
|
|
954
|
+
|
|
955
|
+
if got_any_for_title:
|
|
956
|
+
break # this platform works for this title
|
|
957
|
+
|
|
958
|
+
if len(items) >= max_items:
|
|
959
|
+
break
|
|
960
|
+
|
|
961
|
+
if not items:
|
|
962
|
+
print("- (no recent trophies found or trophy visibility is restricted)")
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
# 2) sort & print
|
|
966
|
+
try:
|
|
967
|
+
items.sort(key=lambda x: x[0], reverse=True)
|
|
968
|
+
except Exception:
|
|
969
|
+
def _ts(dt):
|
|
970
|
+
try:
|
|
971
|
+
return int(dt.timestamp())
|
|
972
|
+
except Exception:
|
|
973
|
+
return -1
|
|
974
|
+
items.sort(key=lambda x: _ts(x[0]), reverse=True)
|
|
975
|
+
|
|
976
|
+
for dt, game, ttype, tname in items[:max_items]:
|
|
977
|
+
try:
|
|
978
|
+
ts = int(dt.timestamp())
|
|
979
|
+
dt_fmt = get_date_from_ts(ts)
|
|
980
|
+
except Exception:
|
|
981
|
+
dt_fmt = "n/a"
|
|
982
|
+
print(f"- {dt_fmt} | {game} | {ttype} | {tname}")
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# Gets detailed user information and displays it (for -i/--info mode)
|
|
986
|
+
def get_user_info(psn_user_id, include_trophies=False, show_recent_games=True):
|
|
987
|
+
print(f"* Fetching details for PlayStation user '{psn_user_id}'... this may take a moment\n")
|
|
988
|
+
|
|
989
|
+
try:
|
|
990
|
+
psnawp = PSNAWP(PSN_NPSSO)
|
|
991
|
+
psn_user = psnawp.user(online_id=psn_user_id)
|
|
992
|
+
accountid = psn_user.account_id
|
|
993
|
+
profile = psn_user.profile()
|
|
994
|
+
aboutme = profile.get("aboutMe")
|
|
995
|
+
isplus = profile.get("isPlus")
|
|
996
|
+
langs = profile.get("languages") or []
|
|
997
|
+
is_verified = profile.get("isOfficiallyVerified")
|
|
998
|
+
fs = psn_user.friendship()
|
|
999
|
+
share = psn_user.get_shareable_profile_link()
|
|
1000
|
+
except Exception as e:
|
|
1001
|
+
print(f"* Error: {e}")
|
|
1002
|
+
sys.exit(1)
|
|
1003
|
+
|
|
1004
|
+
try:
|
|
1005
|
+
psn_user_presence = psn_user.get_presence()
|
|
1006
|
+
except Exception as e:
|
|
1007
|
+
print(f"* Error: Cannot get presence for user {psn_user_id}: {e}")
|
|
1008
|
+
sys.exit(1)
|
|
1009
|
+
|
|
1010
|
+
status = psn_user_presence["basicPresence"]["primaryPlatformInfo"].get("onlineStatus")
|
|
1011
|
+
|
|
1012
|
+
if not status:
|
|
1013
|
+
print(f"* Error: Cannot get status for user {psn_user_id}")
|
|
1014
|
+
sys.exit(1)
|
|
1015
|
+
|
|
1016
|
+
status = str(status).lower()
|
|
1017
|
+
|
|
1018
|
+
psn_platform = psn_user_presence["basicPresence"]["primaryPlatformInfo"].get("platform")
|
|
1019
|
+
psn_platform = str(psn_platform).upper() if psn_platform else ""
|
|
1020
|
+
lastonline = psn_user_presence["basicPresence"]["primaryPlatformInfo"].get("lastOnlineDate")
|
|
1021
|
+
availability = psn_user_presence["basicPresence"].get("availability")
|
|
1022
|
+
|
|
1023
|
+
lastonline_dt = convert_iso_str_to_datetime(lastonline)
|
|
1024
|
+
if lastonline_dt:
|
|
1025
|
+
lastonline_ts = int(lastonline_dt.timestamp())
|
|
1026
|
+
else:
|
|
1027
|
+
lastonline_ts = 0
|
|
1028
|
+
|
|
1029
|
+
gametitleinfolist = psn_user_presence["basicPresence"].get("gameTitleInfoList")
|
|
1030
|
+
game_name = ""
|
|
1031
|
+
launchplatform = ""
|
|
1032
|
+
|
|
1033
|
+
if gametitleinfolist:
|
|
1034
|
+
game_name_raw = gametitleinfolist[0].get("titleName")
|
|
1035
|
+
game_name = normalize_ascii(game_name_raw) if game_name_raw else ""
|
|
1036
|
+
launchplatform = gametitleinfolist[0].get("launchPlatform")
|
|
1037
|
+
launchplatform = str(launchplatform).upper()
|
|
1038
|
+
|
|
1039
|
+
psn_last_status_file = f"psn_{psn_user_id}_last_status.json"
|
|
1040
|
+
status_ts_old = int(time.time())
|
|
1041
|
+
|
|
1042
|
+
if os.path.isfile(psn_last_status_file):
|
|
1043
|
+
try:
|
|
1044
|
+
with open(psn_last_status_file, 'r', encoding="utf-8") as f:
|
|
1045
|
+
last_status_read = json.load(f)
|
|
1046
|
+
if last_status_read:
|
|
1047
|
+
last_status_ts = last_status_read[0]
|
|
1048
|
+
last_status = last_status_read[1]
|
|
1049
|
+
|
|
1050
|
+
if lastonline_ts and status == "offline":
|
|
1051
|
+
if lastonline_ts >= last_status_ts:
|
|
1052
|
+
status_ts_old = lastonline_ts
|
|
1053
|
+
else:
|
|
1054
|
+
status_ts_old = last_status_ts
|
|
1055
|
+
elif not lastonline_ts and status == "offline":
|
|
1056
|
+
status_ts_old = last_status_ts
|
|
1057
|
+
elif status and status != "offline" and status == last_status:
|
|
1058
|
+
status_ts_old = last_status_ts
|
|
1059
|
+
except Exception:
|
|
1060
|
+
if lastonline_ts and status == "offline":
|
|
1061
|
+
status_ts_old = lastonline_ts
|
|
1062
|
+
else:
|
|
1063
|
+
if lastonline_ts and status == "offline":
|
|
1064
|
+
status_ts_old = lastonline_ts
|
|
1065
|
+
|
|
1066
|
+
print(f"PlayStation ID:\t\t\t{psn_user_id}")
|
|
1067
|
+
print(f"PSN account ID:\t\t\t{accountid}")
|
|
1068
|
+
print(f"\nStatus:\t\t\t\t{str(status).upper()}")
|
|
1069
|
+
if availability:
|
|
1070
|
+
available_str = "Yes" if availability == "availableToPlay" else "No"
|
|
1071
|
+
print(f"Available to play:\t\t{available_str}")
|
|
1072
|
+
|
|
1073
|
+
psn_platform_displayed = False
|
|
1074
|
+
if psn_platform:
|
|
1075
|
+
print(f"\nPlatform:\t\t\t{psn_platform}")
|
|
1076
|
+
psn_platform_displayed = True
|
|
1077
|
+
|
|
1078
|
+
if not psn_platform_displayed:
|
|
1079
|
+
print()
|
|
1080
|
+
print(f"PS+ user:\t\t\t{isplus}")
|
|
1081
|
+
|
|
1082
|
+
# an official account belonging to a recognised developer, publisher, community manager or another official role
|
|
1083
|
+
if is_verified is not None:
|
|
1084
|
+
print(f"Verified:\t\t\t{is_verified}")
|
|
1085
|
+
|
|
1086
|
+
newline_needed = False
|
|
1087
|
+
|
|
1088
|
+
if aboutme:
|
|
1089
|
+
print(f"\nAbout me:\t\t\t{aboutme}")
|
|
1090
|
+
newline_needed = True
|
|
1091
|
+
|
|
1092
|
+
if langs:
|
|
1093
|
+
prefix = "\n" if not newline_needed else ""
|
|
1094
|
+
print(f"{prefix}Languages:\t\t\t{', '.join(langs)}")
|
|
1095
|
+
|
|
1096
|
+
try:
|
|
1097
|
+
relation = fs.get("friendRelation")
|
|
1098
|
+
print(f"\nRelation:\t\t\t{relation}")
|
|
1099
|
+
|
|
1100
|
+
if relation == "friend":
|
|
1101
|
+
mf = fs.get("mutualFriendsCount")
|
|
1102
|
+
if isinstance(mf, int) and mf >= 0:
|
|
1103
|
+
print(f"Mutual friends:\t\t\t{mf}")
|
|
1104
|
+
elif mf is None:
|
|
1105
|
+
print("Mutual friends:\t\t\tunknown")
|
|
1106
|
+
else:
|
|
1107
|
+
print("Mutual friends:\t\t\thidden")
|
|
1108
|
+
else:
|
|
1109
|
+
# Don't print mutual friends at all
|
|
1110
|
+
pass
|
|
1111
|
+
|
|
1112
|
+
except Exception:
|
|
1113
|
+
pass
|
|
1114
|
+
|
|
1115
|
+
try:
|
|
1116
|
+
print(f"\nProfile URL:\t\t\t{share.get('shareUrl')}")
|
|
1117
|
+
# print(f"Profile QR image:\t\t{share.get('shareImageUrl')}")
|
|
1118
|
+
except Exception:
|
|
1119
|
+
pass
|
|
1120
|
+
|
|
1121
|
+
if status == "offline" and status_ts_old > 0:
|
|
1122
|
+
last_status_dt_str = get_date_from_ts(status_ts_old)
|
|
1123
|
+
print(f"\n* Last time user was available:\t{last_status_dt_str}")
|
|
1124
|
+
print(f"* User is OFFLINE for:\t\t{calculate_timespan(now_local(), int(status_ts_old), show_seconds=False)}")
|
|
1125
|
+
elif status != "offline":
|
|
1126
|
+
if os.path.isfile(psn_last_status_file):
|
|
1127
|
+
try:
|
|
1128
|
+
with open(psn_last_status_file, 'r', encoding="utf-8") as f:
|
|
1129
|
+
last_status_read = json.load(f)
|
|
1130
|
+
if last_status_read and last_status_read[1] == status:
|
|
1131
|
+
print(f"* User is {str(status).upper()} for:\t\t{calculate_timespan(now_local(), int(last_status_read[0]), show_seconds=False)}")
|
|
1132
|
+
except Exception:
|
|
1133
|
+
pass
|
|
1134
|
+
|
|
1135
|
+
# Show trophy summary and last earned trophies only if requested
|
|
1136
|
+
if include_trophies:
|
|
1137
|
+
try:
|
|
1138
|
+
print(f"\n* Getting trophy summary ...")
|
|
1139
|
+
ts = psn_user.trophy_summary()
|
|
1140
|
+
et = ts.earned_trophies
|
|
1141
|
+
prog = int(ts.progress) if ts.progress is not None else 0
|
|
1142
|
+
print(f"\nTrophy level:\t\t\t{ts.trophy_level} ({prog}% to next, tier {ts.tier})")
|
|
1143
|
+
print(
|
|
1144
|
+
"Trophies earned:\t\t"
|
|
1145
|
+
f"{et.platinum} Platinum, {et.gold} Gold, {et.silver} Silver, {et.bronze} Bronze "
|
|
1146
|
+
f"({et.platinum + et.gold + et.silver + et.bronze} total)"
|
|
1147
|
+
)
|
|
1148
|
+
except Exception:
|
|
1149
|
+
pass
|
|
1150
|
+
|
|
1151
|
+
num_trophies = 5
|
|
1152
|
+
try:
|
|
1153
|
+
print(f"\n* Getting list of last {num_trophies} earned trophies ...\n")
|
|
1154
|
+
print_last_earned_trophies(psn_user, max_items=num_trophies, title_limit=15)
|
|
1155
|
+
except Exception:
|
|
1156
|
+
pass
|
|
1157
|
+
|
|
1158
|
+
# Show recently played games only if requested
|
|
1159
|
+
if show_recent_games:
|
|
1160
|
+
try:
|
|
1161
|
+
# Helper function to compact duration format, convert "X day(s), HH:MM:SS" to "Xd HH:MM:SS"
|
|
1162
|
+
def _compact_duration(s):
|
|
1163
|
+
if not s:
|
|
1164
|
+
return "0:00:00"
|
|
1165
|
+
s = str(s).strip()
|
|
1166
|
+
|
|
1167
|
+
if "day" in s.lower():
|
|
1168
|
+
try:
|
|
1169
|
+
if "," in s:
|
|
1170
|
+
parts = s.split(",", 1)
|
|
1171
|
+
days_part = parts[0].strip() # "1 day" / "2 days"
|
|
1172
|
+
time_part = parts[1].strip() # "23:47:54"
|
|
1173
|
+
d = int(days_part.split()[0])
|
|
1174
|
+
return f"{d}d {time_part}"
|
|
1175
|
+
else:
|
|
1176
|
+
# No comma, try to extract days anyway (unlikely but handle it)
|
|
1177
|
+
words = s.split()
|
|
1178
|
+
if len(words) >= 2 and words[1].lower().startswith("day"):
|
|
1179
|
+
d = int(words[0])
|
|
1180
|
+
if len(words) > 2:
|
|
1181
|
+
time_part = " ".join(words[2:])
|
|
1182
|
+
return f"{d}d {time_part}"
|
|
1183
|
+
return f"{d}d"
|
|
1184
|
+
except (ValueError, IndexError):
|
|
1185
|
+
return s # fallback to original if parsing fails
|
|
1186
|
+
return s
|
|
1187
|
+
|
|
1188
|
+
def _shorten_middle(s, max_len, ellipsis="..."):
|
|
1189
|
+
if s is None:
|
|
1190
|
+
return ""
|
|
1191
|
+
s = str(s)
|
|
1192
|
+
if len(s) <= max_len:
|
|
1193
|
+
return s
|
|
1194
|
+
keep = max_len - len(ellipsis)
|
|
1195
|
+
if keep <= 0:
|
|
1196
|
+
return ellipsis[:max_len]
|
|
1197
|
+
left = keep // 2
|
|
1198
|
+
right = keep - left
|
|
1199
|
+
return f"{s[:left]}{ellipsis}{s[-right:]}"
|
|
1200
|
+
|
|
1201
|
+
recent_entries = []
|
|
1202
|
+
print(f"\n* Getting list of recently played games ...")
|
|
1203
|
+
for i, t in enumerate(psn_user.title_stats(limit=10, page_size=50), 1):
|
|
1204
|
+
if not t:
|
|
1205
|
+
continue
|
|
1206
|
+
name_raw = t.name or "(unknown)"
|
|
1207
|
+
name = normalize_ascii(name_raw)
|
|
1208
|
+
cat = getattr(getattr(t, "category", None), "name", "UNKNOWN")
|
|
1209
|
+
last_played = (
|
|
1210
|
+
get_date_from_ts(int(t.last_played_date_time.timestamp()))
|
|
1211
|
+
if t.last_played_date_time else "n/a"
|
|
1212
|
+
)
|
|
1213
|
+
total_raw = str(t.play_duration) if t.play_duration else "0:00:00"
|
|
1214
|
+
# Compact duration immediately to ensure it fits in the column
|
|
1215
|
+
total = _compact_duration(total_raw)
|
|
1216
|
+
recent_entries.append(f"Recent #{i}:\t\t\t{name} | {cat} | last played {last_played} | total {total}")
|
|
1217
|
+
|
|
1218
|
+
# Decide column widths based on terminal size
|
|
1219
|
+
try:
|
|
1220
|
+
import shutil
|
|
1221
|
+
term_width = shutil.get_terminal_size(fallback=(100, 24)).columns
|
|
1222
|
+
except Exception:
|
|
1223
|
+
term_width = 100
|
|
1224
|
+
|
|
1225
|
+
w_num = 3
|
|
1226
|
+
w_platform = 8
|
|
1227
|
+
w_last = 24
|
|
1228
|
+
w_total = 14 # fits "999d 23:59:59" (14 chars) after compacting "X day(s), HH:MM:SS" -> "Xd HH:MM:SS"
|
|
1229
|
+
fixed = 1 + w_num + 2 + w_platform + 2 + w_last + 2 + w_total
|
|
1230
|
+
w_title = max(24, term_width - fixed)
|
|
1231
|
+
|
|
1232
|
+
# Only print the table if we have entries
|
|
1233
|
+
if recent_entries:
|
|
1234
|
+
print()
|
|
1235
|
+
hdr = f"{'#'.ljust(w_num)} {'Title'.ljust(w_title)} {'Platform'.ljust(w_platform)} {'Last played'.ljust(w_last)} {'Total'.ljust(w_total)}"
|
|
1236
|
+
sep = f"{'-' * w_num} {'-' * w_title} {'-' * w_platform} {'-' * w_last} {'-' * w_total}"
|
|
1237
|
+
print(hdr)
|
|
1238
|
+
print(sep)
|
|
1239
|
+
|
|
1240
|
+
for i, entry in enumerate(recent_entries, 1):
|
|
1241
|
+
try:
|
|
1242
|
+
_, rest = entry.split(":", 1)
|
|
1243
|
+
parts = rest.strip().split("|")
|
|
1244
|
+
name = parts[0].strip()
|
|
1245
|
+
cat = parts[1].strip()
|
|
1246
|
+
last_played = parts[2].replace("last played", "").strip()
|
|
1247
|
+
total = _compact_duration(parts[3].replace("total", "").strip())
|
|
1248
|
+
except Exception:
|
|
1249
|
+
# If parsing ever fails, print raw line as a fallback
|
|
1250
|
+
print(entry)
|
|
1251
|
+
continue
|
|
1252
|
+
|
|
1253
|
+
name_fmt = _shorten_middle(name, w_title)
|
|
1254
|
+
row = (
|
|
1255
|
+
f"{str(i).ljust(w_num)} "
|
|
1256
|
+
f"{name_fmt.ljust(w_title)} "
|
|
1257
|
+
f"{cat.ljust(w_platform)} "
|
|
1258
|
+
f"{last_played.ljust(w_last)} "
|
|
1259
|
+
f"{total.ljust(w_total)}"
|
|
1260
|
+
)
|
|
1261
|
+
print(row)
|
|
1262
|
+
except Exception:
|
|
1263
|
+
pass
|
|
1264
|
+
|
|
1265
|
+
if game_name:
|
|
1266
|
+
launchplatform_str = ""
|
|
1267
|
+
if launchplatform:
|
|
1268
|
+
launchplatform_str = f" ({launchplatform})"
|
|
1269
|
+
print(f"\nUser is currently in-game:\t{game_name}{launchplatform_str}")
|
|
1270
|
+
|
|
1271
|
+
|
|
782
1272
|
# Main function that monitors gaming activity of the specified PSN user
|
|
783
1273
|
def psn_monitor_user(psn_user_id, csv_file_name):
|
|
784
1274
|
|
|
@@ -801,6 +1291,8 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
801
1291
|
except Exception as e:
|
|
802
1292
|
print(f"* Error: {e}")
|
|
803
1293
|
|
|
1294
|
+
print("Sneaking into PlayStation like a ninja ... (be patient, secrets take time)\n")
|
|
1295
|
+
|
|
804
1296
|
try:
|
|
805
1297
|
psnawp = PSNAWP(PSN_NPSSO)
|
|
806
1298
|
psn_user = psnawp.user(online_id=psn_user_id)
|
|
@@ -808,6 +1300,10 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
808
1300
|
profile = psn_user.profile()
|
|
809
1301
|
aboutme = profile.get("aboutMe")
|
|
810
1302
|
isplus = profile.get("isPlus")
|
|
1303
|
+
langs = profile.get("languages") or []
|
|
1304
|
+
is_verified = profile.get("isOfficiallyVerified")
|
|
1305
|
+
fs = psn_user.friendship()
|
|
1306
|
+
share = psn_user.get_shareable_profile_link()
|
|
811
1307
|
except Exception as e:
|
|
812
1308
|
print("* Error:", e)
|
|
813
1309
|
sys.exit(1)
|
|
@@ -827,19 +1323,23 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
827
1323
|
status = str(status).lower()
|
|
828
1324
|
|
|
829
1325
|
psn_platform = psn_user_presence["basicPresence"]["primaryPlatformInfo"].get("platform")
|
|
830
|
-
psn_platform = str(psn_platform).upper()
|
|
1326
|
+
psn_platform = str(psn_platform).upper() if psn_platform else ""
|
|
831
1327
|
lastonline = psn_user_presence["basicPresence"]["primaryPlatformInfo"].get("lastOnlineDate")
|
|
1328
|
+
availability = psn_user_presence["basicPresence"].get("availability")
|
|
832
1329
|
|
|
833
1330
|
lastonline_dt = convert_iso_str_to_datetime(lastonline)
|
|
834
1331
|
if lastonline_dt:
|
|
835
1332
|
lastonline_ts = int(lastonline_dt.timestamp())
|
|
836
1333
|
else:
|
|
837
1334
|
lastonline_ts = 0
|
|
1335
|
+
|
|
838
1336
|
gametitleinfolist = psn_user_presence["basicPresence"].get("gameTitleInfoList")
|
|
839
1337
|
game_name = ""
|
|
840
1338
|
launchplatform = ""
|
|
1339
|
+
|
|
841
1340
|
if gametitleinfolist:
|
|
842
|
-
|
|
1341
|
+
game_name_raw = gametitleinfolist[0].get("titleName")
|
|
1342
|
+
game_name = normalize_ascii(game_name_raw) if game_name_raw else ""
|
|
843
1343
|
launchplatform = gametitleinfolist[0].get("launchPlatform")
|
|
844
1344
|
launchplatform = str(launchplatform).upper()
|
|
845
1345
|
|
|
@@ -905,12 +1405,57 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
905
1405
|
print(f"PSN account ID:\t\t\t{accountid}")
|
|
906
1406
|
|
|
907
1407
|
print(f"\nStatus:\t\t\t\t{str(status).upper()}")
|
|
1408
|
+
if availability:
|
|
1409
|
+
available_str = "Yes" if availability == "availableToPlay" else "No"
|
|
1410
|
+
print(f"Available to play:\t\t{available_str}")
|
|
1411
|
+
|
|
1412
|
+
psn_platform_displayed = False
|
|
908
1413
|
if psn_platform:
|
|
909
|
-
print(f"
|
|
1414
|
+
print(f"\nPlatform:\t\t\t{psn_platform}")
|
|
1415
|
+
psn_platform_displayed = True
|
|
1416
|
+
|
|
1417
|
+
if not psn_platform_displayed:
|
|
1418
|
+
print()
|
|
910
1419
|
print(f"PS+ user:\t\t\t{isplus}")
|
|
911
1420
|
|
|
1421
|
+
# an official account belonging to a recognised developer, publisher, community manager or another official role
|
|
1422
|
+
if is_verified is not None:
|
|
1423
|
+
print(f"Verified:\t\t\t{is_verified}")
|
|
1424
|
+
|
|
1425
|
+
newline_needed = False
|
|
1426
|
+
|
|
912
1427
|
if aboutme:
|
|
913
1428
|
print(f"\nAbout me:\t\t\t{aboutme}")
|
|
1429
|
+
newline_needed = True
|
|
1430
|
+
|
|
1431
|
+
if langs:
|
|
1432
|
+
prefix = "\n" if not newline_needed else ""
|
|
1433
|
+
print(f"{prefix}Languages:\t\t\t{', '.join(langs)}")
|
|
1434
|
+
|
|
1435
|
+
try:
|
|
1436
|
+
relation = fs.get("friendRelation")
|
|
1437
|
+
print(f"\nRelation:\t\t\t{relation}")
|
|
1438
|
+
|
|
1439
|
+
if relation == "friend":
|
|
1440
|
+
mf = fs.get("mutualFriendsCount")
|
|
1441
|
+
if isinstance(mf, int) and mf >= 0:
|
|
1442
|
+
print(f"Mutual friends:\t\t\t{mf}")
|
|
1443
|
+
elif mf is None:
|
|
1444
|
+
print("Mutual friends:\t\t\tunknown")
|
|
1445
|
+
else:
|
|
1446
|
+
print("Mutual friends:\t\t\thidden")
|
|
1447
|
+
else:
|
|
1448
|
+
# Don't print mutual friends at all
|
|
1449
|
+
pass
|
|
1450
|
+
|
|
1451
|
+
except Exception:
|
|
1452
|
+
pass
|
|
1453
|
+
|
|
1454
|
+
try:
|
|
1455
|
+
print(f"\nProfile URL:\t\t\t{share.get('shareUrl')}")
|
|
1456
|
+
# print(f"Profile QR image:\t\t{share.get('shareImageUrl')}")
|
|
1457
|
+
except Exception:
|
|
1458
|
+
pass
|
|
914
1459
|
|
|
915
1460
|
if status != "offline" and game_name:
|
|
916
1461
|
launchplatform_str = ""
|
|
@@ -947,6 +1492,7 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
947
1492
|
email_sent = False
|
|
948
1493
|
|
|
949
1494
|
m_subject = m_body = ""
|
|
1495
|
+
error_streak = 0
|
|
950
1496
|
|
|
951
1497
|
def get_sleep_interval():
|
|
952
1498
|
return PSN_ACTIVE_CHECK_INTERVAL if status and status != "offline" else PSN_CHECK_INTERVAL
|
|
@@ -968,7 +1514,8 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
968
1514
|
game_name = ""
|
|
969
1515
|
launchplatform = ""
|
|
970
1516
|
if gametitleinfolist:
|
|
971
|
-
|
|
1517
|
+
game_name_raw = gametitleinfolist[0].get("titleName")
|
|
1518
|
+
game_name = normalize_ascii(game_name_raw) if game_name_raw else ""
|
|
972
1519
|
launchplatform = gametitleinfolist[0].get("launchPlatform")
|
|
973
1520
|
launchplatform = str(launchplatform).upper()
|
|
974
1521
|
if platform.system() != 'Windows':
|
|
@@ -1004,15 +1551,45 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
1004
1551
|
if platform.system() != 'Windows':
|
|
1005
1552
|
signal.alarm(0)
|
|
1006
1553
|
|
|
1007
|
-
|
|
1554
|
+
msg = str(e).lower()
|
|
1555
|
+
# Connection-related errors that can often be fixed by recreating the session
|
|
1556
|
+
connection_error = ('remote end closed connection' in msg or 'connection reset by peer' in msg or 'connection aborted' in msg)
|
|
1557
|
+
|
|
1558
|
+
if connection_error:
|
|
1008
1559
|
try:
|
|
1009
1560
|
psnawp = PSNAWP(PSN_NPSSO)
|
|
1010
1561
|
psn_user = psnawp.user(online_id=psn_user_id)
|
|
1011
1562
|
except Exception:
|
|
1012
1563
|
pass
|
|
1013
|
-
|
|
1564
|
+
error_streak += 1
|
|
1565
|
+
# For connection errors, retry quickly since they're often transient
|
|
1566
|
+
retry_delay = FUNCTION_TIMEOUT
|
|
1567
|
+
# However, if connection errors persist, it might indicate an expired NPSSO token
|
|
1568
|
+
# Send notification after 5+ consecutive connection errors
|
|
1569
|
+
if ERROR_NOTIFICATION and not email_sent and error_streak >= 5:
|
|
1570
|
+
print(f"* Multiple consecutive connection errors detected - this may indicate an expired NPSSO token, error streak {error_streak}: {e}")
|
|
1571
|
+
m_subject = f"psn_monitor: PSN NPSSO key might be expired! (user: {psn_user_id})"
|
|
1572
|
+
m_body = f"Multiple consecutive connection errors detected - this may indicate an expired NPSSO token.\n\nError: {e}\n\nError streak: {error_streak}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
1573
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1574
|
+
send_email(m_subject, m_body, "", SMTP_SSL)
|
|
1575
|
+
email_sent = True
|
|
1576
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1577
|
+
# print(f"* Connection error, recreating session and retrying in {display_time(retry_delay)}: {e}")
|
|
1578
|
+
# print_cur_ts("Timestamp:\t\t\t")
|
|
1579
|
+
time.sleep(retry_delay)
|
|
1014
1580
|
continue
|
|
1015
1581
|
|
|
1582
|
+
error_streak += 1
|
|
1583
|
+
|
|
1584
|
+
# Check for authentication errors (excluding connection errors which we already handled)
|
|
1585
|
+
likely_auth = ('401' in msg) or ('expired' in msg) or ('invalid' in msg)
|
|
1586
|
+
if likely_auth and ERROR_NOTIFICATION and not email_sent and error_streak >= 5:
|
|
1587
|
+
m_subject = f"psn_monitor: PSN NPSSO key error! (user: {psn_user_id})"
|
|
1588
|
+
m_body = f"PSN NPSSO key might not be valid anymore: {e}{get_cur_ts(nl_ch + nl_ch + 'Timestamp: ')}"
|
|
1589
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
1590
|
+
send_email(m_subject, m_body, "", SMTP_SSL)
|
|
1591
|
+
email_sent = True
|
|
1592
|
+
|
|
1016
1593
|
sleep_interval = get_sleep_interval()
|
|
1017
1594
|
print(f"* Error, retrying in {display_time(sleep_interval)}: {e}")
|
|
1018
1595
|
print_cur_ts("Timestamp:\t\t\t")
|
|
@@ -1021,6 +1598,7 @@ def psn_monitor_user(psn_user_id, csv_file_name):
|
|
|
1021
1598
|
|
|
1022
1599
|
else:
|
|
1023
1600
|
email_sent = False
|
|
1601
|
+
error_streak = 0
|
|
1024
1602
|
|
|
1025
1603
|
finally:
|
|
1026
1604
|
if platform.system() != 'Windows':
|
|
@@ -1274,6 +1852,27 @@ def main():
|
|
|
1274
1852
|
help="Send test email to verify SMTP settings"
|
|
1275
1853
|
)
|
|
1276
1854
|
|
|
1855
|
+
# User information
|
|
1856
|
+
info = parser.add_argument_group("User information")
|
|
1857
|
+
info.add_argument(
|
|
1858
|
+
"-i", "--info",
|
|
1859
|
+
dest="info_mode",
|
|
1860
|
+
action="store_true",
|
|
1861
|
+
help="Get detailed user information and display it, then exit"
|
|
1862
|
+
)
|
|
1863
|
+
info.add_argument(
|
|
1864
|
+
"--trophies",
|
|
1865
|
+
dest="include_trophies",
|
|
1866
|
+
action="store_true",
|
|
1867
|
+
help="Show trophy summary and last earned trophies (only works with -i/--info)"
|
|
1868
|
+
)
|
|
1869
|
+
info.add_argument(
|
|
1870
|
+
"--no-recent-games",
|
|
1871
|
+
dest="no_recent_games",
|
|
1872
|
+
action="store_true",
|
|
1873
|
+
help="Don't fetch recently played games list (only works with -i/--info)"
|
|
1874
|
+
)
|
|
1875
|
+
|
|
1277
1876
|
# Intervals & timers
|
|
1278
1877
|
times = parser.add_argument_group("Intervals & timers")
|
|
1279
1878
|
times.add_argument(
|
|
@@ -1403,6 +2002,12 @@ def main():
|
|
|
1403
2002
|
print("* Error: PSN_NPSSO (-n / --npsso_key) value is empty or incorrect")
|
|
1404
2003
|
sys.exit(1)
|
|
1405
2004
|
|
|
2005
|
+
if args.info_mode:
|
|
2006
|
+
include_trophies = args.include_trophies if hasattr(args, 'include_trophies') and args.include_trophies else False
|
|
2007
|
+
show_recent_games = not (hasattr(args, 'no_recent_games') and args.no_recent_games)
|
|
2008
|
+
get_user_info(args.psn_user_id, include_trophies=include_trophies, show_recent_games=show_recent_games)
|
|
2009
|
+
sys.exit(0)
|
|
2010
|
+
|
|
1406
2011
|
if args.check_interval:
|
|
1407
2012
|
PSN_CHECK_INTERVAL = args.check_interval
|
|
1408
2013
|
LIVENESS_CHECK_COUNTER = LIVENESS_CHECK_INTERVAL / PSN_CHECK_INTERVAL
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
psn_monitor.py,sha256=a84GPByu6XlwlCm04lnNUWng9IXkJFQpCCZxPa_IX8Y,55082
|
|
2
|
-
psn_monitor-1.5.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
psn_monitor-1.5.1.dist-info/METADATA,sha256=l5yH9pB3Hwvwx-Ik_09iP6rzxx6ab2Ipd5WBKRaR7cM,12577
|
|
4
|
-
psn_monitor-1.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
psn_monitor-1.5.1.dist-info/entry_points.txt,sha256=2yXV06LBmzhWnvc4bKuEblezaWSHEWKdB2HClGXZlnk,49
|
|
6
|
-
psn_monitor-1.5.1.dist-info/top_level.txt,sha256=IG37NL5yiB0wgx_MN-L47SDxKdqRXzjIqBpkU7JiISE,12
|
|
7
|
-
psn_monitor-1.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|