Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c202600cb | ||
|
|
7c37868cd1 |
@@ -55,7 +55,12 @@ nur eine kompilierte .exe mit eigenem Namen und eigener Versionsinfo kann das ae
|
||||
powershell -ExecutionPolicy Bypass -File packaging\build.ps1
|
||||
```
|
||||
|
||||
Ergebnis liegt danach unter `dist\Playtube\Playtube.exe`. Das Build-Skript benennt
|
||||
Ergebnis liegt danach unter `dist\Playtube\Playtube.exe`. Beim ersten Start dieser
|
||||
`.exe` legt Playtube automatisch eine Verknuepfung im Windows-Startmenue an (Playtube
|
||||
wird ja als portables ZIP ohne Installer ausgeliefert - ohne diesen Schritt gaebe es
|
||||
sonst keinen Startmenue-Eintrag).
|
||||
|
||||
Das Build-Skript benennt
|
||||
zusaetzlich den QtWebEngine-Hilfsprozess (der den eigentlichen Ton ausgibt) zu
|
||||
`PlaytubeHelper.exe` um, damit er im Taskmanager nicht als `QtWebEngineProcess`
|
||||
auftaucht. Fuer eine vollstaendige Umbenennung inkl. Icon/Versionsinfo dieses
|
||||
@@ -91,6 +96,12 @@ fragt ein Dialog, ob sie installiert werden soll:
|
||||
Auto-Update laesst sich im Einstellungen-Tab oder in `config.json` unter
|
||||
`updates.enabled` deaktivieren.
|
||||
|
||||
Nach einem erkannten Update wird beim naechsten Start automatisch der QtWebEngine-
|
||||
HTTP-Cache geleert (`webprofile/cache`) - alte Cache-Eintraege koennen sonst nicht mehr
|
||||
zum neuen Code passen (fruehere Ursache fuer fehlende Icons). Der Login bleibt davon
|
||||
unberuehrt, da Cookies/LocalStorage in einem komplett getrennten Ordner
|
||||
(`webprofile/storage`) liegen.
|
||||
|
||||
### Eine neue Version veroeffentlichen
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -10,8 +10,9 @@ from pathlib import Path
|
||||
|
||||
# Branding-Schritte MUESSEN vor dem Import von QtWebEngine passieren.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from playtube import app_id # noqa: E402
|
||||
from playtube.config import APP_NAME, load_config # noqa: E402
|
||||
from playtube import __version__, app_id # noqa: E402
|
||||
from playtube.config import APP_NAME, clear_cache_on_update, load_config # noqa: E402
|
||||
from playtube.shortcuts import ensure_start_menu_shortcut # noqa: E402
|
||||
|
||||
app_id.set_app_user_model_id()
|
||||
app_id.configure_webengine_process_path()
|
||||
@@ -45,6 +46,12 @@ def main() -> int:
|
||||
app.setWindowIcon(QIcon(str(icon_path)))
|
||||
|
||||
config = load_config()
|
||||
# Cache leeren, wenn seit dem letzten Start ein Update installiert wurde (Login
|
||||
# bleibt erhalten, siehe clear_cache_on_update); Startmenue-Verknuepfung fehlt sonst
|
||||
# komplett, da Playtube als portables ZIP ohne Installer ausgeliefert wird.
|
||||
clear_cache_on_update(__version__)
|
||||
ensure_start_menu_shortcut(APP_NAME)
|
||||
|
||||
window = MainWindow(config)
|
||||
window.show()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Playtube - ein eigenstaendiger YouTube- & YouTube-Music-Player mit Discord Rich Presence."""
|
||||
|
||||
__app_name__ = "Playtube"
|
||||
__version__ = "2.1.5"
|
||||
__version__ = "2.2.0"
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -101,3 +102,28 @@ def profile_dir() -> Path:
|
||||
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
|
||||
|
||||
+54
-2
@@ -28,6 +28,54 @@ MEDIA_PROBE_JS = r"""
|
||||
function ytThumbnailUrl(videoId) {
|
||||
return videoId ? ('https://i.ytimg.com/vi/' + videoId + '/hqdefault.jpg') : null;
|
||||
}
|
||||
// "1:23" oder "1:02:03" -> Sekunden.
|
||||
function parseClock(str) {
|
||||
if (!str) { return null; }
|
||||
var parts = String(str).trim().split(':').map(function(p) { return parseInt(p, 10); });
|
||||
if (!parts.length) { return null; }
|
||||
for (var i = 0; i < parts.length; i++) { if (isNaN(parts[i])) { return null; } }
|
||||
var seconds = 0;
|
||||
for (var j = 0; j < parts.length; j++) { seconds = seconds * 60 + parts[j]; }
|
||||
return seconds;
|
||||
}
|
||||
function numAttr(el, name) {
|
||||
if (!el) { return null; }
|
||||
var raw = el.getAttribute(name);
|
||||
var n = raw !== null ? parseFloat(raw) : NaN;
|
||||
return isFinite(n) ? n : null;
|
||||
}
|
||||
// WICHTIG: video.currentTime/video.duration sind bei YouTube Music NICHT
|
||||
// verlaesslich pro Titel - bei nahtlosem (gapless) Songwechsel laeuft darunter
|
||||
// teils ein durchgehender Buffer weiter, dessen currentTime/duration beim
|
||||
// Songwechsel NICHT auf den neuen Titel zurueckgesetzt wird, sondern einfach ueber
|
||||
// mehrere Songs hinweg weiterzaehlt (empirisch bestaetigt: nach einem Songwechsel
|
||||
// wurde die verbleibende Spielzeit des VORHERIGEN Titels als Start-Offset des NEUEN
|
||||
// uebernommen, und die "Dauer" wuchs bei jedem Poll weiter statt konstant zu
|
||||
// bleiben). Deshalb wird die sichtbar angezeigte Fortschrittsanzeige ausgelesen
|
||||
// (ARIA-Attribute des Sliders in Sekunden, sonst der Zeit-Text im Player) - die
|
||||
// zeigt garantiert den Fortschritt des GERADE LAUFENDEN Titels, weil der Nutzer sie
|
||||
// ja selbst so sieht.
|
||||
function musicTimeInfo() {
|
||||
var slider = document.querySelector('#progress-bar, tp-yt-paper-slider#progress-bar');
|
||||
var current = numAttr(slider, 'aria-valuenow');
|
||||
var total = numAttr(slider, 'aria-valuemax');
|
||||
if (current !== null && total !== null && total > 0) {
|
||||
return { current: current, total: total };
|
||||
}
|
||||
var el = document.querySelector('.time-info.ytmusic-player-bar, ytmusic-player-bar .time-info');
|
||||
var parts = el ? (el.textContent || '').split('/') : [];
|
||||
if (parts.length === 2) {
|
||||
var c = parseClock(parts[0]), t = parseClock(parts[1]);
|
||||
if (c !== null && t !== null) { return { current: c, total: t }; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function youtubeTimeInfo() {
|
||||
var current = parseClock(txt('.ytp-time-current'));
|
||||
var total = parseClock(txt('.ytp-time-duration'));
|
||||
if (current !== null && total !== null) { return { current: current, total: total }; }
|
||||
return null;
|
||||
}
|
||||
|
||||
var video = document.querySelector('video');
|
||||
var isMusic = location.hostname.indexOf('music.youtube.com') !== -1;
|
||||
@@ -49,6 +97,10 @@ MEDIA_PROBE_JS = r"""
|
||||
thumbnail = (imgY ? imgY.href : null) || ytThumbnailUrl(videoId);
|
||||
}
|
||||
|
||||
var timeInfo = isMusic ? musicTimeInfo() : youtubeTimeInfo();
|
||||
var currentTime = timeInfo ? timeInfo.current : (video ? video.currentTime : 0);
|
||||
var duration = timeInfo ? timeInfo.total : ((video && isFinite(video.duration)) ? video.duration : 0);
|
||||
|
||||
// WICHTIG: QtWebEngine's runJavaScript()-Bruecke liefert bei einem direkt
|
||||
// zurueckgegebenen JS-Objekt zuverlaessig nur einen leeren String statt des
|
||||
// Objekts (Zahlen/Strings funktionieren, Objekte nicht) - deshalb hier als
|
||||
@@ -61,8 +113,8 @@ MEDIA_PROBE_JS = r"""
|
||||
thumbnail: thumbnail,
|
||||
url: location.href,
|
||||
playing: video ? (!video.paused && !video.ended && video.readyState > 2) : false,
|
||||
currentTime: video ? video.currentTime : 0,
|
||||
duration: (video && isFinite(video.duration)) ? video.duration : 0,
|
||||
currentTime: currentTime,
|
||||
duration: duration,
|
||||
hasVideo: !!video
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Erstellt bei Bedarf eine Windows-Startmenue-Verknuepfung.
|
||||
|
||||
Playtube wird als portables ZIP ausgeliefert (kein MSI/Installer) - ohne das gaebe es
|
||||
also nie einen Eintrag im Windows-Startmenue, wie man ihn von "richtig installierten"
|
||||
Programmen kennt. Wird beim Start der gepackten .exe einmalig nachgeholt (idempotent -
|
||||
prueft vorher, ob die Verknuepfung schon existiert)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _find_icon(exe_dir: Path) -> Path | None:
|
||||
matches = list(exe_dir.rglob("icon.ico"))
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def ensure_start_menu_shortcut(app_name: str) -> None:
|
||||
if sys.platform != "win32" or not getattr(sys, "frozen", False):
|
||||
return
|
||||
try:
|
||||
appdata = os.environ.get("APPDATA")
|
||||
if not appdata:
|
||||
return
|
||||
start_menu = Path(appdata) / "Microsoft" / "Windows" / "Start Menu" / "Programs"
|
||||
shortcut_path = start_menu / f"{app_name}.lnk"
|
||||
if shortcut_path.exists():
|
||||
return
|
||||
|
||||
exe_path = Path(sys.executable).resolve()
|
||||
icon_path = _find_icon(exe_path.parent) or exe_path
|
||||
|
||||
ps_script = (
|
||||
'$WshShell = New-Object -ComObject WScript.Shell\n'
|
||||
f'$Shortcut = $WshShell.CreateShortcut("{shortcut_path}")\n'
|
||||
f'$Shortcut.TargetPath = "{exe_path}"\n'
|
||||
f'$Shortcut.WorkingDirectory = "{exe_path.parent}"\n'
|
||||
f'$Shortcut.IconLocation = "{icon_path}"\n'
|
||||
f'$Shortcut.Description = "{app_name}"\n'
|
||||
'$Shortcut.Save()\n'
|
||||
)
|
||||
subprocess.run(
|
||||
["powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps_script],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
except Exception:
|
||||
# Kein Startmenue-Eintrag ist kein Grund, den App-Start scheitern zu lassen.
|
||||
pass
|
||||
Reference in New Issue
Block a user