Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63b6b36d4 | ||
|
|
06e88ced19 | ||
|
|
fbf5f3b93f | ||
|
|
9bd4b360cd | ||
|
|
e67e8d5d90 | ||
|
|
2ba4e1bb6f | ||
|
|
39ebaa84c5 | ||
|
|
1b587edaaa |
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"discord": {
|
"discord": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"client_id": "1548023494976086127",
|
"client_id": "1548023494976086127",
|
||||||
"update_interval_seconds": 5,
|
"update_interval_seconds": 15,
|
||||||
"show_idle_presence": true
|
"show_idle_presence": true
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Playtube - ein eigenstaendiger YouTube- & YouTube-Music-Player mit Discord Rich Presence."""
|
"""Playtube - ein eigenstaendiger YouTube- & YouTube-Music-Player mit Discord Rich Presence."""
|
||||||
|
|
||||||
__app_name__ = "Playtube"
|
__app_name__ = "Playtube"
|
||||||
__version__ = "2.1.1"
|
__version__ = "2.1.5"
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ def _app_data_dir() -> Path:
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def app_data_dir() -> Path:
|
||||||
|
"""Oeffentlicher Zugriff auf den App-Datenordner, z.B. fuer Debug-Logs - die
|
||||||
|
gepackte .exe laeuft ohne Konsolenfenster (console=False), print()-Debugging ist
|
||||||
|
dort also unsichtbar; ein Log-File ist die einzige Moeglichkeit, dort etwas
|
||||||
|
nachtraeglich einzusehen."""
|
||||||
|
return _app_data_dir()
|
||||||
|
|
||||||
|
|
||||||
def _config_path() -> Path:
|
def _config_path() -> Path:
|
||||||
# Im Entwicklungsmodus liegt config.json direkt im Projektordner (leicht editierbar),
|
# Im Entwicklungsmodus liegt config.json direkt im Projektordner (leicht editierbar),
|
||||||
# im gepackten Build im APPDATA-Ordner.
|
# im gepackten Build im APPDATA-Ordner.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Sehr einfaches, immer aktives Datei-Log fuer schwer reproduzierbare Bugs.
|
||||||
|
|
||||||
|
Die gepackte .exe laeuft ohne Konsolenfenster (console=False in packaging/playtube.spec),
|
||||||
|
also sind print()-Debugausgaben dort unsichtbar - selbst mit PLAYTUBE_DEBUG=1 sieht der
|
||||||
|
Nutzer nichts. Dieses Modul schreibt stattdessen in eine kleine, automatisch gekappte
|
||||||
|
Log-Datei im App-Datenordner, die sich jederzeit nachtraeglich auslesen laesst.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import app_data_dir
|
||||||
|
|
||||||
|
_MAX_LINES = 500
|
||||||
|
|
||||||
|
|
||||||
|
def _log_path() -> Path:
|
||||||
|
d = app_data_dir() / "logs"
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
return d / "discord_rpc.log"
|
||||||
|
|
||||||
|
|
||||||
|
def log_line(msg: str) -> None:
|
||||||
|
"""Haengt eine Zeile mit Zeitstempel an - haelt die Datei klein, indem bei
|
||||||
|
Ueberlaenge nur die letzten _MAX_LINES Zeilen behalten werden."""
|
||||||
|
try:
|
||||||
|
path = _log_path()
|
||||||
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
line = f"[{timestamp}] {msg}\n"
|
||||||
|
with open(path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(line)
|
||||||
|
|
||||||
|
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||||
|
if len(lines) > _MAX_LINES:
|
||||||
|
path.write_text("\n".join(lines[-_MAX_LINES:]) + "\n", encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
# Logging darf niemals die App zum Absturz bringen.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def log_exception(context: str, exc: BaseException) -> None:
|
||||||
|
log_line(f"{context}: {exc!r}\n{''.join(traceback.format_exception(exc))}")
|
||||||
+72
-19
@@ -16,6 +16,7 @@ from typing import Any
|
|||||||
from PySide6.QtCore import QThread
|
from PySide6.QtCore import QThread
|
||||||
|
|
||||||
from .config import APP_NAME
|
from .config import APP_NAME
|
||||||
|
from .debug_log import log_line
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pypresence.types import ActivityType
|
from pypresence.types import ActivityType
|
||||||
@@ -54,9 +55,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 +89,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 +119,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)."""
|
||||||
@@ -195,6 +200,7 @@ class DiscordRPCWorker(QThread):
|
|||||||
self._presence = Presence(self._client_id)
|
self._presence = Presence(self._client_id)
|
||||||
self._presence.connect()
|
self._presence.connect()
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
log_line("[connect] verbunden")
|
||||||
if os.environ.get("PLAYTUBE_DEBUG"):
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
||||||
print("[discord-rpc] verbunden", flush=True)
|
print("[discord-rpc] verbunden", flush=True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -202,11 +208,46 @@ 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:
|
||||||
|
old_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)
|
||||||
|
log_line(f"[track-change] alt={old_key!r} neu={key!r} offset={offset:.1f}s")
|
||||||
|
|
||||||
|
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,13 +255,25 @@ 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)
|
||||||
|
log_line(
|
||||||
|
"[send] title=%r thumbnail=%r large_image=%r start=%s end=%s"
|
||||||
|
% (
|
||||||
|
item.get("title"),
|
||||||
|
item.get("thumbnail"),
|
||||||
|
payload.get("large_image"),
|
||||||
|
start_ts,
|
||||||
|
end_ts,
|
||||||
|
)
|
||||||
|
)
|
||||||
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)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Discord evtl. geschlossen worden -> beim naechsten Mal neu verbinden.
|
# Discord evtl. geschlossen worden -> beim naechsten Mal neu verbinden.
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
log_line(f"[send-error] {exc!r}")
|
||||||
if os.environ.get("PLAYTUBE_DEBUG"):
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
||||||
print(f"[discord-rpc] Senden fehlgeschlagen: {exc!r}", flush=True)
|
print(f"[discord-rpc] Senden fehlgeschlagen: {exc!r}", flush=True)
|
||||||
|
|
||||||
|
|||||||
@@ -333,7 +333,14 @@ class MainWindow(QMainWindow):
|
|||||||
self._settings_tab.set_download_progress(100)
|
self._settings_tab.set_download_progress(100)
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
# Ein Hintergrund-Skript wartet bereits darauf, dass dieser Prozess
|
# Ein Hintergrund-Skript wartet bereits darauf, dass dieser Prozess
|
||||||
# beendet wird, tauscht dann die Dateien aus und startet die App neu.
|
# beendet wird, tauscht dann die Dateien aus und startet die App neu -
|
||||||
|
# es zeigt dabei selbst ein kleines Fortschrittsfenster an (siehe
|
||||||
|
# updater.py), damit der Nutzer zwischen "App schliesst sich" und
|
||||||
|
# "neue App startet" nicht denkt, etwas sei abgestuerzt.
|
||||||
|
self._settings_tab.set_update_status(
|
||||||
|
"Installation abgeschlossen. Playtube wird neu gestartet …"
|
||||||
|
)
|
||||||
|
self._tray.setToolTip(f"{APP_NAME} – wird neu gestartet …")
|
||||||
self._quit()
|
self._quit()
|
||||||
else:
|
else:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ class SettingsTab(QWidget):
|
|||||||
self._client_id = QLineEdit(str(discord_cfg.get("client_id", "")))
|
self._client_id = QLineEdit(str(discord_cfg.get("client_id", "")))
|
||||||
self._client_id.setPlaceholderText("Discord Application Client-ID")
|
self._client_id.setPlaceholderText("Discord Application Client-ID")
|
||||||
self._discord_interval = QSpinBox()
|
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.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 = QCheckBox("Status anzeigen, wenn gerade nichts läuft")
|
||||||
self._show_idle.setChecked(bool(discord_cfg.get("show_idle_presence", True)))
|
self._show_idle.setChecked(bool(discord_cfg.get("show_idle_presence", True)))
|
||||||
discord_form.addRow(self._discord_enabled)
|
discord_form.addRow(self._discord_enabled)
|
||||||
|
|||||||
+83
-11
@@ -222,32 +222,88 @@ class UpdateInstaller(QThread):
|
|||||||
|
|
||||||
self.finished_ok.emit()
|
self.finished_ok.emit()
|
||||||
|
|
||||||
|
# Kleines, immer sichtbares Fortschrittsfenster fuer den Windows-Installationsschritt.
|
||||||
|
# Der Hauptprozess ist zu diesem Zeitpunkt schon beendet (Datei-Sperren!), daher
|
||||||
|
# laeuft das komplett im separaten PowerShell-Skript - ohne dieses Fenster wuerde
|
||||||
|
# der Nutzer nach dem Schliessen der App fuer die Dauer der Installation (bei
|
||||||
|
# einem vollen Paket ggf. mehrere Sekunden) gar nichts sehen, was wie ein Absturz
|
||||||
|
# oder Haenger wirkt.
|
||||||
|
_WINDOWS_PROGRESS_FORM_PS = """
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
$form = New-Object System.Windows.Forms.Form
|
||||||
|
$form.Text = "Playtube-Update"
|
||||||
|
$form.Size = New-Object System.Drawing.Size(380,130)
|
||||||
|
$form.StartPosition = "CenterScreen"
|
||||||
|
$form.FormBorderStyle = "FixedDialog"
|
||||||
|
$form.ControlBox = $false
|
||||||
|
$form.TopMost = $true
|
||||||
|
$label = New-Object System.Windows.Forms.Label
|
||||||
|
$label.Text = "Playtube wird aktualisiert – bitte warten …"
|
||||||
|
$label.AutoSize = $false
|
||||||
|
$label.Size = New-Object System.Drawing.Size(340,20)
|
||||||
|
$label.Location = New-Object System.Drawing.Point(20,15)
|
||||||
|
$form.Controls.Add($label)
|
||||||
|
$bar = New-Object System.Windows.Forms.ProgressBar
|
||||||
|
$bar.Style = "Marquee"
|
||||||
|
$bar.MarqueeAnimationSpeed = 30
|
||||||
|
$bar.Size = New-Object System.Drawing.Size(340,20)
|
||||||
|
$bar.Location = New-Object System.Drawing.Point(20,50)
|
||||||
|
$form.Controls.Add($bar)
|
||||||
|
$form.Show()
|
||||||
|
$form.Refresh()
|
||||||
|
"""
|
||||||
|
|
||||||
# -- Patch (nur .exe/Binary tauschen, _internal/ bleibt unangetastet) --
|
# -- Patch (nur .exe/Binary tauschen, _internal/ bleibt unangetastet) --
|
||||||
|
|
||||||
def _install_patch_windows(self, new_exe: Path, install_dir: Path, staging: Path) -> None:
|
def _install_patch_windows(self, new_exe: Path, install_dir: Path, staging: Path) -> None:
|
||||||
exe_path = install_dir / _WINDOWS_BINARY_NAME
|
exe_path = install_dir / _WINDOWS_BINARY_NAME
|
||||||
script = f"""
|
script = self._WINDOWS_PROGRESS_FORM_PS + f"""
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
$targetPid = {os.getpid()}
|
$targetPid = {os.getpid()}
|
||||||
while (Get-Process -Id $targetPid -ErrorAction SilentlyContinue) {{
|
while (Get-Process -Id $targetPid -ErrorAction SilentlyContinue) {{
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 300
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
}}
|
}}
|
||||||
|
$label.Text = "Kopiere aktualisierte Datei …"
|
||||||
|
$form.Refresh()
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
Copy-Item -Path "{new_exe}" -Destination "{exe_path}" -Force
|
Copy-Item -Path "{new_exe}" -Destination "{exe_path}" -Force
|
||||||
|
$label.Text = "Fertig – Playtube wird neu gestartet …"
|
||||||
|
$form.Refresh()
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
Start-Process -FilePath "{exe_path}"
|
Start-Process -FilePath "{exe_path}"
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Milliseconds 800
|
||||||
|
$form.Close()
|
||||||
Remove-Item -Recurse -Force "{staging}" -ErrorAction SilentlyContinue
|
Remove-Item -Recurse -Force "{staging}" -ErrorAction SilentlyContinue
|
||||||
"""
|
"""
|
||||||
self._spawn_windows_script(script, staging)
|
self._spawn_windows_script(script, staging)
|
||||||
|
|
||||||
|
# Falls zenity installiert ist (auf den meisten Desktop-Distros vorhanden), waehrend
|
||||||
|
# Wartezeit/Installation ein pulsierendes Fortschrittsfenster zeigen - rein optisch,
|
||||||
|
# das Update funktioniert auch ohne (dann passiert der Neustart einfach unsichtbar).
|
||||||
|
_POSIX_PROGRESS_HEADER = """#!/bin/sh
|
||||||
|
ZPID=""
|
||||||
|
if command -v zenity >/dev/null 2>&1; then
|
||||||
|
tail -f /dev/null | zenity --progress --title="Playtube-Update" \
|
||||||
|
--text="Playtube wird aktualisiert - bitte warten ..." --pulsate --no-cancel \
|
||||||
|
>/dev/null 2>&1 &
|
||||||
|
ZPID=$!
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
_POSIX_PROGRESS_FOOTER = """
|
||||||
|
[ -n "$ZPID" ] && kill "$ZPID" 2>/dev/null
|
||||||
|
"""
|
||||||
|
|
||||||
def _install_patch_posix(self, new_binary: Path, install_dir: Path, staging: Path) -> None:
|
def _install_patch_posix(self, new_binary: Path, install_dir: Path, staging: Path) -> None:
|
||||||
exe_path = install_dir / _LINUX_BINARY_NAME
|
exe_path = install_dir / _LINUX_BINARY_NAME
|
||||||
script = f"""#!/bin/sh
|
script = self._POSIX_PROGRESS_HEADER + f"""
|
||||||
while kill -0 {os.getpid()} 2>/dev/null; do
|
while kill -0 {os.getpid()} 2>/dev/null; do
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
done
|
done
|
||||||
cp -f "{new_binary}" "{exe_path}"
|
cp -f "{new_binary}" "{exe_path}"
|
||||||
chmod +x "{exe_path}"
|
chmod +x "{exe_path}"
|
||||||
|
""" + self._POSIX_PROGRESS_FOOTER + f"""
|
||||||
nohup "{exe_path}" >/dev/null 2>&1 &
|
nohup "{exe_path}" >/dev/null 2>&1 &
|
||||||
rm -rf "{staging}"
|
rm -rf "{staging}"
|
||||||
"""
|
"""
|
||||||
@@ -257,29 +313,45 @@ rm -rf "{staging}"
|
|||||||
|
|
||||||
def _install_full_windows(self, source_dir: Path, install_dir: Path, staging: Path) -> None:
|
def _install_full_windows(self, source_dir: Path, install_dir: Path, staging: Path) -> None:
|
||||||
exe_path = install_dir / _WINDOWS_BINARY_NAME
|
exe_path = install_dir / _WINDOWS_BINARY_NAME
|
||||||
script = f"""
|
script = self._WINDOWS_PROGRESS_FORM_PS + f"""
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
$targetPid = {os.getpid()}
|
$targetPid = {os.getpid()}
|
||||||
while (Get-Process -Id $targetPid -ErrorAction SilentlyContinue) {{
|
while (Get-Process -Id $targetPid -ErrorAction SilentlyContinue) {{
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 300
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
}}
|
}}
|
||||||
robocopy "{source_dir}" "{install_dir}" /MIR /NFL /NDL /NJH /NJS /NC /NS /NP | Out-Null
|
$label.Text = "Kopiere Programmdateien … (kann etwas dauern)"
|
||||||
|
$form.Refresh()
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
|
# Ueber Start-Process (statt direktem Aufruf) gestartet und per Polling statt -Wait
|
||||||
|
# abgewartet, damit die Fensternachrichtenschleife per DoEvents() weiterlaeuft -
|
||||||
|
# sonst wuerde Windows das Fenster waehrend robocopy als "Keine Rueckmeldung" anzeigen.
|
||||||
|
$roboArgs = @("{source_dir}", "{install_dir}", "/MIR", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP")
|
||||||
|
$roboProc = Start-Process -FilePath "robocopy" -ArgumentList $roboArgs -WindowStyle Hidden -PassThru
|
||||||
|
while (-not $roboProc.HasExited) {{
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
|
}}
|
||||||
|
$label.Text = "Fertig – Playtube wird neu gestartet …"
|
||||||
|
$form.Refresh()
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
Start-Process -FilePath "{exe_path}"
|
Start-Process -FilePath "{exe_path}"
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Milliseconds 800
|
||||||
|
$form.Close()
|
||||||
Remove-Item -Recurse -Force "{staging}" -ErrorAction SilentlyContinue
|
Remove-Item -Recurse -Force "{staging}" -ErrorAction SilentlyContinue
|
||||||
"""
|
"""
|
||||||
self._spawn_windows_script(script, staging)
|
self._spawn_windows_script(script, staging)
|
||||||
|
|
||||||
def _install_full_posix(self, source_dir: Path, install_dir: Path, staging: Path) -> None:
|
def _install_full_posix(self, source_dir: Path, install_dir: Path, staging: Path) -> None:
|
||||||
exe_path = install_dir / _LINUX_BINARY_NAME
|
exe_path = install_dir / _LINUX_BINARY_NAME
|
||||||
script = f"""#!/bin/sh
|
script = self._POSIX_PROGRESS_HEADER + f"""
|
||||||
while kill -0 {os.getpid()} 2>/dev/null; do
|
while kill -0 {os.getpid()} 2>/dev/null; do
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
done
|
done
|
||||||
rm -rf "{install_dir}"/*
|
rm -rf "{install_dir}"/*
|
||||||
cp -a "{source_dir}"/. "{install_dir}"/
|
cp -a "{source_dir}"/. "{install_dir}"/
|
||||||
chmod +x "{exe_path}"
|
chmod +x "{exe_path}"
|
||||||
|
""" + self._POSIX_PROGRESS_FOOTER + f"""
|
||||||
nohup "{exe_path}" >/dev/null 2>&1 &
|
nohup "{exe_path}" >/dev/null 2>&1 &
|
||||||
rm -rf "{staging}"
|
rm -rf "{staging}"
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user