4 Commits
Author SHA1 Message Date
Fojadrachi e67e8d5d90 Release v2.1.3
Release / build-windows (push) Waiting to run
Release / build-linux (push) Waiting to run
Release / publish-release (push) Blocked by required conditions
2026-09-12 00:08:37 +02:00
FojadrachiandClaude Sonnet 5 2ba4e1bb6f Fix: Discord-Fortschrittszeit stackt weiterhin bei YT-Music-Songwechsel (gapless playback)
video.currentTime wird bei YouTube Music beim Songwechsel wegen nahtloser
(gapless) Wiedergabe aus einem durchgehenden Buffer nicht zuverlaessig auf 0
zurueckgesetzt. Da start/end bisher bei JEDEM RPC-Update frisch aus
currentTime berechnet wurden, blieb die in Discord angezeigte Zeit ueber
Songwechsel hinweg stehen bzw. stackte sich, obwohl Titel/Bild laengst
gewechselt hatten (v2.1.2 hat nur das Rate-Limit-Problem fuer das Bild
behoben, nicht dieses).

Der Zeit-Anker wird jetzt im DiscordRPCWorker anhand von Titel-/Untertitel-
Aenderung gesetzt (_track_timestamps) und bleibt danach stabil ueber die
Systemzeit, statt bei jedem Send erneut aus currentTime abgeleitet zu werden.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-12 00:08:32 +02:00
Fojadrachi 39ebaa84c5 Release v2.1.2
Release / build-windows (push) Waiting to run
Release / build-linux (push) Waiting to run
Release / publish-release (push) Blocked by required conditions
2026-09-11 23:57:48 +02:00
FojadrachiandClaude Sonnet 5 1b587edaaa Fix: Discord-RPC-Updates unterhalb 15s werden von Discord verworfen (eingefrorenes Bild/Zeit)
Discord ignoriert Rich-Presence-Updates stillschweigend, wenn sie haeufiger als
etwa alle 15 Sekunden gesendet werden. Das update_interval_seconds war auf 5s
konfigurierbar/konfiguriert, wodurch ein Teil der Updates (inkl. neuem
Thumbnail und neuem Start-Zeitstempel bei Songwechsel) verworfen wurde -
sichtbar als eingefrorenes altes Bild und 'stackende' Zeit bei Autoplay.
Hartes Minimum von 15s jetzt sowohl im Worker als auch im Einstellungen-Tab.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-11 23:57:43 +02:00
4 changed files with 63 additions and 23 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"discord": {
"enabled": true,
"client_id": "1548023494976086127",
"update_interval_seconds": 5,
"update_interval_seconds": 15,
"show_idle_presence": true
},
"updates": {
+1 -1
View File
@@ -1,4 +1,4 @@
"""Playtube - ein eigenstaendiger YouTube- & YouTube-Music-Player mit Discord Rich Presence."""
__app_name__ = "Playtube"
__version__ = "2.1.1"
__version__ = "2.1.3"
+57 -19
View File
@@ -54,9 +54,13 @@ def _upsize_thumbnail(url: str | None) -> str | None:
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
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"))
playing = bool(info.get("playing"))
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",
"small_image": ASSET_PLAY if playing else ASSET_PAUSE,
"small_text": "Spielt" if playing else "Pausiert",
"start": start_ts,
}
duration = info.get("duration") or 0
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)
if end_ts:
payload["end"] = end_ts
url = info.get("url")
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):
super().__init__(parent)
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._queue: "queue.Queue[dict | None | object]" = queue.Queue(maxsize=1)
self._running = True
self._connected = False
self._presence = None
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:
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
@@ -202,11 +206,44 @@ class DiscordRPCWorker(QThread):
if os.environ.get("PLAYTUBE_DEBUG"):
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:
if self._presence is None:
return
try:
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:
payload = build_idle_payload(self._session_start)
self._presence.update(**payload)
@@ -214,7 +251,8 @@ class DiscordRPCWorker(QThread):
payload = None
self._presence.clear()
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)
if os.environ.get("PLAYTUBE_DEBUG"):
print(f"[discord-rpc] gesendet: {payload!r}", flush=True)
+4 -2
View File
@@ -60,9 +60,11 @@ class SettingsTab(QWidget):
self._client_id = QLineEdit(str(discord_cfg.get("client_id", "")))
self._client_id.setPlaceholderText("Discord Application Client-ID")
self._discord_interval = QSpinBox()
self._discord_interval.setRange(5, 120)
# Minimum 15s: Discord ignoriert Rich-Presence-Updates, die haeufiger kommen,
# stillschweigend (fuehrt zu "eingefrorenem" Bild/Fortschrittsbalken).
self._discord_interval.setRange(15, 120)
self._discord_interval.setSuffix(" s")
self._discord_interval.setValue(int(discord_cfg.get("update_interval_seconds", 15)))
self._discord_interval.setValue(max(15, int(discord_cfg.get("update_interval_seconds", 15))))
self._show_idle = QCheckBox("Status anzeigen, wenn gerade nichts läuft")
self._show_idle.setChecked(bool(discord_cfg.get("show_idle_presence", True)))
discord_form.addRow(self._discord_enabled)