1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
#!/usr/bin/env python3
import socket, time, subprocess, json, sys, select, argparse, os, signal, random, threading
import requests
def signal_handler(sig, frame):
try:
addr, tags = b"/chatbox/input\0\0", b",sT\0"
socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(addr + tags + b" \0", ("127.0.0.1", 9000))
except: pass
os._exit(0)
signal.signal(signal.SIGINT, signal_handler)
parser = argparse.ArgumentParser(description="vrc-py: NVIDIA VRChat OSC HUD", add_help=False)
parser.add_argument('--message', type=str, default="vrc-py | NVIDIA")
parser.add_argument('--order', nargs='+', default=['line1', 'line2', 'line3', 'line4', 'line5'])
parser.add_argument('--player', type=str, default="spotify")
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()
lyric_data = {"lyrics": [], "loading": False, "song_id": "", "last_idx": -1}
def fetch_lyrics_bg(artist, title, song_id):
global lyric_data
lyric_data["loading"] = True
# Clean tags for strict matching on Omarchy
clean_t = title.split(" - ")[0].split(" (")[0].split(" [")[0].split(" feat.")[0].strip()
clean_a = artist.split(",")[0].split(" & ")[0].strip()
try:
# THE DEAL: Strict lookup to stop the Oh Land / Nostalgia guessing
r = requests.get("https://lrclib.net/api/get",
params={'artist_name': clean_a, 'track_name': clean_t},
headers={'User-Agent': 'vrc-py-hud/1.0'}, timeout=10)
if r.status_code == 200:
res = r.json()
# If plainLyrics is null or it's an instrumental track
if res.get('plainLyrics') is None or res.get('instrumental'):
lyric_data["lyrics"] = [{"ts": 0, "txt": "πΆ Instrumental Song πΆ"}]
else:
raw = res.get('syncedLyrics') or res.get('plainLyrics')
if raw:
parsed = []
for l in raw.split('\n'):
if "[" in l and "]" in l:
try:
ts = int(l[1:3]) * 60 + float(l[4:9])
txt = l[10:].strip()
parsed.append({"ts": ts, "txt": txt})
except: continue
elif l.strip():
parsed.append({"ts": 0, "txt": l.strip()})
lyric_data["lyrics"] = parsed
else:
# Fallback if song isn't found
lyric_data["lyrics"] = [{"ts": 0, "txt": "πΆ Instrumental Song πΆ"}]
except:
lyric_data["lyrics"] = []
lyric_data["loading"] = False
lyric_data["song_id"] = song_id
lyric_data["last_idx"] = -1
def get_audio_data():
try:
p = f"--player={args.player}"
meta = lambda k: subprocess.check_output(["playerctl", p, "metadata", k], text=True).strip()
pos = float(subprocess.check_output(["playerctl", p, "position"], text=True).strip())
stat = subprocess.check_output(["playerctl", p, "status"], text=True).strip()
try: length = float(meta("mpris:length")) / 1000000
except: length = 0
return meta("artist"), meta("title"), pos, length, stat
except: return None, None, 0, 0, "Closed"
def get_stats():
try:
# Querying NVIDIA GPU specifically for your system
gpu = subprocess.check_output(["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"], text=True).strip()
with open('/proc/stat') as f:
l = f.readline().split()
cpu = round((1 - float(l[4])/sum(float(i) for i in l[1:])) * 100)
with open('/proc/meminfo') as f:
m = {ln.split()[0]: int(ln.split()[1]) for ln in f.readlines()[:4]}
ram = round(((m['MemTotal:'] - m['MemAvailable:']) / m['MemTotal:']) * 100)
return f"C:{cpu}% | G:{gpu}% | R:{ram}%"
except: return "Stats Err"
def fmt_time(s):
m, s = divmod(int(s), 60); h, m = divmod(m, 60)
return f"{h:02}:{m:02}:{s:02}" if h > 0 else f"{m:02}:{s:02}"
def send(text):
# OSC Address for VRChat Chatbox
addr, tags = b"/chatbox/input\0\0", b",sT\0"
b_text = text.encode('utf-8') + b"\0"
b_text += b"\0" * (4 - (len(b_text) % 4)) if len(b_text) % 4 != 0 else b""
socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(addr + tags + b_text, ("127.0.0.1", 9000))
# State Management
cur_song, msg_ovr, last, last_pos = "", args.message, "", 0
last_send_time = 0
active_lyric = "πΆπΆπΆ"
lyric_locked_until = 0
while True:
if select.select([sys.stdin], [], [], 0)[0]:
msg_ovr = sys.stdin.readline().strip() or msg_ovr
try:
art, tit, pos, length, stat = get_audio_data()
now_ts = time.time()
if tit and tit != cur_song:
cur_song = tit
lyric_data["lyrics"] = []
lyric_data["last_idx"] = -1
if not lyric_data["loading"]:
threading.Thread(target=fetch_lyrics_bg, args=(art, tit, tit), daemon=True).start()
l3 = get_stats()
if now_ts >= lyric_locked_until:
if lyric_data["loading"]:
active_lyric = "π€ Searching..."
elif lyric_data["lyrics"]:
found_new = False
for i, l in enumerate(lyric_data["lyrics"]):
if l["ts"] <= pos:
if i > lyric_data["last_idx"]:
txt = l["txt"]
if not txt or any(x in txt.lower() for x in ["lyrics by", "produced by", "album:"]):
active_lyric = "πΆπΆπΆ"
else:
active_lyric = f"π€ {txt}"
lyric_data["last_idx"] = i
found_new = True
if found_new:
lyric_locked_until = now_ts + 1.2
is_paused = (pos == last_pos and stat != "Playing")
p_flag = " βΈ" if is_paused else ""
l5 = f"π {fmt_time(pos)} / {fmt_time(length)} [{'β°'*int((pos/length)*10) if length > 0 else ''}{'β±'*(10-int((pos/length)*10)) if length > 0 else ''}]" + p_flag if length > 0 else f"π {fmt_time(pos)} [LIVE]" + p_flag
last_pos = pos
data = {"line1": f"π΅ {tit} - {art}" if tit else "Idle", "line2": active_lyric, "line3": l3, "line4": f"π¬ {msg_ovr}", "line5": l5}
out = "\n".join([data[k] for k in args.order if k in data])
if out != last and (now_ts - last_send_time) > 1.1:
send(out)
last = out
last_send_time = now_ts
except: pass
time.sleep(0.3)
|