-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
266 lines (215 loc) · 7.26 KB
/
Copy pathmain.py
File metadata and controls
266 lines (215 loc) · 7.26 KB
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import asyncio
import hashlib
import io
import json
import os
import sys
import time
import requests
from dotenv import load_dotenv
from minio import Minio
from minio.error import S3Error
from PIL import Image
from pypresence import AioPresence
import pypresence
from config import ENV_PATH, CACHE_PATH
load_dotenv(ENV_PATH)
MINIO_URL = os.getenv('MINIO_URL')
MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
DISCORD_CLIENT_ID = os.getenv('DISCORD_CLIENT_ID')
BUCKET_NAME = os.getenv('MINIO_BUCKET', 'coversimage')
IGNORED_SOURCES = {
'Chrome._crx_fdfllangmnopcnemepphpfaihc', # PWA non-musicale (gestionnaire)
}
ICON_NAMES = {
# Windows
'Spotify': 'spotify',
'Chrome': 'chrome',
'YouTube Music': 'youtube_music',
'music.youtube.com': 'youtube_music',
'Chrome._crx_cinhimbnkkghhklpknlkffjgod': 'youtube_music',
'Microsoft Edge': 'edge',
'Firefox': 'firefox',
'VLC media player': 'vlc',
'foobar2000': 'foobar2000',
'MusicBee': 'musicbee',
'wacup.exe': 'wacup',
# Linux
'Clementine': 'clementine',
'Media Player Classic Qute Theater': 'mpc-qt',
'mpv': 'mpv',
'Music Player Daemon': 'mpd',
'SMPlayer': 'smplayer',
'Lollypop': 'lollypop',
'Mozilla Firefox': 'firefox',
'MellowPlayer': 'mellowplayer',
'Spotube': 'spotube',
'Strawberry': 'strawberry',
'default': 'default_icon',
}
if sys.platform == 'win32':
from backends.smtc import get_track_info
else:
from backends.mpris import get_track_info
RPC = AioPresence(DISCORD_CLIENT_ID, response_timeout=15)
# ---------- Cache ----------
def _load_cache() -> dict:
try:
with open(CACHE_PATH, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
return {}
def _save_cache(data: dict):
with open(CACHE_PATH, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
# ---------- MinIO ----------
def _minio_client():
return Minio(
MINIO_URL,
access_key=MINIO_ACCESS_KEY,
secret_key=MINIO_SECRET_KEY,
secure=True,
)
def upload_cover(image_bytes: bytes) -> str | None:
image_hash = hashlib.sha256(image_bytes).hexdigest()
cache = _load_cache()
if image_hash in cache:
print(f"Cache hit: {image_hash[:12]}...")
return cache[image_hash]
try:
img = Image.open(io.BytesIO(image_bytes)).convert('RGBA')
img.thumbnail((512, 512), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
size = buf.getbuffer().nbytes
object_name = f"{image_hash}.png"
client = _minio_client()
client.put_object(BUCKET_NAME, object_name, buf, size, content_type='image/png')
url = f"https://{MINIO_URL}/{BUCKET_NAME}/{object_name}"
cache[image_hash] = url
_save_cache(cache)
print(f"Uploaded: {object_name}")
return url
except S3Error as e:
print(f"MinIO error: {e}")
return None
# ---------- Discord ----------
def get_icon(source_app: str) -> str:
return ICON_NAMES.get(source_app, ICON_NAMES['default'])
async def update_discord(title, artist, position_s, duration_s, image_url, source_app):
now = time.time()
start_ts = now - position_s
end_ts = start_ts + duration_s if duration_s > 0 else None
icon = get_icon(source_app)
large_image = image_url or icon
try:
kwargs = dict(
details=title,
state=f"par {artist}",
start=int(start_ts),
large_image=large_image,
small_image=icon,
large_text="Écoute en cours",
small_text=source_app or "Lecteur inconnu",
)
if end_ts:
kwargs['end'] = int(end_ts)
await RPC.update(**kwargs)
except pypresence.exceptions.ResponseTimeout:
print("Discord timeout, tentative de reconnexion...")
try:
await RPC.connect()
except Exception:
pass
async def clear_discord():
try:
await RPC.clear()
print("Présence Discord effacée.")
except Exception as e:
print(f"Erreur effacement présence: {e}")
# ---------- Boucle principale ----------
async def main_loop(bridge=None):
def emit_status(s):
if bridge:
try:
bridge.status_changed.emit(s)
except Exception:
pass
def emit_track(artist, title):
if bridge:
try:
bridge.track_changed.emit(artist, title)
except Exception:
pass
try:
await RPC.connect()
except Exception as e:
print(f"Erreur connexion Discord: {e}")
emit_status('error')
print(f"MusicLocal Discord Presence démarré ({sys.platform}).")
last_log = None
last_track = None
last_update_time = 0
last_position_s = 0
none_count = 0
SYNC_INTERVAL = 15
SEEK_TOLERANCE = 3
NONE_GRACE = 3 # nombre de polls None consécutifs avant d'effacer
while True:
info = await get_track_info()
if info is None:
none_count += 1
if last_log is not None and none_count >= NONE_GRACE:
await clear_discord()
last_log = None
last_track = None
last_update_time = 0
emit_status('idle')
emit_track('', '')
elif last_log is None:
print("Aucune session multimédia active.")
else:
none_count = 0
title, artist, image_bytes, source_app, position_s, duration_s = info
if source_app in IGNORED_SOURCES:
await asyncio.sleep(2)
continue
current_track = (title, artist, source_app)
now = time.time()
expected_position = last_position_s + (now - last_update_time)
position_drift = abs(position_s - expected_position)
track_changed = current_track != last_track
needs_sync = (now - last_update_time) >= SYNC_INTERVAL
seeked = last_track is not None and position_drift > SEEK_TOLERANCE
if track_changed or needs_sync or seeked:
image_url = upload_cover(image_bytes) if image_bytes else None
await update_discord(title, artist, position_s, duration_s, image_url, source_app)
last_update_time = now
last_position_s = position_s
last_track = current_track
emit_status('playing')
emit_track(artist, title)
log = f"[{source_app}] {artist} — {title}"
if log != last_log:
print(log)
last_log = log
await asyncio.sleep(2)
# ---------- Entrée ----------
def _get_tray():
if sys.platform == 'win32':
from ui.tray_qt import TrayApp
return TrayApp(main_loop)
desktop = os.getenv('XDG_CURRENT_DESKTOP', '').lower()
if 'gnome' in desktop or 'unity' in desktop:
try:
from ui.tray_gtk import TrayApp
return TrayApp(main_loop)
except Exception:
pass
from ui.tray_qt import TrayApp
return TrayApp(main_loop)
if __name__ == '__main__':
tray = _get_tray()
tray.run()