|
|
@@ -54,9 +54,13 @@ def _upsize_thumbnail(url: str | None) -> str | None:
|
|
|
|
return _THUMBNAIL_SIZE_RE.sub("=w544-h544-l90-rj", url)
|
|
|
|
return _THUMBNAIL_SIZE_RE.sub("=w544-h544-l90-rj", url)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_presence_payload(info: dict[str, Any], session_start: int) -> dict[str, Any]:
|
|
|
|
def build_presence_payload(
|
|
|
|
|
|
|
|
info: dict[str, Any], session_start: int, start_ts: int, end_ts: int | None
|
|
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Baut das update()-Payload fuer pypresence aus den vom Browser-Tab gelieferten
|
|
|
|
"""Baut das update()-Payload fuer pypresence aus den vom Browser-Tab gelieferten
|
|
|
|
Medien-Informationen."""
|
|
|
|
Medien-Informationen. start_ts/end_ts werden vom DiscordRPCWorker mitgegeben (siehe
|
|
|
|
|
|
|
|
dort _track_timestamps) statt hier direkt aus info["currentTime"] berechnet zu
|
|
|
|
|
|
|
|
werden."""
|
|
|
|
is_music = bool(info.get("isMusic"))
|
|
|
|
is_music = bool(info.get("isMusic"))
|
|
|
|
playing = bool(info.get("playing"))
|
|
|
|
playing = bool(info.get("playing"))
|
|
|
|
title = _truncate(info.get("title"), fallback="Unbekannter Titel")
|
|
|
|
title = _truncate(info.get("title"), fallback="Unbekannter Titel")
|
|
|
@@ -84,22 +88,10 @@ def build_presence_payload(info: dict[str, Any], session_start: int) -> dict[str
|
|
|
|
"large_text": "YouTube Music" if is_music else "YouTube",
|
|
|
|
"large_text": "YouTube Music" if is_music else "YouTube",
|
|
|
|
"small_image": ASSET_PLAY if playing else ASSET_PAUSE,
|
|
|
|
"small_image": ASSET_PLAY if playing else ASSET_PAUSE,
|
|
|
|
"small_text": "Spielt" if playing else "Pausiert",
|
|
|
|
"small_text": "Spielt" if playing else "Pausiert",
|
|
|
|
|
|
|
|
"start": start_ts,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if end_ts:
|
|
|
|
duration = info.get("duration") or 0
|
|
|
|
payload["end"] = end_ts
|
|
|
|
current_time = info.get("currentTime") or 0
|
|
|
|
|
|
|
|
# IMMER start/end mitschicken, unabhaengig vom playing-Status - nicht nur wenn
|
|
|
|
|
|
|
|
# playing=true. Discord ersetzt "timestamps" bei einem SET_ACTIVITY-Update ohne
|
|
|
|
|
|
|
|
# diese Felder offenbar nicht sauber, sondern behaelt intern die zuletzt bekannten
|
|
|
|
|
|
|
|
# Werte bei ("stackt"). Waehrend eines Songwechsels ist "playing" durch das kurze
|
|
|
|
|
|
|
|
# Neuladen des <video>-Elements oft fuer 1-2 Polls faelschlich false - wurden
|
|
|
|
|
|
|
|
# start/end dann weggelassen, blieb Discords alte (viel zu weit zurueckliegende)
|
|
|
|
|
|
|
|
# Zeit einfach stehen, bis irgendwann wieder echte Werte kamen. Ein pausierter
|
|
|
|
|
|
|
|
# Titel zeigt so einfach einen eingefrorenen Fortschrittsbalken statt gar keinen.
|
|
|
|
|
|
|
|
start_ts = int(time.time() - current_time)
|
|
|
|
|
|
|
|
payload["start"] = start_ts
|
|
|
|
|
|
|
|
if duration and duration > 0:
|
|
|
|
|
|
|
|
payload["end"] = start_ts + int(duration)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url = info.get("url")
|
|
|
|
url = info.get("url")
|
|
|
|
if url and isinstance(url, str) and url.startswith("http"):
|
|
|
|
if url and isinstance(url, str) and url.startswith("http"):
|
|
|
@@ -126,13 +118,25 @@ class DiscordRPCWorker(QThread):
|
|
|
|
def __init__(self, client_id: str, interval: float, show_idle: bool, parent=None):
|
|
|
|
def __init__(self, client_id: str, interval: float, show_idle: bool, parent=None):
|
|
|
|
super().__init__(parent)
|
|
|
|
super().__init__(parent)
|
|
|
|
self._client_id = client_id
|
|
|
|
self._client_id = client_id
|
|
|
|
self._interval = max(5.0, float(interval))
|
|
|
|
# Discord ignoriert/verwirft Rich-Presence-Updates stillschweigend, wenn sie
|
|
|
|
|
|
|
|
# haeufiger als ca. alle 15 Sekunden gesendet werden (offizielle Grenze fuer
|
|
|
|
|
|
|
|
# SET_ACTIVITY). Wird diese Grenze unterschritten, landet zwar technisch jedes
|
|
|
|
|
|
|
|
# Update im Code, aber Discord uebernimmt nur einen Teil davon - nach aussen
|
|
|
|
|
|
|
|
# sieht das wie "eingefrorene" Bilder/Zeiten aus (Bild wechselt nicht, Fortschritt
|
|
|
|
|
|
|
|
# "stackt" beim Songwechsel), weil zufaellig immer wieder ein veraltetes Update
|
|
|
|
|
|
|
|
# durchkommt statt des aktuellen. Deshalb hartes Minimum von 15s, unabhaengig
|
|
|
|
|
|
|
|
# davon, was in der Konfiguration steht.
|
|
|
|
|
|
|
|
self._interval = max(15.0, float(interval))
|
|
|
|
self._show_idle = show_idle
|
|
|
|
self._show_idle = show_idle
|
|
|
|
self._queue: "queue.Queue[dict | None | object]" = queue.Queue(maxsize=1)
|
|
|
|
self._queue: "queue.Queue[dict | None | object]" = queue.Queue(maxsize=1)
|
|
|
|
self._running = True
|
|
|
|
self._running = True
|
|
|
|
self._connected = False
|
|
|
|
self._connected = False
|
|
|
|
self._presence = None
|
|
|
|
self._presence = None
|
|
|
|
self._session_start = int(time.time())
|
|
|
|
self._session_start = int(time.time())
|
|
|
|
|
|
|
|
# Anker fuer die Fortschrittsanzeige: wird NICHT mehr bei jedem Send aus
|
|
|
|
|
|
|
|
# video.currentTime neu berechnet (siehe _track_timestamps).
|
|
|
|
|
|
|
|
self._track_key: tuple | None = None
|
|
|
|
|
|
|
|
self._track_start_ts: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
def submit_media_info(self, info: dict[str, Any] | None) -> None:
|
|
|
|
def submit_media_info(self, info: dict[str, Any] | None) -> None:
|
|
|
|
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
|
|
|
|
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
|
|
|
@@ -202,11 +206,44 @@ class DiscordRPCWorker(QThread):
|
|
|
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
|
|
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
|
|
|
print(f"[discord-rpc] Verbindung fehlgeschlagen: {exc!r}", flush=True)
|
|
|
|
print(f"[discord-rpc] Verbindung fehlgeschlagen: {exc!r}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _track_timestamps(self, info: dict[str, Any]) -> tuple[int, int | None]:
|
|
|
|
|
|
|
|
"""Liefert (start_ts, end_ts) fuer die Fortschrittsanzeige. Der Anker wird nur
|
|
|
|
|
|
|
|
NEU gesetzt, wenn sich Titel/Untertitel aendern (= neuer Track) - nicht bei
|
|
|
|
|
|
|
|
jedem Send aus video.currentTime neu berechnet. Grund: YouTube Music spielt
|
|
|
|
|
|
|
|
beim Songwechsel oft nahtlos (gapless) aus einem durchgehenden Buffer weiter -
|
|
|
|
|
|
|
|
video.currentTime springt dabei nicht zuverlaessig auf 0 zurueck, sondern kann
|
|
|
|
|
|
|
|
einfach vom vorherigen Titel weiterzaehlen. Wuerde man start_ts jedes Mal aus
|
|
|
|
|
|
|
|
currentTime neu ableiten, "stackt" die in Discord angezeigte Zeit ueber mehrere
|
|
|
|
|
|
|
|
Songs hinweg, obwohl Titel/Bild laengst gewechselt haben."""
|
|
|
|
|
|
|
|
title = info.get("title") or ""
|
|
|
|
|
|
|
|
subtitle = info.get("subtitle") or ""
|
|
|
|
|
|
|
|
is_music = bool(info.get("isMusic"))
|
|
|
|
|
|
|
|
key = (is_music, title, subtitle)
|
|
|
|
|
|
|
|
duration = info.get("duration") or 0
|
|
|
|
|
|
|
|
current_time = info.get("currentTime") or 0
|
|
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if key != self._track_key:
|
|
|
|
|
|
|
|
self._track_key = key
|
|
|
|
|
|
|
|
# current_time nur als grobe Anfangs-Schaetzung verwenden (z.B. Programm
|
|
|
|
|
|
|
|
# startet waehrend ein Titel schon laeuft) - plausibilisiert, damit ein
|
|
|
|
|
|
|
|
# verlaesslicher Wert genau EINMAL beim Trackwechsel einfriert und danach
|
|
|
|
|
|
|
|
# rein ueber die Systemzeit weiterlaeuft statt ueber currentTime.
|
|
|
|
|
|
|
|
offset = current_time if (duration <= 0 or 0 <= current_time <= duration) else 0
|
|
|
|
|
|
|
|
self._track_start_ts = int(now - offset)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start_ts = self._track_start_ts if self._track_start_ts is not None else int(now)
|
|
|
|
|
|
|
|
end_ts = start_ts + int(duration) if duration and duration > 0 else None
|
|
|
|
|
|
|
|
return start_ts, end_ts
|
|
|
|
|
|
|
|
|
|
|
|
def _send(self, item) -> None:
|
|
|
|
def _send(self, item) -> None:
|
|
|
|
if self._presence is None:
|
|
|
|
if self._presence is None:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
if item is _IDLE_SENTINEL:
|
|
|
|
if item is _IDLE_SENTINEL:
|
|
|
|
|
|
|
|
# Naechster echter Track soll wieder einen frischen Zeit-Anker bekommen.
|
|
|
|
|
|
|
|
self._track_key = None
|
|
|
|
|
|
|
|
self._track_start_ts = None
|
|
|
|
if self._show_idle:
|
|
|
|
if self._show_idle:
|
|
|
|
payload = build_idle_payload(self._session_start)
|
|
|
|
payload = build_idle_payload(self._session_start)
|
|
|
|
self._presence.update(**payload)
|
|
|
|
self._presence.update(**payload)
|
|
|
@@ -214,7 +251,8 @@ class DiscordRPCWorker(QThread):
|
|
|
|
payload = None
|
|
|
|
payload = None
|
|
|
|
self._presence.clear()
|
|
|
|
self._presence.clear()
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
payload = build_presence_payload(item, self._session_start)
|
|
|
|
start_ts, end_ts = self._track_timestamps(item)
|
|
|
|
|
|
|
|
payload = build_presence_payload(item, self._session_start, start_ts, end_ts)
|
|
|
|
self._presence.update(**payload)
|
|
|
|
self._presence.update(**payload)
|
|
|
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
|
|
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
|
|
|
print(f"[discord-rpc] gesendet: {payload!r}", flush=True)
|
|
|
|
print(f"[discord-rpc] gesendet: {payload!r}", flush=True)
|
|
|
|