Discord-Presence: zeigt "In den Einstellungen" und das gerade bearbeitete Feld

Solange der Einstellungen-Tab offen ist, zeigt Discord "In den Einstellungen" mit dem
Feld, das gerade den Fokus hat (z.B. "Bearbeitet: Discord Rich Presence > Client-ID").
Es werden nur Feldnamen uebermittelt, nie Inhalte (z.B. nicht die Client-ID selbst).

- discord_rpc: build_settings_payload + DiscordRPCWorker.submit_settings_activity
- settings_tab: Signal activityChanged, verfolgt den Fokus (auch bei Spinner/Combobox,
  deren Fokus auf einem internen Kind-Widget liegt)
- mainwindow: _update_discord_presence waehlt zwischen Einstellungen, Wiedergabe und
  Leerlauf; Einstellungs-Status folgt der Option "Status anzeigen, wenn gerade nichts
  laeuft" (show_idle_presence); bei Tab-Wechsel und nach dem Speichern aktualisiert
- README: Hinweis zur neuen Anzeige
This commit is contained in:
2026-09-19 20:30:33 +02:00
parent 61b38fe6b8
commit d28b5d05da
4 changed files with 118 additions and 11 deletions
+6 -1
View File
@@ -8,7 +8,12 @@ Erweiterung) mit:
- **Login/Premium** – eigenes, persistentes Profil (`%APPDATA%\Playtube`), einmal bei
Google anmelden reicht für beide Dienste.
- **Discord Rich Presence** – zeigt Titel, Kanal/Interpret, Fortschrittsbalken und
einen Link-Button in deinem Discord-Profil, sobald etwas läuft.
einen Link-Button in deinem Discord-Profil, sobald etwas läuft. Ist der
**Einstellungen-Tab** offen, steht dort stattdessen „In den Einstellungen“ und welches
Feld du gerade bearbeitest (z.B. „Discord Rich Presence › Client-ID“) - nur der
Feldname, nie der Inhalt. Das folgt der Option „Status anzeigen, wenn gerade nichts
läuft“ (aus = auch kein Einstellungs-Status). Discord übernimmt Änderungen höchstens
alle 15 Sekunden, die Anzeige hinkt dem Klicken also etwas hinterher.
- **System-Tray** – Fenster schliessen minimiert nur (Musik läuft weiter), Rechtsklick
aufs Tray-Icon zum Beenden, Play/Pause/Skip direkt aus dem Menü.
- **Eigener Name** – erscheint als "Playtube" im Taskmanager, Fenstertitel, Alt-Tab
+40
View File
@@ -112,6 +112,24 @@ def build_idle_payload(session_start: int) -> dict[str, Any]:
}
SETTINGS_DETAILS = "In den Einstellungen"
SETTINGS_DEFAULT_STATE = "Schaut sich die Einstellungen an"
def build_settings_payload(session_start: int, activity: str | None) -> dict[str, Any]:
"""Presence, solange der Einstellungen-Tab offen ist. `activity` beschreibt, was
gerade bearbeitet wird (z.B. "Bearbeitet: Discord Rich Presence › Client-ID") - es
wird bewusst nur der NAME des Feldes uebergeben, nie sein Inhalt (sonst wuerde
z.B. die eingetragene Client-ID oeffentlich im Discord-Profil stehen)."""
return {
"details": SETTINGS_DETAILS,
"state": _truncate(activity, fallback=SETTINGS_DEFAULT_STATE),
"large_image": ASSET_DEFAULT,
"large_text": APP_NAME,
"start": session_start,
}
class DiscordRPCWorker(QThread):
"""Eigener Thread, der die Verbindung zu Discord haelt und Presence-Updates
debounced (max. 1 Update pro `interval` Sekunden) versendet."""
@@ -143,6 +161,11 @@ class DiscordRPCWorker(QThread):
"""Neuester bekannter Zustand (None = nichts spielt / idle)."""
self._replace_queue(info if info is not None else _IDLE_SENTINEL)
def submit_settings_activity(self, activity: str | None) -> None:
"""Nutzer ist im Einstellungen-Tab; `activity` = was gerade bearbeitet wird
(None = nur "schaut sich die Einstellungen an")."""
self._replace_queue(_SettingsActivity(activity))
def _replace_queue(self, item) -> None:
try:
self._queue.get_nowait()
@@ -254,6 +277,14 @@ class DiscordRPCWorker(QThread):
else:
payload = None
self._presence.clear()
elif isinstance(item, _SettingsActivity):
# Wie beim Leerlauf: der naechste echte Track bekommt einen frischen
# Zeit-Anker.
self._track_key = None
self._track_start_ts = None
payload = build_settings_payload(self._session_start, item.activity)
self._presence.update(**payload)
log_line("[send-settings] state=%r" % (payload["state"],))
else:
start_ts, end_ts = self._track_timestamps(item)
payload = build_presence_payload(item, self._session_start, start_ts, end_ts)
@@ -287,6 +318,15 @@ class DiscordRPCWorker(QThread):
pass
class _SettingsActivity:
"""Queue-Eintrag: Nutzer ist in den Einstellungen und bearbeitet `activity`."""
__slots__ = ("activity",)
def __init__(self, activity: str | None) -> None:
self.activity = activity
class _Sentinel:
__slots__ = ()
+32 -10
View File
@@ -62,6 +62,7 @@ class MainWindow(QMainWindow):
self._tabs.addTab(self._settings_tab, "Einstellungen")
self._tabs.setCurrentIndex(0 if config.get("start_tab") != "music" else 1)
self._settings_tab.settingsSaved.connect(self._on_settings_saved)
self._settings_tab.activityChanged.connect(lambda _activity: self._update_discord_presence())
self._settings_tab.checkUpdatesRequested.connect(self._check_for_updates)
self._settings_tab.installUpdateRequested.connect(
lambda: self._start_update_install(self._pending_download_url)
@@ -180,26 +181,46 @@ class MainWindow(QMainWindow):
tab = self._current_browser_tab()
self._url_bar.setEnabled(tab is not None)
self._url_bar.setText(tab.url().toString() if tab is not None else "")
# Tab-Wechsel von/zu den Einstellungen aendert, was Discord anzeigen soll.
self._update_discord_presence()
def _active_media_info(self) -> dict | None:
"""Bevorzugt der Tab, der gerade wirklich abspielt, sonst der aktuell sichtbare."""
playing_info = next((i for i in self._latest_media.values() if i.get("playing")), None)
return playing_info or self._latest_media.get(id(self._current_tab()))
def _on_media_info(self, info: dict) -> None:
sender = self.sender()
self._latest_media[id(sender)] = info
# Update Fenstertitel + Discord: bevorzugt der Tab, der gerade wirklich
# abspielt, sonst der aktuell sichtbare Tab.
playing_info = next((i for i in self._latest_media.values() if i.get("playing")), None)
active_info = playing_info or self._latest_media.get(id(self._current_tab()))
active_info = self._active_media_info()
if active_info and active_info.get("title"):
self.setWindowTitle(f"{active_info['title']} – {APP_NAME}")
else:
self.setWindowTitle(APP_NAME)
if self._rpc_worker is not None:
if active_info and active_info.get("hasVideo"):
self._rpc_worker.submit_media_info(active_info)
else:
self._rpc_worker.submit_media_info(None)
self._update_discord_presence()
def _update_discord_presence(self) -> None:
"""Entscheidet, was Discord gerade anzeigt: solange der Einstellungen-Tab offen
ist, "In den Einstellungen" samt dem gerade bearbeiteten Feld; sonst wie bisher
die laufende Wiedergabe bzw. den Leerlauf. Die Einstellungs-Anzeige folgt der
Option "Status anzeigen, wenn gerade nichts laeuft" (show_idle_presence) - wer den
Leerlauf-Status abgeschaltet hat, bekommt auch keinen Einstellungs-Status."""
worker = getattr(self, "_rpc_worker", None)
if worker is None:
return
discord_cfg = self._config.get("discord", {})
if self._current_tab() is self._settings_tab and discord_cfg.get("show_idle_presence", True):
worker.submit_settings_activity(self._settings_tab.current_activity())
return
active_info = self._active_media_info()
if active_info and active_info.get("hasVideo"):
worker.submit_media_info(active_info)
else:
worker.submit_media_info(None)
def _start_discord_worker(self) -> None:
discord_cfg = self._config.get("discord", {})
@@ -219,6 +240,7 @@ class MainWindow(QMainWindow):
self._rpc_worker.wait(2000)
self._rpc_worker = None
self._start_discord_worker()
self._update_discord_presence() # frischen Worker sofort mit dem aktuellen Stand versorgen
self._setup_auto_update()
+40
View File
@@ -6,6 +6,7 @@ from typing import Any
from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QFormLayout,
@@ -55,6 +56,9 @@ class SettingsTab(QWidget):
settingsSaved = Signal(dict)
checkUpdatesRequested = Signal()
installUpdateRequested = Signal()
# Neue Beschreibung, was gerade bearbeitet wird (fuer die Discord-Presence, siehe
# MainWindow._update_discord_presence).
activityChanged = Signal(str)
def __init__(self, config: dict[str, Any], parent=None):
super().__init__(parent)
@@ -155,6 +159,42 @@ class SettingsTab(QWidget):
outer.addStretch(1)
# Beschreibung je Eingabefeld fuer die Discord-Presence ("was wird bearbeitet").
# Bewusst nur Feld-NAMEN, nie Inhalte (z.B. nicht die Client-ID selbst).
self._activity_by_widget: dict[QWidget, str] = {
self._discord_enabled: "Bearbeitet: Discord Rich Presence › Aktiviert",
self._client_id: "Bearbeitet: Discord Rich Presence › Client-ID",
self._discord_interval: "Bearbeitet: Discord Rich Presence › Update-Intervall",
self._show_idle: "Bearbeitet: Discord Rich Presence › Status im Leerlauf",
self._updates_enabled: "Bearbeitet: Automatische Updates › Aktiviert",
self._check_interval: "Bearbeitet: Automatische Updates › Prüfintervall",
self._check_updates_btn: "Sucht nach Updates",
self._install_update_btn: "Installiert ein Update",
self._start_tab: "Bearbeitet: Allgemein › Beim Start öffnen",
save_btn: "Speichert die Einstellungen",
}
self._current_activity: str | None = None
app = QApplication.instance()
if app is not None:
app.focusChanged.connect(self._on_focus_changed)
def current_activity(self) -> str | None:
"""Zuletzt bearbeitetes Feld (None, solange noch kein Feld angeklickt wurde)."""
return self._current_activity
def _on_focus_changed(self, _old: QWidget | None, new: QWidget | None) -> None:
# Vom fokussierten Widget nach oben suchen: bei QSpinBox/QComboBox liegt der Fokus
# auf einem internen Kind-Widget (z.B. dem Textfeld des Spinners).
widget = new
while widget is not None and widget is not self:
activity = self._activity_by_widget.get(widget)
if activity:
if activity != self._current_activity:
self._current_activity = activity
self.activityChanged.emit(activity)
return
widget = widget.parentWidget()
def _on_save(self) -> None:
self._config.setdefault("discord", {})
self._config.setdefault("updates", {})