Eigene Anwendung neben der Qt-Version: PlaytubeEdge.exe, eigene Installer-ID, eigener Datenordner, Updates nur ueber Releases mit Tag *-edge.
143 lines
5.3 KiB
Python
143 lines
5.3 KiB
Python
"""Laden/Speichern der Benutzerkonfiguration (config.json im Projekt- bzw. App-Datenordner)."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
APP_NAME = "Playtube Edge"
|
|
APP_AUMID = "Playtube.Edge.DesktopClient" # Windows AppUserModelID
|
|
|
|
# Der Entwicklungsmodus (python main.py) benutzt einen eigenen APPDATA-Ordner
|
|
# ("PlaytubeDev" statt "Playtube"), damit Login-Profil und Config sich NIE mit einer
|
|
# gepackten/installierten Playtube.exe ueberschneiden (frueher fuehrte das dazu, dass
|
|
# ein lokaler Test-Build und die echte Installation sich dieselbe config.json bzw.
|
|
# denselben Browser-Profil-Lock geteilt haben).
|
|
#
|
|
# Edge-Variante (WebView2): eigene Ordnernamen ("PlaytubeEdge"/"PlaytubeEdgeDev"), damit sie sich
|
|
# nie mit der Qt-Variante ueberschneidet (Profil, Einzelinstanz-Schutz, Fernsteuerungs-Pipe).
|
|
_DATA_DIR_NAME = "PlaytubeEdge" if getattr(sys, "frozen", False) else "PlaytubeEdgeDev"
|
|
|
|
DEFAULT_CONFIG: dict[str, Any] = {
|
|
"app_name": APP_NAME,
|
|
"discord": {
|
|
"enabled": True,
|
|
# Deine Discord Application Client-ID (discord.com/developers/applications).
|
|
"client_id": "1548023494976086127",
|
|
"update_interval_seconds": 15,
|
|
# Wenn nichts laeuft: Idle-Status anzeigen statt Presence komplett zu leeren.
|
|
"show_idle_presence": True,
|
|
},
|
|
# Die Edge-Variante aktualisiert sich nur ueber eigene Releases (Tag "...-edge", siehe updater.py).
|
|
"updates": {
|
|
"enabled": True,
|
|
"check_interval_hours": 6,
|
|
},
|
|
# Ausgabegeraet pro Tab (Name des Geraets, wie in den Einstellungen gewaehlt; leer =
|
|
# Systemstandard) - siehe playtube/audio_routing.py.
|
|
"audio": {
|
|
"youtube_output": "",
|
|
"music_output": "",
|
|
},
|
|
# Lokale Fernsteuerung fuer das Stream-Dock-Plugin (Named Pipe, nur dieser Benutzer) -
|
|
# siehe playtube/remote_control.py.
|
|
"remote_control": {"enabled": True},
|
|
"start_tab": "youtube", # "youtube" oder "music"
|
|
"home_youtube": "https://www.youtube.com/",
|
|
"home_music": "https://music.youtube.com/",
|
|
"window": {"width": 1366, "height": 860},
|
|
}
|
|
|
|
|
|
def _app_data_dir() -> Path:
|
|
"""Ordner fuer persistente Daten (Login-Profil, Config) - auch im gepackten .exe
|
|
stabil. Entwicklungsmodus und gepackte .exe nutzen bewusst unterschiedliche
|
|
Ordner (siehe _DATA_DIR_NAME)."""
|
|
base = os.environ.get("APPDATA") or str(Path.home())
|
|
d = Path(base) / _DATA_DIR_NAME
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
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:
|
|
# Im Entwicklungsmodus liegt config.json direkt im Projektordner (leicht editierbar),
|
|
# im gepackten Build im APPDATA-Ordner.
|
|
if getattr(sys, "frozen", False):
|
|
return _app_data_dir() / "config.json"
|
|
return Path(__file__).resolve().parent.parent / "config.json"
|
|
|
|
|
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
result = dict(base)
|
|
for key, value in override.items():
|
|
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
result[key] = _deep_merge(result[key], value)
|
|
else:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def load_config() -> dict[str, Any]:
|
|
path = _config_path()
|
|
if path.exists():
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
user_cfg = json.load(f)
|
|
return _deep_merge(DEFAULT_CONFIG, user_cfg)
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
save_config(DEFAULT_CONFIG)
|
|
return dict(DEFAULT_CONFIG)
|
|
|
|
|
|
def save_config(cfg: dict[str, Any]) -> None:
|
|
path = _config_path()
|
|
try:
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def profile_dir() -> Path:
|
|
"""Ordner fuer das persistente Browser-Profil (Login-Session, Cookies, Local Storage)."""
|
|
d = _app_data_dir() / "webprofile"
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return d
|
|
|
|
|
|
def clear_cache_on_update(current_version: str) -> None:
|
|
"""Loescht den QtWebEngine-HTTP-Cache (webprofile/cache), wenn seit dem letzten
|
|
Start ein Update installiert wurde - der Login (Cookies/LocalStorage liegen in
|
|
webprofile/storage, einem komplett getrennten Ordner) bleibt dabei unangetastet.
|
|
Alte Cache-Eintraege (z.B. Icon-Sprites, Skripte) koennen nach einem Update nicht
|
|
mehr zum neuen Code passen - frueher Ursache fuer fehlende Icons nach einem
|
|
beschaedigten Cache, siehe README."""
|
|
marker = _app_data_dir() / "installed_version.txt"
|
|
previous = None
|
|
if marker.exists():
|
|
try:
|
|
previous = marker.read_text(encoding="utf-8").strip()
|
|
except OSError:
|
|
previous = None
|
|
|
|
if previous != current_version:
|
|
cache_dir = profile_dir() / "cache"
|
|
if cache_dir.exists():
|
|
shutil.rmtree(cache_dir, ignore_errors=True)
|
|
try:
|
|
marker.write_text(current_version, encoding="utf-8")
|
|
except OSError:
|
|
pass
|