Compare commits
6
Commits
6a682fc49c
...
edge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f4514d08 | ||
|
|
9e8e9caafc | ||
|
|
d14272991c | ||
|
|
bb15ff3316 | ||
|
|
f1bde49cf7 | ||
|
|
c3e69bd5da |
@@ -0,0 +1,78 @@
|
|||||||
|
name: Release (Edge)
|
||||||
|
|
||||||
|
# Baut Playtube Edge (Microsoft-Edge-Engine / WebView2) fuer Windows und veroeffentlicht Setup
|
||||||
|
# und ZIP als Release auf dem eigenen Gitea-Server, sobald ein Tag im Format "vX.Y.Z-edge"
|
||||||
|
# gepusht wird (z.B. durch packaging/release.ps1).
|
||||||
|
#
|
||||||
|
# WICHTIG:
|
||||||
|
# - Nur Tags mit dem Suffix "-edge" loesen diesen Workflow aus - die normale (Qt-)Playtube
|
||||||
|
# baut aus dem Branch "main" mit Tags "vX.Y.Z" und bleibt davon unberuehrt.
|
||||||
|
# - Das Release wird als VORABVERSION veroeffentlicht. Der Updater der normalen Playtube
|
||||||
|
# ignoriert Vorabversionen und Tags mit "-edge"; der Updater der Edge-Version sucht seinerseits
|
||||||
|
# nur nach Tags "...-edge" (siehe playtube/updater.py).
|
||||||
|
#
|
||||||
|
# Voraussetzungen (einmalig, siehe README "Releases bauen"):
|
||||||
|
# - Gitea-Actions-Runner unter Windows mit Label "windows" (Host-Modus) mit Python 3.12, git,
|
||||||
|
# PowerShell und Inno Setup 6.
|
||||||
|
# - Repo-Secret "RELEASE_TOKEN" (Zugriffstoken mit Schreibrecht); ersatzweise der automatische Token.
|
||||||
|
# Bewusst KEINE "uses:"-Aktionen: die wuerden standardmaessig von github.com geladen.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*-edge"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- name: Quellcode holen
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
git clone "${{ gitea.server_url }}/${{ gitea.repository }}.git" src
|
||||||
|
git -C src checkout "${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Abhaengigkeiten installieren
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: src
|
||||||
|
run: |
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\python.exe -m pip install --upgrade pip
|
||||||
|
.\.venv\Scripts\python.exe -m pip install -r requirements.txt pyinstaller
|
||||||
|
|
||||||
|
- name: PlaytubeEdge.exe bauen
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: src
|
||||||
|
run: .\.venv\Scripts\python.exe -m PyInstaller packaging\playtube.spec --noconfirm --distpath dist --workpath build
|
||||||
|
|
||||||
|
- name: Setup-Installer bauen (Inno Setup)
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: src
|
||||||
|
run: |
|
||||||
|
$version = (Select-String -Path playtube\__init__.py -Pattern '__version__\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
|
||||||
|
$candidates = @(
|
||||||
|
(Get-Command iscc -ErrorAction SilentlyContinue | ForEach-Object { $_.Source }),
|
||||||
|
(Join-Path $env:LOCALAPPDATA "Programs\Inno Setup 6\ISCC.exe"),
|
||||||
|
(Join-Path ${env:ProgramFiles(x86)} "Inno Setup 6\ISCC.exe"),
|
||||||
|
(Join-Path $env:ProgramFiles "Inno Setup 6\ISCC.exe")
|
||||||
|
)
|
||||||
|
$iscc = $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
|
||||||
|
if (-not $iscc) { throw "Inno Setup 6 ist auf dem Runner nicht installiert." }
|
||||||
|
& $iscc "/DAppVersion=$version" packaging\playtube.iss
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Inno-Setup-Build fehlgeschlagen." }
|
||||||
|
|
||||||
|
- name: ZIP packen (portabel)
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: src
|
||||||
|
run: Compress-Archive -Path dist\PlaytubeEdge -DestinationPath "PlaytubeEdge-${{ gitea.ref_name }}-win64.zip"
|
||||||
|
|
||||||
|
- name: Release auf Gitea veroeffentlichen (Vorabversion)
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: src
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
$tag = "${{ gitea.ref_name }}"
|
||||||
|
$files = @((Get-ChildItem dist\installer\PlaytubeEdge-Setup-v*.exe).FullName) + @((Get-ChildItem "PlaytubeEdge-*-win64.zip").FullName)
|
||||||
|
.\packaging\publish_release.ps1 -Tag $tag -Title "Playtube Edge $tag" -NotesFile packaging\release_notes.md `
|
||||||
|
-Files $files -Prerelease -Server "${{ gitea.server_url }}" -Repo "${{ gitea.repository }}"
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
# Baut Playtube fuer Windows und Linux und veroeffentlicht beide als Assets an einem
|
|
||||||
# GitHub Release, sobald ein Tag im Format "vX.Y.Z" gepusht wird (z.B. durch
|
|
||||||
# packaging/release.ps1). Playtube.exe/Playtube auf den Nutzer-Rechnern erkennt neue
|
|
||||||
# Releases danach automatisch (siehe playtube/updater.py) und bietet die Installation an.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
workflow_dispatch: {}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Abhaengigkeiten installieren
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install pyinstaller
|
|
||||||
|
|
||||||
- name: Playtube.exe bauen
|
|
||||||
run: pyinstaller packaging\playtube.spec --noconfirm --distpath dist --workpath build
|
|
||||||
|
|
||||||
- name: Setup-Installer bauen (Inno Setup)
|
|
||||||
# Der Installer ist der empfohlene Download fuer Neueinsteiger UND das, was
|
|
||||||
# Playtube fuer volle Updates benutzt (siehe playtube/updater.py): eine einzige
|
|
||||||
# Installation, die jedes Update an Ort und Stelle ueberschreibt (feste AppId in
|
|
||||||
# packaging/playtube.iss) - keine parallelen Versionsordner mehr.
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$version = (Select-String -Path playtube\__init__.py -Pattern '__version__\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
|
|
||||||
$iscc = Join-Path ${env:ProgramFiles(x86)} "Inno Setup 6\ISCC.exe"
|
|
||||||
if (-not (Test-Path $iscc)) {
|
|
||||||
choco install innosetup -y --no-progress
|
|
||||||
}
|
|
||||||
& $iscc "/DAppVersion=$version" packaging\playtube.iss
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Inno-Setup-Build fehlgeschlagen." }
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-setup
|
|
||||||
path: dist/installer/Playtube-Setup-v*.exe
|
|
||||||
|
|
||||||
- name: Als .zip packen (volles Paket, portabel)
|
|
||||||
run: Compress-Archive -Path dist\Playtube -DestinationPath Playtube-${{ github.ref_name }}-win64.zip
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-build
|
|
||||||
path: Playtube-${{ github.ref_name }}-win64.zip
|
|
||||||
|
|
||||||
- name: Patch-Paket packen (nur Playtube.exe, fuer schnelle Updates)
|
|
||||||
# .play statt .zip: eigene Dateiendung, die Playtube nach dem ersten Start als
|
|
||||||
# Windows-Dateizuordnung registriert (siehe shortcuts.py) - ein manuell
|
|
||||||
# heruntergeladenes Patch laesst sich so auch per Doppelklick installieren.
|
|
||||||
# Technisch bleibt es ein ganz normales ZIP-Archiv.
|
|
||||||
# Die PySide6-Version steht im Dateinamen: hat sich die Laufzeit gegenueber der
|
|
||||||
# installierten App geaendert, ist ein reines .exe-Patch nicht mehr kompatibel und
|
|
||||||
# der Updater nimmt stattdessen den Setup-Installer (siehe _patch_is_compatible).
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$pyside = python -c "import PySide6; print(PySide6.__version__)"
|
|
||||||
$name = "Playtube-${{ github.ref_name }}-win64-pyside${pyside}-patch"
|
|
||||||
Compress-Archive -Path dist\Playtube\Playtube.exe -DestinationPath "$name.zip"
|
|
||||||
Rename-Item -Path "$name.zip" -NewName "$name.play"
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-patch
|
|
||||||
path: Playtube-*-win64-pyside*-patch.play
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Qt-WebEngine-Laufzeitabhaengigkeiten installieren
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libxkbcommon0 libegl1 libopengl0 libnss3 libnspr4 \
|
|
||||||
libxcomposite1 libxdamage1 libxrandr2 libxtst6 libxcursor1 \
|
|
||||||
libgbm1 libasound2t64 libatk-bridge2.0-0 libatk1.0-0 libcups2 \
|
|
||||||
libdbus-1-3 libdrm2 libpango-1.0-0 libpangocairo-1.0-0 \
|
|
||||||
libxfixes3 libxi6 libxext6 fonts-liberation
|
|
||||||
|
|
||||||
- name: Abhaengigkeiten installieren
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install pyinstaller
|
|
||||||
|
|
||||||
- name: Playtube-Binary bauen
|
|
||||||
run: pyinstaller packaging/playtube.spec --noconfirm --distpath dist --workpath build
|
|
||||||
|
|
||||||
- name: Als .tar.gz packen (volles Paket)
|
|
||||||
run: tar -C dist -czf Playtube-${{ github.ref_name }}-linux-x86_64.tar.gz Playtube
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux-build
|
|
||||||
path: Playtube-${{ github.ref_name }}-linux-x86_64.tar.gz
|
|
||||||
|
|
||||||
- name: Patch-Paket packen (nur Playtube-Binary, fuer schnelle Updates)
|
|
||||||
run: tar -C dist/Playtube -czf Playtube-${{ github.ref_name }}-linux-x86_64-patch.tar.gz Playtube
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux-patch
|
|
||||||
path: Playtube-${{ github.ref_name }}-linux-x86_64-patch.tar.gz
|
|
||||||
|
|
||||||
publish-release:
|
|
||||||
needs: [build-windows, build-linux]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: GitHub Release veroeffentlichen
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: artifacts/*
|
|
||||||
generate_release_notes: true
|
|
||||||
@@ -4,4 +4,8 @@ __pycache__/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
run*.log
|
run*.log
|
||||||
|
*_run.log
|
||||||
|
build_edge.log
|
||||||
|
.claude/
|
||||||
|
.pytest_cache/
|
||||||
test_login*.log
|
test_login*.log
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
# Playtube
|
# Playtube Edge
|
||||||
|
|
||||||
|
> **Das ist die „Edge“-Variante von Playtube (Branch `edge`).**
|
||||||
|
> **Diese Version brauchst du, wenn du selbst hochgeladene Musik in YouTube Music hören
|
||||||
|
> willst.** Die normale Playtube (Branch `main`) zeigt bei eigenen Uploads „Dieses
|
||||||
|
> Videoformat wird nicht unterstützt“, weil ihre Browser-Engine (QtWebEngine) kein AAC und
|
||||||
|
> kein H.264 abspielen kann. Playtube Edge rendert stattdessen mit der **Microsoft-Edge-Engine
|
||||||
|
> (WebView2)** – damit laufen auch Uploads.
|
||||||
|
>
|
||||||
|
> - Download: [Releases](https://git.fojadrachi.de/Fojadrachi/Playtube/releases) → Eintrag „Playtube
|
||||||
|
> Edge vX.Y.Z-edge“ → `PlaytubeEdge-Setup-vX.Y.Z.exe`. Voraussetzung: Windows 10/11 mit
|
||||||
|
> WebView2-Runtime (Windows 11: vorinstalliert).
|
||||||
|
> - Läuft **neben** der normalen Playtube (eigener Ordner `%APPDATA%\PlaytubeEdge`, eigenes
|
||||||
|
> Login – beim ersten Start einmal bei Google anmelden).
|
||||||
|
> - Updates kommen nur über die `-edge`-Releases (die normale Playtube sieht sie nie).
|
||||||
|
> - Entwicklung: `pip install -r requirements.txt`, dann `python main.py`. Die
|
||||||
|
> WebView2-DLLs liegen in `vendor/webview2` (Microsoft, NuGet `Microsoft.Web.WebView2`).
|
||||||
|
>
|
||||||
|
> Der Rest dieser README beschreibt die gemeinsame Basis mit der normalen Playtube; wo dort
|
||||||
|
> „QtWebEngine“ steht, ist in dieser Variante WebView2 gemeint.
|
||||||
|
|
||||||
Eigenständige Desktop-App für YouTube & YouTube Music (kein Browser-Fenster, keine
|
Eigenständige Desktop-App für YouTube & YouTube Music (kein Browser-Fenster, keine
|
||||||
Erweiterung) mit:
|
Erweiterung) mit:
|
||||||
@@ -23,8 +42,8 @@ Erweiterung) mit:
|
|||||||
|
|
||||||
## Installation (Windows)
|
## Installation (Windows)
|
||||||
|
|
||||||
Lade auf der [Releases-Seite](https://github.com/fojadrachi/Playtube/releases)
|
Lade auf der [Releases-Seite](https://git.fojadrachi.de/Fojadrachi/Playtube/releases)
|
||||||
`Playtube-Setup-vX.Y.Z.exe` herunter und starte sie. Der Installer
|
`PlaytubeEdge-Setup-vX.Y.Z.exe` herunter und starte sie. Der Installer
|
||||||
|
|
||||||
- installiert Playtube **pro Benutzer** nach `%LOCALAPPDATA%\Programs\Playtube` (keine
|
- installiert Playtube **pro Benutzer** nach `%LOCALAPPDATA%\Programs\Playtube` (keine
|
||||||
Admin-Rechte nötig) und legt Startmenü-Eintrag (optional Desktop-Verknüpfung) an,
|
Admin-Rechte nötig) und legt Startmenü-Eintrag (optional Desktop-Verknüpfung) an,
|
||||||
@@ -152,7 +171,7 @@ o.ä. umgehen).
|
|||||||
|
|
||||||
Playtube prüft beim Start und danach alle `updates.check_interval_hours` Stunden
|
Playtube prüft beim Start und danach alle `updates.check_interval_hours` Stunden
|
||||||
(Standard 6, im **Einstellungen-Tab** oder in `config.json` einstellbar) die
|
(Standard 6, im **Einstellungen-Tab** oder in `config.json` einstellbar) die
|
||||||
[GitHub Releases](https://github.com/fojadrachi/Playtube/releases) des Projekts. Im
|
[Releases](https://git.fojadrachi.de/Fojadrachi/Playtube/releases) auf dem eigenen Gitea-Server (nur Tags `-edge`). Im
|
||||||
Einstellungen-Tab gibt es zusätzlich einen "Jetzt nach Updates suchen"-Button mit
|
Einstellungen-Tab gibt es zusätzlich einen "Jetzt nach Updates suchen"-Button mit
|
||||||
Status-Anzeige und Fortschrittsbalken für den Download. Gibt es eine neuere Version,
|
Status-Anzeige und Fortschrittsbalken für den Download. Gibt es eine neuere Version,
|
||||||
fragt ein Dialog, ob sie installiert werden soll:
|
fragt ein Dialog, ob sie installiert werden soll:
|
||||||
@@ -193,7 +212,7 @@ Windows-Patch-Pakete tragen die eigene Dateiendung `.play` statt `.zip` (technis
|
|||||||
weiterhin ein ganz normales ZIP-Archiv). Playtube registriert `.play` beim ersten Start
|
weiterhin ein ganz normales ZIP-Archiv). Playtube registriert `.play` beim ersten Start
|
||||||
automatisch als Windows-Dateizuordnung - eine manuell heruntergeladene
|
automatisch als Windows-Dateizuordnung - eine manuell heruntergeladene
|
||||||
`Playtube-vX.Y.Z-win64-patch.play` (z.B. von der
|
`Playtube-vX.Y.Z-win64-patch.play` (z.B. von der
|
||||||
[Releases-Seite](https://github.com/fojadrachi/Playtube/releases)) lässt sich also
|
[Releases-Seite](https://git.fojadrachi.de/Fojadrachi/Playtube/releases)) lässt sich also
|
||||||
einfach per Doppelklick installieren, ohne dass Playtube selbst etwas herunterladen
|
einfach per Doppelklick installieren, ohne dass Playtube selbst etwas herunterladen
|
||||||
muss.
|
muss.
|
||||||
|
|
||||||
@@ -204,12 +223,32 @@ powershell -ExecutionPolicy Bypass -File packaging\release.ps1 -Version 1.1.0
|
|||||||
```
|
```
|
||||||
|
|
||||||
Das Skript setzt die Versionsnummer, committet, erstellt Git-Tag `v1.1.0` und pusht zu
|
Das Skript setzt die Versionsnummer, committet, erstellt Git-Tag `v1.1.0` und pusht zu
|
||||||
`origin` (dein Repo unter https://github.com/fojadrachi/Playtube). Der gepushte Tag
|
`origin` (dein Gitea unter https://git.fojadrachi.de/Fojadrachi/Playtube). Für Playtube Edge
|
||||||
löst automatisch die GitHub-Actions-Pipeline
|
trägt der Tag den Zusatz `-edge` (z. B. `v1.0.1-edge`). Der gepushte Tag löst automatisch die
|
||||||
([.github/workflows/release.yml](.github/workflows/release.yml)) aus, die **sowohl
|
Gitea-Actions-Pipeline ([.gitea/workflows/release.yml](.gitea/workflows/release.yml)) aus, die
|
||||||
eine Windows- als auch eine Linux-Version baut** und beide als Assets an einem GitHub
|
Playtube Edge für Windows baut und als **Vorabversion** auf dem Gitea veröffentlicht -
|
||||||
Release veröffentlicht - Fortschritt unter
|
Fortschritt unter https://git.fojadrachi.de/Fojadrachi/Playtube/actions.
|
||||||
https://github.com/fojadrachi/Playtube/actions.
|
|
||||||
|
#### Releases bauen (einmalige Einrichtung)
|
||||||
|
|
||||||
|
GitHub wird nirgends mehr benutzt. Die Pipeline braucht:
|
||||||
|
|
||||||
|
1. **Einen Gitea-Actions-Runner unter Windows** (`act_runner`, Label `windows`, Host-Modus)
|
||||||
|
mit Python 3.12, git, PowerShell und [Inno Setup 6](https://jrsoftware.org/isinfo.php).
|
||||||
|
Registrierung z. B.:
|
||||||
|
`act_runner register --instance https://git.fojadrachi.de --token <Runner-Token> --labels windows:host`
|
||||||
|
2. **Ein Secret `RELEASE_TOKEN`** im Repo (Einstellungen → Actions → Secrets): ein
|
||||||
|
Zugriffstoken (Gitea → Einstellungen → Anwendungen) mit Schreibrecht auf das Repo.
|
||||||
|
3. In Gitea **Actions für das Repo aktivieren** (Einstellungen → Erweitert → Repository-Einheiten).
|
||||||
|
|
||||||
|
**Ohne Runner** kannst du selbst bauen und veröffentlichen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File packaging\build.ps1
|
||||||
|
$env:GITEA_TOKEN = "<Zugriffstoken>"
|
||||||
|
powershell -ExecutionPolicy Bypass -File packaging\publish_release.ps1 -Tag v1.0.1-edge -Prerelease `
|
||||||
|
-Files dist\installer\PlaytubeEdge-Setup-v1.0.1.exe -NotesFile packaging\release_notes.md
|
||||||
|
```
|
||||||
|
|
||||||
Für Windows entstehen dabei drei Dateien: `Playtube-Setup-vX.Y.Z.exe` (Installer, für
|
Für Windows entstehen dabei drei Dateien: `Playtube-Setup-vX.Y.Z.exe` (Installer, für
|
||||||
Neueinsteiger und volle Updates), `Playtube-vX.Y.Z-win64.zip` (portabel) und das kleine
|
Neueinsteiger und volle Updates), `Playtube-vX.Y.Z-win64.zip` (portabel) und das kleine
|
||||||
@@ -221,9 +260,9 @@ neue Version danach automatisch angeboten.
|
|||||||
|
|
||||||
## Native Linux-Version
|
## Native Linux-Version
|
||||||
|
|
||||||
Playtube läuft genauso unter Linux (gleicher Code, gleiches PySide6/QtWebEngine) und
|
**Hinweis:** Playtube Edge nutzt WebView2 und läuft nur unter Windows. Die Linux-Beschreibung
|
||||||
wird bei jedem Release automatisch als `Playtube-vX.Y.Z-linux-x86_64.tar.gz` unter
|
unten gehört zur normalen Playtube (Branch `main`, QtWebEngine); für Edge entstehen keine
|
||||||
https://github.com/fojadrachi/Playtube/releases mitgebaut.
|
Linux-Pakete.
|
||||||
|
|
||||||
**Fertiges Release installieren** (richtet Startmenü-Eintrag + Icon ein):
|
**Fertiges Release installieren** (richtet Startmenü-Eintrag + Icon ein):
|
||||||
|
|
||||||
@@ -244,8 +283,7 @@ python3 -m venv .venv
|
|||||||
```
|
```
|
||||||
|
|
||||||
QtWebEngine benötigt unter Linux ein paar System-Bibliotheken (auf Debian/Ubuntu):
|
QtWebEngine benötigt unter Linux ein paar System-Bibliotheken (auf Debian/Ubuntu):
|
||||||
`sudo apt install libxkbcommon0 libegl1 libnss3 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64 libatk-bridge2.0-0 libcups2` (siehe auch die vollstaendige Liste in
|
`sudo apt install libxkbcommon0 libegl1 libnss3 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64 libatk-bridge2.0-0 libcups2`.
|
||||||
[.github/workflows/release.yml](.github/workflows/release.yml)).
|
|
||||||
|
|
||||||
## Hinweise
|
## Hinweise
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ from playtube import __version__, app_id # noqa: E402
|
|||||||
from playtube.config import APP_NAME, app_data_dir, clear_cache_on_update, load_config # noqa: E402
|
from playtube.config import APP_NAME, app_data_dir, clear_cache_on_update, load_config # noqa: E402
|
||||||
from playtube.remote_control import PIPE_PREFIX, RemoteControlServer # noqa: E402
|
from playtube.remote_control import PIPE_PREFIX, RemoteControlServer # noqa: E402
|
||||||
from playtube.single_instance import SingleInstanceGuard # noqa: E402
|
from playtube.single_instance import SingleInstanceGuard # noqa: E402
|
||||||
from playtube.shortcuts import ( # noqa: E402
|
from playtube.shortcuts import ensure_start_menu_shortcut # noqa: E402
|
||||||
ensure_play_file_association,
|
|
||||||
ensure_start_menu_shortcut,
|
|
||||||
)
|
|
||||||
from playtube.updater import cleanup_old_staging # noqa: E402
|
from playtube.updater import cleanup_old_staging # noqa: E402
|
||||||
|
|
||||||
app_id.set_app_user_model_id()
|
app_id.set_app_user_model_id()
|
||||||
@@ -26,12 +23,6 @@ app_id.configure_webengine_process_path()
|
|||||||
# High-DPI, sauberes GPU-Verhalten und (in Kombination mit den Sec-CH-UA-Headern in
|
# High-DPI, sauberes GPU-Verhalten und (in Kombination mit den Sec-CH-UA-Headern in
|
||||||
# playtube/browser.py) ein moeglichst "normales" Chrome-Fingerprint, damit Google-Login
|
# playtube/browser.py) ein moeglichst "normales" Chrome-Fingerprint, damit Google-Login
|
||||||
# das eingebettete QtWebEngine nicht als Embedded-WebView blockiert.
|
# das eingebettete QtWebEngine nicht als Embedded-WebView blockiert.
|
||||||
os.environ.setdefault(
|
|
||||||
"QTWEBENGINE_CHROMIUM_FLAGS",
|
|
||||||
"--disable-features=WinRetrieveSuggestionsOnlyOnDemand "
|
|
||||||
"--disable-blink-features=AutomationControlled",
|
|
||||||
)
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt # noqa: E402
|
from PySide6.QtCore import Qt # noqa: E402
|
||||||
from PySide6.QtGui import QIcon # noqa: E402
|
from PySide6.QtGui import QIcon # noqa: E402
|
||||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||||
@@ -78,7 +69,7 @@ def main() -> int:
|
|||||||
# komplett, da Playtube als portables ZIP ohne Installer ausgeliefert wird.
|
# komplett, da Playtube als portables ZIP ohne Installer ausgeliefert wird.
|
||||||
clear_cache_on_update(__version__)
|
clear_cache_on_update(__version__)
|
||||||
ensure_start_menu_shortcut(APP_NAME)
|
ensure_start_menu_shortcut(APP_NAME)
|
||||||
ensure_play_file_association(APP_NAME)
|
# Keine ".play"-Dateizuordnung: die gehoert der Qt-Version von Playtube (Edge nutzt nur Setup-Updates).
|
||||||
|
|
||||||
window = MainWindow(config)
|
window = MainWindow(config)
|
||||||
window.show()
|
window.show()
|
||||||
|
|||||||
+7
-56
@@ -1,19 +1,11 @@
|
|||||||
# Baut Playtube.exe mit PyInstaller und sorgt dafuer, dass auch der QtWebEngine-
|
# Baut PlaytubeEdge.exe (Playtube mit Microsoft-Edge-Engine / WebView2) mit PyInstaller und
|
||||||
# Hilfsprozess (der eigentlich den Ton ausgibt) den Namen "Playtube" traegt, damit er
|
# danach den Setup-Installer (Inno Setup 6).
|
||||||
# im Taskmanager und - so weit von der jeweiligen Windows-Version respektiert - im
|
|
||||||
# Lautstaerkemixer nicht als "QtWebEngineProcess" auftaucht.
|
|
||||||
#
|
#
|
||||||
# Aufruf (im Projekt-Root, mit aktivierter venv):
|
# Aufruf (im Projekt-Root, mit aktivierter venv):
|
||||||
# powershell -ExecutionPolicy Bypass -File packaging\build.ps1
|
# powershell -ExecutionPolicy Bypass -File packaging\build.ps1
|
||||||
#
|
|
||||||
# Optional: rcedit (https://github.com/electron/rcedit) im PATH oder unter
|
|
||||||
# packaging\rcedit.exe ablegen, dann werden Icon + Versionsinfo des umbenannten
|
|
||||||
# Hilfsprozesses ebenfalls auf "Playtube" gesetzt (sonst bleibt intern "QtWebEngineProcess"
|
|
||||||
# stehen, nur der Dateiname aendert sich - Windows zeigt dann meist trotzdem den
|
|
||||||
# Dateinamen "PlaytubeHelper" an).
|
|
||||||
|
|
||||||
param(
|
param(
|
||||||
# Ueberspringt den Bau des Setup-Installers (Playtube-Setup-vX.Y.Z.exe, benoetigt Inno Setup 6).
|
# Ueberspringt den Bau des Setup-Installers (PlaytubeEdge-Setup-vX.Y.Z.exe, benoetigt Inno Setup 6).
|
||||||
[switch]$SkipInstaller
|
[switch]$SkipInstaller
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,53 +13,13 @@ $ErrorActionPreference = "Stop"
|
|||||||
$root = Split-Path -Parent $PSScriptRoot
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
Set-Location $root
|
Set-Location $root
|
||||||
|
|
||||||
Write-Host "==> Baue Playtube.exe mit PyInstaller ..." -ForegroundColor Cyan
|
Write-Host "==> Baue PlaytubeEdge.exe mit PyInstaller ..." -ForegroundColor Cyan
|
||||||
pyinstaller packaging\playtube.spec --noconfirm --distpath dist --workpath build
|
pyinstaller packaging\playtube.spec --noconfirm --distpath dist --workpath build
|
||||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller-Build fehlgeschlagen." }
|
if ($LASTEXITCODE -ne 0) { throw "PyInstaller-Build fehlgeschlagen." }
|
||||||
|
Write-Host "Fertig! Die App liegt unter dist\PlaytubeEdge\PlaytubeEdge.exe" -ForegroundColor Green
|
||||||
$distDir = Join-Path $root "dist\Playtube"
|
|
||||||
$internalDir = Join-Path $distDir "_internal"
|
|
||||||
$searchRoot = if (Test-Path $internalDir) { $internalDir } else { $distDir }
|
|
||||||
|
|
||||||
$engineProcess = Get-ChildItem -Path $searchRoot -Recurse -Filter "QtWebEngineProcess.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
||||||
if (-not $engineProcess) {
|
|
||||||
Write-Warning "QtWebEngineProcess.exe wurde im Build nicht gefunden - Audio-/Taskmanager-Branding des Hilfsprozesses wird uebersprungen."
|
|
||||||
} else {
|
|
||||||
$helperPath = Join-Path $engineProcess.Directory.FullName "PlaytubeHelper.exe"
|
|
||||||
Copy-Item $engineProcess.FullName $helperPath -Force
|
|
||||||
Write-Host "==> Hilfsprozess kopiert nach $helperPath" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
$rcedit = Get-Command rcedit -ErrorAction SilentlyContinue
|
|
||||||
$rceditLocal = Join-Path $PSScriptRoot "rcedit.exe"
|
|
||||||
if ($rcedit) {
|
|
||||||
$rceditExe = $rcedit.Source
|
|
||||||
} elseif (Test-Path $rceditLocal) {
|
|
||||||
$rceditExe = $rceditLocal
|
|
||||||
} else {
|
|
||||||
$rceditExe = $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($rceditExe) {
|
|
||||||
Write-Host "==> Patche Icon/Versionsinfo des Hilfsprozesses mit rcedit ..." -ForegroundColor Cyan
|
|
||||||
& $rceditExe $helperPath --set-icon "$root\assets\icon.ico"
|
|
||||||
& $rceditExe $helperPath --set-version-string "FileDescription" "Playtube"
|
|
||||||
& $rceditExe $helperPath --set-version-string "ProductName" "Playtube"
|
|
||||||
& $rceditExe $helperPath --set-version-string "CompanyName" "Playtube"
|
|
||||||
& $rceditExe $helperPath --set-version-string "OriginalFilename" "PlaytubeHelper.exe"
|
|
||||||
} else {
|
|
||||||
Write-Host "Hinweis: rcedit nicht gefunden - Hilfsprozess heisst jetzt 'PlaytubeHelper.exe'," -ForegroundColor Yellow
|
|
||||||
Write-Host "seine interne Versionsinfo/Icon bleibt aber 'QtWebEngineProcess'. Fuer volles" -ForegroundColor Yellow
|
|
||||||
Write-Host "Branding: rcedit von https://github.com/electron/rcedit/releases laden und" -ForegroundColor Yellow
|
|
||||||
Write-Host "als packaging\rcedit.exe ablegen, dann dieses Skript erneut ausfuehren." -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Fertig! Die App liegt unter dist\Playtube\Playtube.exe" -ForegroundColor Green
|
|
||||||
|
|
||||||
# --- Setup-Installer (Inno Setup) ------------------------------------------------------
|
# --- Setup-Installer (Inno Setup) ------------------------------------------------------
|
||||||
# Baut aus dist\Playtube den Installer dist\installer\Playtube-Setup-vX.Y.Z.exe. Ein und
|
# Ein und derselbe Installer richtet Playtube Edge erstmalig ein UND aktualisiert spaeter eine
|
||||||
# derselbe Installer richtet Playtube erstmalig ein UND aktualisiert spaeter eine
|
|
||||||
# vorhandene Installation an Ort und Stelle (feste AppId) - es gibt nie mehrere Versionen.
|
# vorhandene Installation an Ort und Stelle (feste AppId) - es gibt nie mehrere Versionen.
|
||||||
if (-not $SkipInstaller) {
|
if (-not $SkipInstaller) {
|
||||||
$versionLine = Select-String -Path (Join-Path $root "playtube\__init__.py") -Pattern '__version__\s*=\s*"([^"]+)"' | Select-Object -First 1
|
$versionLine = Select-String -Path (Join-Path $root "playtube\__init__.py") -Pattern '__version__\s*=\s*"([^"]+)"' | Select-Object -First 1
|
||||||
@@ -86,12 +38,11 @@ if (-not $SkipInstaller) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Hinweis: Inno Setup 6 nicht gefunden - Setup-Installer wird uebersprungen." -ForegroundColor Yellow
|
Write-Host "Hinweis: Inno Setup 6 nicht gefunden - Setup-Installer wird uebersprungen." -ForegroundColor Yellow
|
||||||
Write-Host "Installieren: winget install --id JRSoftware.InnoSetup -e" -ForegroundColor Yellow
|
Write-Host "Installieren: winget install --id JRSoftware.InnoSetup -e" -ForegroundColor Yellow
|
||||||
Write-Host "(oder mit -SkipInstaller diesen Schritt bewusst auslassen)" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "==> Baue Setup-Installer (Version $version) mit Inno Setup ..." -ForegroundColor Cyan
|
Write-Host "==> Baue Setup-Installer (Version $version) mit Inno Setup ..." -ForegroundColor Cyan
|
||||||
& $iscc "/DAppVersion=$version" (Join-Path $PSScriptRoot "playtube.iss")
|
& $iscc "/DAppVersion=$version" (Join-Path $PSScriptRoot "playtube.iss")
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Inno-Setup-Build fehlgeschlagen." }
|
if ($LASTEXITCODE -ne 0) { throw "Inno-Setup-Build fehlgeschlagen." }
|
||||||
Write-Host "Fertig! Installer: dist\installer\Playtube-Setup-v$version.exe" -ForegroundColor Green
|
Write-Host "Fertig! Installer: dist\installer\PlaytubeEdge-Setup-v$version.exe" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-54
@@ -1,18 +1,19 @@
|
|||||||
; Inno-Setup-Skript fuer den Playtube-Windows-Installer (Playtube-Setup-vX.Y.Z.exe).
|
; Inno-Setup-Skript fuer den Playtube-Edge-Windows-Installer (PlaytubeEdge-Setup-vX.Y.Z.exe).
|
||||||
;
|
;
|
||||||
; Bauen (Inno Setup 6.3+ noetig, https://jrsoftware.org/isinfo.php):
|
; Bauen (Inno Setup 6.3+ noetig, https://jrsoftware.org/isinfo.php):
|
||||||
; ISCC.exe /DAppVersion=2.4.0 packaging\playtube.iss
|
; ISCC.exe /DAppVersion=1.0.0 packaging\playtube.iss
|
||||||
; Voraussetzung: dist\Playtube\ existiert bereits (PyInstaller-Build, siehe build.ps1).
|
; Voraussetzung: dist\PlaytubeEdge\ existiert bereits (PyInstaller-Build, siehe build.ps1).
|
||||||
; Ergebnis: dist\installer\Playtube-Setup-v2.4.0.exe
|
; Ergebnis: dist\installer\PlaytubeEdge-Setup-v1.0.0.exe
|
||||||
;
|
;
|
||||||
; Wichtig fuer "es soll nie mehrere Versionen geben":
|
; Playtube Edge ist bewusst eine EIGENE Anwendung neben der Qt-Version von Playtube:
|
||||||
; - Feste AppId (unten): jede neue Setup-Version erkennt darueber die vorhandene
|
; - Eigene AppId, eigener Installationsordner (%LOCALAPPDATA%\Programs\PlaytubeEdge), eigener
|
||||||
; Installation und ueberschreibt sie im SELBEN Ordner (statt eine zweite anzulegen).
|
; Datenordner (%APPDATA%\PlaytubeEdge), eigene Exe (PlaytubeEdge.exe). Beide Versionen koennen
|
||||||
; - Installiert pro Benutzer nach %LOCALAPPDATA%\Programs\Playtube (keine Admin-Rechte
|
; parallel installiert sein; keine ueberschreibt die andere.
|
||||||
; noetig, Ordner ist fuer Playtubes eigenen Updater beschreibbar).
|
; - Feste AppId: jede neue Edge-Setup-Version erkennt darueber die vorhandene Edge-Installation
|
||||||
; - Laufende Playtube-Prozesse werden vor dem Kopieren beendet, der alte Programmordner
|
; und ueberschreibt sie im SELBEN Ordner. Muss mit _INNO_APP_ID in playtube/updater.py
|
||||||
; (_internal) wird vor dem Kopieren geleert, damit keine Reste alter Versionen bleiben.
|
; uebereinstimmen.
|
||||||
; - Login/Einstellungen liegen in %APPDATA%\Playtube und bleiben bei Updates erhalten.
|
; - Keine ".play"-Dateizuordnung und kein Beenden von Playtube.exe / PlaytubeHelper.exe - das
|
||||||
|
; gehoert alles der Qt-Version.
|
||||||
;
|
;
|
||||||
; Hinweis zur Datei: bewusst reines ASCII (kein BOM), damit die Umlaute-Kodierung von
|
; Hinweis zur Datei: bewusst reines ASCII (kein BOM), damit die Umlaute-Kodierung von
|
||||||
; Inno Setup nicht ins Spiel kommt.
|
; Inno Setup nicht ins Spiel kommt.
|
||||||
@@ -21,38 +22,36 @@
|
|||||||
#define AppVersion "0.0.0"
|
#define AppVersion "0.0.0"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define AppName "Playtube"
|
#define AppName "Playtube Edge"
|
||||||
#define AppExeName "Playtube.exe"
|
#define AppDirName "PlaytubeEdge"
|
||||||
#define AppHelperName "PlaytubeHelper.exe"
|
#define AppExeName "PlaytubeEdge.exe"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
; NIEMALS aendern - identifiziert die Installation ueber alle Versionen hinweg. Muss mit
|
; NIEMALS aendern - identifiziert die Edge-Installation ueber alle Versionen hinweg.
|
||||||
; _INNO_APP_ID in playtube/updater.py uebereinstimmen.
|
AppId={{4F0F3698-DAF3-4028-B0A4-896D31292C28}
|
||||||
AppId={{87FBA502-2B84-4C42-A66B-2F03F490912D}
|
|
||||||
AppName={#AppName}
|
AppName={#AppName}
|
||||||
AppVersion={#AppVersion}
|
AppVersion={#AppVersion}
|
||||||
AppVerName={#AppName} {#AppVersion}
|
AppVerName={#AppName} {#AppVersion}
|
||||||
AppPublisher=Playtube
|
AppPublisher=Playtube
|
||||||
AppPublisherURL=https://github.com/fojadrachi/Playtube
|
AppPublisherURL=https://git.fojadrachi.de/Fojadrachi/Playtube
|
||||||
AppSupportURL=https://github.com/fojadrachi/Playtube/issues
|
AppSupportURL=https://git.fojadrachi.de/Fojadrachi/Playtube/issues
|
||||||
AppUpdatesURL=https://github.com/fojadrachi/Playtube/releases
|
AppUpdatesURL=https://git.fojadrachi.de/Fojadrachi/Playtube/releases
|
||||||
VersionInfoVersion={#AppVersion}
|
VersionInfoVersion={#AppVersion}
|
||||||
DefaultDirName={localappdata}\Programs\{#AppName}
|
DefaultDirName={localappdata}\Programs\{#AppDirName}
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
PrivilegesRequired=lowest
|
PrivilegesRequired=lowest
|
||||||
ArchitecturesAllowed=x64compatible
|
ArchitecturesAllowed=x64compatible
|
||||||
ArchitecturesInstallIn64BitMode=x64compatible
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
OutputDir=..\dist\installer
|
OutputDir=..\dist\installer
|
||||||
OutputBaseFilename=Playtube-Setup-v{#AppVersion}
|
OutputBaseFilename=PlaytubeEdge-Setup-v{#AppVersion}
|
||||||
SetupIconFile=..\assets\icon.ico
|
SetupIconFile=..\assets\icon.ico
|
||||||
UninstallDisplayIcon={app}\{#AppExeName}
|
UninstallDisplayIcon={app}\{#AppExeName}
|
||||||
UninstallDisplayName={#AppName}
|
UninstallDisplayName={#AppName}
|
||||||
Compression=lzma2/max
|
Compression=lzma2/max
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
; Prozesse beenden wir selbst (siehe [Code]) - Playtube versteckt sich beim Schliessen des
|
; Prozesse beenden wir selbst (siehe [Code]) - Playtube Edge versteckt sich beim Schliessen des
|
||||||
; Fensters nur im Tray und reagiert daher nicht zuverlaessig auf die Aufforderung, sich zu
|
; Fensters nur im Tray und reagiert daher nicht zuverlaessig auf CloseApplications.
|
||||||
; schliessen, die Inno per CloseApplications senden wuerde.
|
|
||||||
CloseApplications=no
|
CloseApplications=no
|
||||||
RestartApplications=no
|
RestartApplications=no
|
||||||
|
|
||||||
@@ -69,27 +68,16 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
|
|||||||
Type: filesandordirs; Name: "{app}\_internal"
|
Type: filesandordirs; Name: "{app}\_internal"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\dist\Playtube\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\dist\PlaytubeEdge\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
; {autoprograms} = %APPDATA%\Microsoft\Windows\Start Menu\Programs - derselbe Pfad, den die
|
|
||||||
; App frueher selbst fuer eine portable Kopie angelegt hat; eine alte Verknuepfung, die
|
|
||||||
; noch auf einen Downloads-Ordner zeigt, wird dadurch ersetzt.
|
|
||||||
Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||||
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
[Registry]
|
|
||||||
; Dateizuordnung fuer ".play" (Playtube-Patchdateien, siehe playtube/shortcuts.py).
|
|
||||||
Root: HKCU; Subkey: "Software\Classes\.play"; ValueType: string; ValueName: ""; ValueData: "{#AppName}.PatchFile"; Flags: uninsdeletekey
|
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#AppName}.PatchFile"; ValueType: string; ValueName: ""; ValueData: "{#AppName}-Patchdatei"; Flags: uninsdeletekey
|
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#AppName}.PatchFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#AppExeName}"
|
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#AppName}.PatchFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}"" ""%1"""
|
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
; Normale (sichtbare) Installation: Haekchen "Playtube jetzt starten" am Ende.
|
; Normale (sichtbare) Installation: Haekchen "Playtube Edge jetzt starten" am Ende.
|
||||||
Filename: "{app}\{#AppExeName}"; Description: "{#AppName} jetzt starten"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#AppExeName}"; Description: "{#AppName} jetzt starten"; Flags: nowait postinstall skipifsilent
|
||||||
; Stilles Update durch Playtubes eigenen Updater (/RELAUNCH=1): App danach automatisch
|
; Stilles Update durch den eigenen Updater (/RELAUNCH=1): App danach automatisch wieder starten.
|
||||||
; wieder starten.
|
|
||||||
Filename: "{app}\{#AppExeName}"; Flags: nowait; Check: ShouldRelaunch
|
Filename: "{app}\{#AppExeName}"; Flags: nowait; Check: ShouldRelaunch
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
@@ -107,10 +95,8 @@ procedure WaitForOldInstance();
|
|||||||
var
|
var
|
||||||
OldPid, ResultCode: Integer;
|
OldPid, ResultCode: Integer;
|
||||||
begin
|
begin
|
||||||
{ Playtubes Updater startet Setup aus der laufenden App heraus und uebergibt deren
|
{ Der Updater startet Setup aus der laufenden App heraus und uebergibt deren Prozess-ID
|
||||||
Prozess-ID (/WAITPID=...). Setup wartet, bis sich diese Instanz selbst beendet hat,
|
(/WAITPID=...). Setup wartet, bis sich diese Instanz selbst beendet hat (max. 20 s). }
|
||||||
statt sie hart abzuschiessen (max. 20 s). Ohne Angabe (Setup per Doppelklick) passiert
|
|
||||||
hier nichts. }
|
|
||||||
OldPid := StrToIntDef(ExpandConstant('{param:WAITPID|0}'), 0);
|
OldPid := StrToIntDef(ExpandConstant('{param:WAITPID|0}'), 0);
|
||||||
if OldPid > 0 then
|
if OldPid > 0 then
|
||||||
Exec(PowerShellPath(),
|
Exec(PowerShellPath(),
|
||||||
@@ -122,16 +108,13 @@ procedure StopLeftoverProcesses();
|
|||||||
var
|
var
|
||||||
ResultCode: Integer;
|
ResultCode: Integer;
|
||||||
begin
|
begin
|
||||||
{ WICHTIG: bewusst OHNE "/T" (Prozessbaum mitbeenden). Der Updater startet Setup aus
|
{ WICHTIG: bewusst OHNE "/T" - der Updater startet Setup aus PlaytubeEdge.exe heraus, Setup ist
|
||||||
Playtube.exe heraus - Setup ist also ein Kindprozess. Solange Playtube noch beendet
|
also ein Kindprozess; "/T" wuerde den Installer selbst mit abschiessen. Es wird nur
|
||||||
wird, wuerde "/T" den Installer selbst mit abschiessen, bevor er etwas installiert hat
|
PlaytubeEdge.exe beendet, NIE Playtube.exe der Qt-Version. }
|
||||||
(so ging frueher ein Update still verloren: kein Kopieren, kein Neustart). }
|
|
||||||
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppExeName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppExeName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppHelperName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
|
||||||
|
|
||||||
{ QtWebEngine-Kindprozesse, die nach dem Ende von Playtube noch aus dem
|
{ WebView2-Kindprozesse (msedgewebview2.exe), die noch aus dem Installationsordner laufen,
|
||||||
Installationsordner laufen, wuerden Dateien sperren - gezielt nach Pfad beenden
|
wuerden Dateien sperren - gezielt nach Pfad beenden. }
|
||||||
(nicht per Namen: QtWebEngineProcess.exe heisst auch bei anderen Qt-Programmen). }
|
|
||||||
if DirExists(ExpandConstant('{app}')) then
|
if DirExists(ExpandConstant('{app}')) then
|
||||||
Exec(PowerShellPath(),
|
Exec(PowerShellPath(),
|
||||||
'-NoProfile -NonInteractive -WindowStyle Hidden -Command "$d = ''' + ExpandConstant('{app}') + '\''; ' +
|
'-NoProfile -NonInteractive -WindowStyle Hidden -Command "$d = ''' + ExpandConstant('{app}') + '\''; ' +
|
||||||
@@ -161,8 +144,8 @@ begin
|
|||||||
if (CurUninstallStep = usPostUninstall) and (not UninstallSilent()) then
|
if (CurUninstallStep = usPostUninstall) and (not UninstallSilent()) then
|
||||||
begin
|
begin
|
||||||
if MsgBox('Sollen auch die Einstellungen und der gespeicherte Login von {#AppName} geloescht werden?' + #13#10 +
|
if MsgBox('Sollen auch die Einstellungen und der gespeicherte Login von {#AppName} geloescht werden?' + #13#10 +
|
||||||
'(Ordner: ' + ExpandConstant('{userappdata}\{#AppName}') + ')',
|
'(Ordner: ' + ExpandConstant('{userappdata}\{#AppDirName}') + ')',
|
||||||
mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||||
DelTree(ExpandConstant('{userappdata}\{#AppName}'), True, True, True);
|
DelTree(ExpandConstant('{userappdata}\{#AppDirName}'), True, True, True);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+15
-6
@@ -4,22 +4,31 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
|
||||||
ROOT = Path(SPECPATH).resolve().parent
|
ROOT = Path(SPECPATH).resolve().parent
|
||||||
|
|
||||||
|
# Edge-Variante: pythonnet + clr_loader (bringen Python.Runtime.dll/ClrLoader.dll mit) und die
|
||||||
|
# Microsoft-WebView2-DLLs aus vendor/webview2 (siehe playtube/webview2_runtime.py).
|
||||||
|
pythonnet_datas, pythonnet_binaries, pythonnet_hidden = collect_all("pythonnet")
|
||||||
|
clr_datas, clr_binaries, clr_hidden = collect_all("clr_loader")
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
[str(ROOT / "main.py")],
|
[str(ROOT / "main.py")],
|
||||||
pathex=[str(ROOT)],
|
pathex=[str(ROOT)],
|
||||||
binaries=[],
|
binaries=pythonnet_binaries + clr_binaries,
|
||||||
datas=[
|
datas=[
|
||||||
(str(ROOT / "assets"), "assets"),
|
(str(ROOT / "assets"), "assets"),
|
||||||
(str(ROOT / "config.json"), "."),
|
(str(ROOT / "config.json"), "."),
|
||||||
],
|
(str(ROOT / "vendor" / "webview2"), "vendor/webview2"),
|
||||||
hiddenimports=["pypresence"],
|
] + pythonnet_datas + clr_datas,
|
||||||
|
hiddenimports=["pypresence", "clr"] + pythonnet_hidden + clr_hidden,
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=[],
|
# Kein QtWebEngine mehr (WebView2 statt dessen) - haelt das Paket klein.
|
||||||
|
excludes=["PySide6.QtWebEngineCore", "PySide6.QtWebEngineWidgets", "PySide6.QtWebEngineQuick"],
|
||||||
noarchive=False,
|
noarchive=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +38,7 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|||||||
# Linux/macOS gibt es diese Konzepte fuer ELF-Binaries nicht, deshalb nur dort setzen.
|
# Linux/macOS gibt es diese Konzepte fuer ELF-Binaries nicht, deshalb nur dort setzen.
|
||||||
exe_kwargs = dict(
|
exe_kwargs = dict(
|
||||||
exclude_binaries=True,
|
exclude_binaries=True,
|
||||||
name="Playtube",
|
name="PlaytubeEdge",
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
@@ -49,5 +58,5 @@ coll = COLLECT(
|
|||||||
a.datas,
|
a.datas,
|
||||||
strip=False,
|
strip=False,
|
||||||
upx=False,
|
upx=False,
|
||||||
name="Playtube",
|
name="PlaytubeEdge",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Veroeffentlicht ein Release auf dem eigenen Gitea-Server (Installer, ZIP, Patch als Anhaenge).
|
||||||
|
# Wird von der Pipeline (.gitea/workflows/release.yml) benutzt und laesst sich auch von Hand
|
||||||
|
# aufrufen, z.B. nach einem lokalen Build (packaging\build.ps1):
|
||||||
|
#
|
||||||
|
# $env:GITEA_TOKEN = "<Zugriffstoken>"
|
||||||
|
# powershell -ExecutionPolicy Bypass -File packaging\publish_release.ps1 `
|
||||||
|
# -Tag v4.6.0 -Files dist\installer\Playtube-Setup-v4.6.0.exe,Playtube-v4.6.0-win64.zip
|
||||||
|
#
|
||||||
|
# Der Token wird in Gitea unter Einstellungen > Anwendungen erzeugt (Recht "repository: Lesen und
|
||||||
|
# Schreiben"). Er steht NIE im Skript, sondern nur in der Umgebungsvariable GITEA_TOKEN.
|
||||||
|
# Gab es zum selben Tag schon ein Release, wird es vorher entfernt (der Git-Tag bleibt).
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)] [string]$Tag,
|
||||||
|
[Parameter(Mandatory = $true)] [string[]]$Files,
|
||||||
|
[string]$Title = "",
|
||||||
|
[string]$NotesFile = "",
|
||||||
|
[switch]$Prerelease,
|
||||||
|
[string]$Server = "https://git.fojadrachi.de",
|
||||||
|
[string]$Repo = "Fojadrachi/Playtube"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$token = $env:GITEA_TOKEN
|
||||||
|
if (-not $token) {
|
||||||
|
# Alternativ aus einer lokalen Datei (nur fuer deinen Benutzer): %USERPROFILE%\.gitea-token
|
||||||
|
$tokenFile = Join-Path $env:USERPROFILE ".gitea-token"
|
||||||
|
if (Test-Path $tokenFile) { $token = (Get-Content -Raw $tokenFile).Trim() }
|
||||||
|
}
|
||||||
|
if (-not $token) {
|
||||||
|
throw "Kein Token: GITEA_TOKEN setzen oder ihn in %USERPROFILE%\.gitea-token ablegen (Zugriffstoken aus Gitea > Einstellungen > Anwendungen)."
|
||||||
|
}
|
||||||
|
if (-not $Title) { $Title = $Tag }
|
||||||
|
foreach ($file in $Files) {
|
||||||
|
if (-not (Test-Path $file)) { throw "Datei nicht gefunden: $file" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = "$Server/api/v1/repos/$Repo"
|
||||||
|
$headers = @{ Authorization = "token $token" }
|
||||||
|
|
||||||
|
# Vorhandenes Release zum selben Tag entfernen (404 = gab keins).
|
||||||
|
try {
|
||||||
|
$old = Invoke-RestMethod -Headers $headers -Uri "$api/releases/tags/$Tag"
|
||||||
|
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($old.id)" | Out-Null
|
||||||
|
Write-Host "Vorhandenes Release zu $Tag ersetzt."
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response -and [int]$_.Exception.Response.StatusCode -ne 404) { throw }
|
||||||
|
}
|
||||||
|
|
||||||
|
$notes = ""
|
||||||
|
# [string]: sonst serialisiert ConvertTo-Json die PowerShell-Zusatzeigenschaften mit (Objekt statt Text).
|
||||||
|
if ($NotesFile -and (Test-Path $NotesFile)) { $notes = [string](Get-Content -Raw -Encoding UTF8 $NotesFile) }
|
||||||
|
|
||||||
|
$payload = @{
|
||||||
|
tag_name = $Tag
|
||||||
|
name = $Title
|
||||||
|
body = $notes
|
||||||
|
draft = $false
|
||||||
|
prerelease = [bool]$Prerelease
|
||||||
|
} | ConvertTo-Json
|
||||||
|
$release = Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases" `
|
||||||
|
-ContentType "application/json; charset=utf-8" -Body ([Text.Encoding]::UTF8.GetBytes($payload))
|
||||||
|
|
||||||
|
foreach ($file in $Files) {
|
||||||
|
$name = Split-Path $file -Leaf
|
||||||
|
Write-Host "Lade hoch: $name"
|
||||||
|
& curl.exe -sS --fail -X POST -H "Authorization: token $token" -F "attachment=@$file" `
|
||||||
|
"$api/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($name))" | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Upload fehlgeschlagen: $name" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Fertig: $Server/$Repo/releases/tag/$Tag" -ForegroundColor Green
|
||||||
+15
-14
@@ -1,13 +1,14 @@
|
|||||||
# Veroeffentlicht eine neue Playtube-Version: setzt die Versionsnummer, committet,
|
# Veroeffentlicht eine neue Playtube-Edge-Version: setzt die Versionsnummer, committet,
|
||||||
# taggt und pusht. Der eigentliche Build fuer Windows UND Linux sowie die
|
# taggt ("vX.Y.Z-edge") und pusht. Build und Veroeffentlichung als Vorabversion auf dem
|
||||||
# Veroeffentlichung als GitHub Release passiert danach automatisch per GitHub Actions
|
# eigenen Gitea-Server passieren danach automatisch per Gitea Actions
|
||||||
# (.github/workflows/release.yml), ausgeloest durch den gepushten Tag.
|
# (.gitea/workflows/release.yml), ausgeloest durch den gepushten Tag.
|
||||||
#
|
#
|
||||||
# Aufruf (im Projekt-Root):
|
# Aufruf (im Projekt-Root, auf dem Branch "edge"):
|
||||||
# powershell -ExecutionPolicy Bypass -File packaging\release.ps1 -Version 1.1.0
|
# powershell -ExecutionPolicy Bypass -File packaging\release.ps1 -Version 1.0.1
|
||||||
#
|
#
|
||||||
# Voraussetzung: git remote "origin" zeigt auf https://github.com/fojadrachi/Playtube
|
# Voraussetzung: git remote "origin" zeigt auf https://git.fojadrachi.de/Fojadrachi/Playtube
|
||||||
# und du bist dort push-berechtigt.
|
# und du bist dort push-berechtigt. Ohne Actions-Runner kannst du lokal bauen
|
||||||
|
# (packaging\build.ps1) und mit packaging\publish_release.ps1 -Prerelease selbst veroeffentlichen.
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@@ -19,9 +20,9 @@ $root = Split-Path -Parent $PSScriptRoot
|
|||||||
Set-Location $root
|
Set-Location $root
|
||||||
|
|
||||||
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
|
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
|
||||||
throw "Version muss im Format X.Y.Z angegeben werden, z.B. 1.1.0"
|
throw "Version muss im Format X.Y.Z angegeben werden, z.B. 1.0.1"
|
||||||
}
|
}
|
||||||
$tag = "v$Version"
|
$tag = "v$Version-edge"
|
||||||
|
|
||||||
Write-Host "==> Setze Version auf $Version in playtube/__init__.py" -ForegroundColor Cyan
|
Write-Host "==> Setze Version auf $Version in playtube/__init__.py" -ForegroundColor Cyan
|
||||||
$initFile = Join-Path $root "playtube\__init__.py"
|
$initFile = Join-Path $root "playtube\__init__.py"
|
||||||
@@ -30,13 +31,13 @@ $initFile = Join-Path $root "playtube\__init__.py"
|
|||||||
Write-Host "==> Commit + Tag $tag ..." -ForegroundColor Cyan
|
Write-Host "==> Commit + Tag $tag ..." -ForegroundColor Cyan
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "Release $tag" --allow-empty
|
git commit -m "Release $tag" --allow-empty
|
||||||
git tag -a $tag -m "Playtube $tag"
|
git tag -a $tag -m "Playtube Edge $tag"
|
||||||
|
|
||||||
Write-Host "==> Push zu origin ..." -ForegroundColor Cyan
|
Write-Host "==> Push zu origin ..." -ForegroundColor Cyan
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
git push origin $tag
|
git push origin $tag
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Fertig. GitHub Actions baut jetzt Windows- und Linux-Pakete und" -ForegroundColor Green
|
Write-Host "Fertig. Gitea Actions baut jetzt Playtube Edge und" -ForegroundColor Green
|
||||||
Write-Host "veroeffentlicht sie als Release $tag - Fortschritt unter:" -ForegroundColor Green
|
Write-Host "veroeffentlicht es als Release $tag - Fortschritt unter:" -ForegroundColor Green
|
||||||
Write-Host "https://github.com/fojadrachi/Playtube/actions" -ForegroundColor Green
|
Write-Host "https://git.fojadrachi.de/Fojadrachi/Playtube/actions" -ForegroundColor Green
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Playtube Edge - die Version für eigene (hochgeladene) Musik
|
||||||
|
|
||||||
|
**Diese Version brauchst du, wenn du selbst hochgeladene Songs in YouTube Music hören willst.**
|
||||||
|
Die normale Playtube-Version kann sie nicht abspielen („Dieses Videoformat wird nicht
|
||||||
|
unterstützt“), weil ihre eingebaute Browser-Engine (QtWebEngine) kein AAC und kein H.264 kann.
|
||||||
|
Playtube Edge benutzt stattdessen die Microsoft-Edge-Engine (WebView2) – damit laufen auch
|
||||||
|
hochgeladene Titel.
|
||||||
|
|
||||||
|
### Installieren
|
||||||
|
- Download: **PlaytubeEdge-Setup-vX.Y.Z.exe** (Setup, empfohlen) oder die ZIP-Datei (portabel).
|
||||||
|
- Voraussetzung: Windows 10/11 mit „WebView2 Runtime“ (auf Windows 11 schon vorhanden, sonst
|
||||||
|
kostenlos bei Microsoft).
|
||||||
|
- Playtube Edge läuft **neben** der normalen Playtube (eigener Ordner, eigene Einstellungen).
|
||||||
|
Beim ersten Start musst du dich in YouTube/YouTube Music einmal neu anmelden. Lass am besten
|
||||||
|
nur eine der beiden Versionen gleichzeitig spielen.
|
||||||
|
- Updates: Playtube Edge aktualisiert sich nur über diese „Edge“-Releases – nie über die
|
||||||
|
normale Playtube (und umgekehrt).
|
||||||
|
|
||||||
|
Wer keine eigenen Uploads abspielt, braucht diese Version nicht – die normale Playtube reicht dann.
|
||||||
@@ -19,12 +19,12 @@ VSVersionInfo(
|
|||||||
StringTable(
|
StringTable(
|
||||||
u'040704B0',
|
u'040704B0',
|
||||||
[StringStruct(u'CompanyName', u'Playtube'),
|
[StringStruct(u'CompanyName', u'Playtube'),
|
||||||
StringStruct(u'FileDescription', u'Playtube'),
|
StringStruct(u'FileDescription', u'Playtube Edge'),
|
||||||
StringStruct(u'FileVersion', u'1.0.0.0'),
|
StringStruct(u'FileVersion', u'1.0.0.0'),
|
||||||
StringStruct(u'InternalName', u'Playtube'),
|
StringStruct(u'InternalName', u'PlaytubeEdge'),
|
||||||
StringStruct(u'LegalCopyright', u''),
|
StringStruct(u'LegalCopyright', u''),
|
||||||
StringStruct(u'OriginalFilename', u'Playtube.exe'),
|
StringStruct(u'OriginalFilename', u'PlaytubeEdge.exe'),
|
||||||
StringStruct(u'ProductName', u'Playtube'),
|
StringStruct(u'ProductName', u'Playtube Edge'),
|
||||||
StringStruct(u'ProductVersion', u'1.0.0.0')])
|
StringStruct(u'ProductVersion', u'1.0.0.0')])
|
||||||
]),
|
]),
|
||||||
VarFileInfo([VarStruct(u'Translation', [1031, 1200])])
|
VarFileInfo([VarStruct(u'Translation', [1031, 1200])])
|
||||||
|
|||||||
@@ -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 Edge"
|
||||||
__version__ = "4.5.1"
|
__version__ = "1.0.0"
|
||||||
|
|||||||
@@ -30,15 +30,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from PySide6.QtCore import QUrl
|
|
||||||
from PySide6.QtMultimedia import QMediaDevices
|
from PySide6.QtMultimedia import QMediaDevices
|
||||||
from PySide6.QtWebEngineCore import QWebEnginePermission, QWebEngineProfile
|
|
||||||
|
|
||||||
# Name des eingeschleusten Skripts (pro Seite), um es beim Aendern ersetzen zu koennen.
|
|
||||||
SCRIPT_NAME = "playtube-audio-output"
|
|
||||||
|
|
||||||
# Herkunft (Origin), fuer die die Berechtigung erteilt wird - nur die beiden Tabs.
|
|
||||||
YOUTUBE_ORIGINS = ("https://www.youtube.com", "https://music.youtube.com")
|
|
||||||
|
|
||||||
SYSTEM_DEFAULT_LABEL = "Systemstandard"
|
SYSTEM_DEFAULT_LABEL = "Systemstandard"
|
||||||
|
|
||||||
@@ -54,19 +46,13 @@ def list_output_devices() -> list[str]:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
def sync_audio_permissions(profile: QWebEngineProfile, wanted: bool) -> None:
|
def sync_audio_permissions(tabs, wanted: bool) -> None:
|
||||||
"""Erteilt (wanted=True) bzw. entzieht (wanted=False) die Audiogeraete-Berechtigung fuer
|
"""Erlaubt (wanted=True) bzw. entzieht (wanted=False) den Browser-Tabs die
|
||||||
YouTube/YouTube Musik - damit die Seite die Namen der Ausgabegeraete sehen kann (siehe
|
Audiogeraete-Berechtigung fuer YouTube/YouTube Musik - damit die Seite die Namen der
|
||||||
Moduldoku). Gilt nur fuer die laufende Sitzung, deshalb bei jedem Start aufrufen. Der
|
Ausgabegeraete sehen kann (siehe Moduldoku). In der Edge-Variante setzt jeder Tab die
|
||||||
Status wird von Qt fuer Mikrofon-Freigaben nicht zuverlaessig gemeldet (bleibt "Ask"),
|
Freigabe selbst (WebView2-Profil, bleibt dort gespeichert)."""
|
||||||
daher wird hier einfach jedes Mal erteilt bzw. zurueckgesetzt."""
|
for tab in tabs:
|
||||||
permission_type = QWebEnginePermission.PermissionType.MediaAudioCapture
|
tab.set_device_names_allowed(wanted)
|
||||||
for origin in YOUTUBE_ORIGINS:
|
|
||||||
permission = profile.queryPermission(QUrl(origin), permission_type)
|
|
||||||
if wanted:
|
|
||||||
permission.grant()
|
|
||||||
else:
|
|
||||||
permission.reset()
|
|
||||||
|
|
||||||
|
|
||||||
# JavaScript-Vorlage; __TARGET__ wird durch den (JSON-kodierten) Geraetenamen ersetzt.
|
# JavaScript-Vorlage; __TARGET__ wird durch den (JSON-kodierten) Geraetenamen ersetzt.
|
||||||
|
|||||||
+283
-171
@@ -1,25 +1,23 @@
|
|||||||
"""Eingebetteter Browser-Tab fuer YouTube bzw. YouTube Music, mit persistentem Login-
|
"""Eingebetteter Browser-Tab (Microsoft Edge WebView2) fuer YouTube bzw. YouTube Music, mit
|
||||||
Profil (eigener Datenordner, kein System-Browser-Profil) und periodischem Auslesen
|
persistentem Login-Profil und periodischem Auslesen der aktuellen Wiedergabe fuer Discord Rich
|
||||||
der aktuellen Wiedergabe fuer Discord Rich Presence."""
|
Presence.
|
||||||
|
|
||||||
|
Edge-Variante von Playtube: statt QtWebEngine (kein AAC/H.264 - hochgeladene Titel liessen sich
|
||||||
|
nicht abspielen) rendert hier WebView2. Die Schnittstelle von BrowserTab ist dieselbe wie in der
|
||||||
|
Qt-Variante, damit MainWindow, Fernsteuerung und Discord unveraendert bleiben.
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ctypes
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
from typing import Any, Callable
|
||||||
|
|
||||||
from PySide6.QtCore import QTimer, Signal, QUrl
|
from PySide6.QtCore import Qt, QTimer, QUrl, Signal
|
||||||
from PySide6.QtWebEngineCore import (
|
from PySide6.QtWidgets import QWidget
|
||||||
QWebEnginePage,
|
|
||||||
QWebEngineProfile,
|
|
||||||
QWebEngineScript,
|
|
||||||
QWebEngineSettings,
|
|
||||||
QWebEngineUrlRequestInterceptor,
|
|
||||||
)
|
|
||||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
||||||
|
|
||||||
from .audio_routing import SCRIPT_NAME as AUDIO_SCRIPT_NAME
|
from . import webview2_runtime
|
||||||
from .audio_routing import build_router_script
|
from .audio_routing import build_router_script
|
||||||
from .chrome_shim import CHROME_FULL, CHROME_MAJOR, CHROME_SHIM_JS
|
|
||||||
from .config import profile_dir
|
from .config import profile_dir
|
||||||
from .media_control import LIST_PLAYLISTS_JS, MUSIC_PLAYLIST_URL, build_control_js
|
from .media_control import LIST_PLAYLISTS_JS, MUSIC_PLAYLIST_URL, build_control_js
|
||||||
from .media_probe import MEDIA_PROBE_JS, NEXT_TRACK_JS, PREV_TRACK_JS, TOGGLE_PLAYBACK_JS
|
from .media_probe import MEDIA_PROBE_JS, NEXT_TRACK_JS, PREV_TRACK_JS, TOGGLE_PLAYBACK_JS
|
||||||
@@ -27,214 +25,328 @@ from .media_probe import MEDIA_PROBE_JS, NEXT_TRACK_JS, PREV_TRACK_JS, TOGGLE_PL
|
|||||||
# Nach einem Fernsteuerungsbefehl den Status kurz darauf neu auslesen (Seite braucht einen
|
# Nach einem Fernsteuerungsbefehl den Status kurz darauf neu auslesen (Seite braucht einen
|
||||||
# Moment, bis z.B. der neue Titel/Like-Status im DOM steht).
|
# Moment, bis z.B. der neue Titel/Like-Status im DOM steht).
|
||||||
_REFRESH_AFTER_COMMAND_MS = 300
|
_REFRESH_AFTER_COMMAND_MS = 300
|
||||||
|
_TASK_POLL_MS = 25
|
||||||
|
_MEDIA_POLL_MS = 2000
|
||||||
|
_YOUTUBE_ORIGINS = ("https://www.youtube.com", "https://music.youtube.com")
|
||||||
|
# Ohne Nutzergeste starten koennen (Playlist-Start per Stream Dock, Autoplay).
|
||||||
|
_BROWSER_ARGUMENTS = "--autoplay-policy=no-user-gesture-required"
|
||||||
|
|
||||||
# Chrome-Versionsnummer, die exakt zur tatsaechlich in QtWebEngine eingebetteten
|
_user32 = ctypes.windll.user32 if os.name == "nt" else None
|
||||||
# Chromium-Version passt (siehe QWebEngineCore.qWebEngineChromiumVersion()). Legacy-
|
if _user32 is not None:
|
||||||
# User-Agent, die "Sec-CH-UA" Client-Hints (Header) UND navigator.userAgentData (JS,
|
# 64-Bit-sichere Signaturen (Fensterstil und Handles sind pointer-gross).
|
||||||
# siehe chrome_shim.py) muessen konsistent dieselbe Version + Marke ("Google Chrome")
|
_user32.GetWindowLongPtrW.restype = ctypes.c_ssize_t
|
||||||
# melden - sonst erkennt Google-Login den Browser als nicht vertrauenswuerdiges
|
_user32.GetWindowLongPtrW.argtypes = (ctypes.c_void_p, ctypes.c_int)
|
||||||
# Embedded-WebView und blockiert die Anmeldung mit "Dieser Browser oder diese App ist
|
_user32.SetWindowLongPtrW.restype = ctypes.c_ssize_t
|
||||||
# unter Umstaenden nicht sicher".
|
_user32.SetWindowLongPtrW.argtypes = (ctypes.c_void_p, ctypes.c_int, ctypes.c_ssize_t)
|
||||||
_CHROME_VERSION = CHROME_FULL
|
_user32.SetParent.argtypes = (ctypes.c_void_p, ctypes.c_void_p)
|
||||||
_CHROME_MAJOR = CHROME_MAJOR
|
_user32.MoveWindow.argtypes = (ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_bool)
|
||||||
|
_user32.ShowWindow.argtypes = (ctypes.c_void_p, ctypes.c_int)
|
||||||
|
|
||||||
if sys.platform == "win32":
|
_GWL_STYLE = -16
|
||||||
_UA_PLATFORM_TOKEN = "Windows NT 10.0; Win64; x64"
|
_WS_POPUP = 0x80000000
|
||||||
_SEC_CH_UA_PLATFORM = b'"Windows"'
|
_WS_CAPTION = 0x00C00000
|
||||||
_SEC_CH_UA_PLATFORM_VERSION = b'"15.0.0"'
|
_WS_CHILD = 0x40000000
|
||||||
elif sys.platform.startswith("linux"):
|
_WS_VISIBLE = 0x10000000
|
||||||
_UA_PLATFORM_TOKEN = "X11; Linux x86_64"
|
_WS_CLIPSIBLINGS = 0x04000000
|
||||||
_SEC_CH_UA_PLATFORM = b'"Linux"'
|
_WS_CLIPCHILDREN = 0x02000000
|
||||||
_SEC_CH_UA_PLATFORM_VERSION = b'"6.0.0"'
|
_SW_SHOW = 5
|
||||||
else:
|
|
||||||
_UA_PLATFORM_TOKEN = "Macintosh; Intel Mac OS X 10_15_7"
|
|
||||||
_SEC_CH_UA_PLATFORM = b'"macOS"'
|
|
||||||
_SEC_CH_UA_PLATFORM_VERSION = b'"14.0.0"'
|
|
||||||
|
|
||||||
_USER_AGENT = (
|
|
||||||
f"Mozilla/5.0 ({_UA_PLATFORM_TOKEN}) AppleWebKit/537.36 "
|
def _decode_script_result(raw: Any) -> Any:
|
||||||
f"(KHTML, like Gecko) Chrome/{_CHROME_VERSION} Safari/537.36"
|
"""ExecuteScriptAsync liefert das Ergebnis JSON-kodiert; unsere Skripte geben selbst
|
||||||
|
einen JSON-Text zurueck, deshalb wird bei Bedarf ein zweites Mal dekodiert."""
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = json.loads(str(raw))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _in_document(source: str) -> str:
|
||||||
|
"""Umhuellt ein Skript, das document.documentElement braucht, damit es auch beim frueh
|
||||||
|
(vor dem DOM) ausgefuehrten WebView2-Dokumentskript funktioniert."""
|
||||||
|
return (
|
||||||
|
"(function(){function go(){" + source + "}"
|
||||||
|
"if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',go);}"
|
||||||
|
"else{go();}})();"
|
||||||
)
|
)
|
||||||
|
|
||||||
_SEC_CH_UA = (
|
|
||||||
f'"Not(A:Brand";v="99", "Google Chrome";v="{_CHROME_MAJOR}", '
|
|
||||||
f'"Chromium";v="{_CHROME_MAJOR}"'
|
|
||||||
).encode("ascii")
|
|
||||||
_SEC_CH_UA_FULL_VERSION_LIST = (
|
|
||||||
f'"Not(A:Brand";v="99.0.0.0", "Google Chrome";v="{_CHROME_VERSION}", '
|
|
||||||
f'"Chromium";v="{_CHROME_VERSION}"'
|
|
||||||
).encode("ascii")
|
|
||||||
|
|
||||||
|
class BrowserTab(QWidget):
|
||||||
class _ChromeBrandingInterceptor(QWebEngineUrlRequestInterceptor):
|
"""Ein WebView2-Tab mit Medien-Ueberwachung fuer Discord Rich Presence."""
|
||||||
"""Ergaenzt/korrigiert die Sec-CH-UA Client-Hint-Header auf jeder Anfrage, damit
|
|
||||||
sie zum gesetzten User-Agent passen (echtes QtWebEngine meldet dort normalerweise
|
|
||||||
nur "Chromium", ohne die Marke "Google Chrome" - genau daran erkennt Googles
|
|
||||||
Login-Seite ein Embedded-WebView und blockiert die Anmeldung)."""
|
|
||||||
|
|
||||||
def interceptRequest(self, info) -> None: # noqa: N802 (Qt-Override)
|
|
||||||
info.setHttpHeader(b"sec-ch-ua", _SEC_CH_UA)
|
|
||||||
info.setHttpHeader(b"sec-ch-ua-full-version-list", _SEC_CH_UA_FULL_VERSION_LIST)
|
|
||||||
info.setHttpHeader(b"sec-ch-ua-mobile", b"?0")
|
|
||||||
info.setHttpHeader(b"sec-ch-ua-platform", _SEC_CH_UA_PLATFORM)
|
|
||||||
info.setHttpHeader(b"sec-ch-ua-platform-version", _SEC_CH_UA_PLATFORM_VERSION)
|
|
||||||
|
|
||||||
|
|
||||||
_shared_profile: QWebEngineProfile | None = None
|
|
||||||
_shared_interceptor: _ChromeBrandingInterceptor | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_shared_profile() -> QWebEngineProfile:
|
|
||||||
"""Ein gemeinsames, persistentes Profil fuer alle Tabs, damit ein einmaliges
|
|
||||||
Google-Login fuer YouTube und YouTube Music gleichermassen gilt."""
|
|
||||||
global _shared_profile, _shared_interceptor
|
|
||||||
if _shared_profile is None:
|
|
||||||
_shared_profile = QWebEngineProfile("playtube-profile")
|
|
||||||
_shared_profile.setPersistentStoragePath(str(profile_dir() / "storage"))
|
|
||||||
_shared_profile.setCachePath(str(profile_dir() / "cache"))
|
|
||||||
_shared_profile.setPersistentCookiesPolicy(
|
|
||||||
QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies
|
|
||||||
)
|
|
||||||
_shared_profile.setHttpUserAgent(_USER_AGENT)
|
|
||||||
_shared_profile.setHttpAcceptLanguage("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7")
|
|
||||||
|
|
||||||
# Referenz muss gehalten werden, sonst sammelt Python das Objekt vorzeitig ein.
|
|
||||||
_shared_interceptor = _ChromeBrandingInterceptor()
|
|
||||||
_shared_profile.setUrlRequestInterceptor(_shared_interceptor)
|
|
||||||
|
|
||||||
# JS-seitiger "Chrome-Shim" (window.chrome, navigator.userAgentData, Plugins) -
|
|
||||||
# muss vor jedem Seiten-JS laufen, daher DocumentCreation + MainWorld.
|
|
||||||
shim_script = QWebEngineScript()
|
|
||||||
shim_script.setName("playtube-chrome-shim")
|
|
||||||
shim_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
|
|
||||||
shim_script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
|
|
||||||
shim_script.setRunsOnSubFrames(True)
|
|
||||||
shim_script.setSourceCode(CHROME_SHIM_JS)
|
|
||||||
_shared_profile.scripts().insert(shim_script)
|
|
||||||
return _shared_profile
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserTab(QWebEngineView):
|
|
||||||
"""Ein WebEngine-Tab mit Medien-Ueberwachung fuer Discord Rich Presence."""
|
|
||||||
|
|
||||||
mediaInfoChanged = Signal(dict)
|
mediaInfoChanged = Signal(dict)
|
||||||
titleUpdated = Signal(str)
|
titleUpdated = Signal(str)
|
||||||
|
loadFinished = Signal(bool)
|
||||||
|
urlChanged = Signal(QUrl)
|
||||||
|
|
||||||
def __init__(self, home_url: str, parent=None):
|
def __init__(self, home_url: str, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._home_url = home_url
|
self._home_url = home_url
|
||||||
self._audio_output_set = False # war schon ein eigenes Ausgabegeraet gewaehlt? (siehe set_audio_output)
|
self._current_url = QUrl(home_url)
|
||||||
|
self._audio_output_set = False
|
||||||
|
self._allow_device_names = False
|
||||||
|
self._router_script_id: str | None = None
|
||||||
|
self._core = None
|
||||||
|
self._pending: list[tuple[Any, Callable[[Any], None] | None]] = []
|
||||||
|
self._queued_urls: list[str] = [home_url]
|
||||||
|
self._queued_router: str | None = None
|
||||||
|
|
||||||
page = QWebEnginePage(get_shared_profile(), self)
|
# Natives Fenster-Handle, damit das WebView2-Steuerelement als Kindfenster einhaengt.
|
||||||
self.setPage(page)
|
self.setAttribute(Qt.WidgetAttribute.WA_NativeWindow, True)
|
||||||
|
self.setMinimumSize(200, 150)
|
||||||
|
|
||||||
settings = page.settings()
|
runtime = webview2_runtime.load()
|
||||||
settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled, True)
|
self._runtime = runtime
|
||||||
settings.setAttribute(QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture, False)
|
properties = runtime.CreationProperties()
|
||||||
settings.setAttribute(QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)
|
properties.UserDataFolder = str(profile_dir() / "webview2")
|
||||||
settings.setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
|
properties.AdditionalBrowserArguments = _BROWSER_ARGUMENTS
|
||||||
settings.setAttribute(QWebEngineSettings.WebAttribute.ScreenCaptureEnabled, True)
|
properties.Language = "de-DE"
|
||||||
settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
|
self._view = runtime.WebView2()
|
||||||
|
self._view.CreationProperties = properties
|
||||||
|
self._view.CoreWebView2InitializationCompleted += self._on_core_initialized
|
||||||
|
|
||||||
page.fullScreenRequested.connect(self._on_full_screen_requested)
|
self._task_timer = QTimer(self)
|
||||||
|
self._task_timer.setInterval(_TASK_POLL_MS)
|
||||||
self.load(QUrl(home_url))
|
self._task_timer.timeout.connect(self._drain_tasks)
|
||||||
|
self._task_timer.start()
|
||||||
|
|
||||||
self._poll_timer = QTimer(self)
|
self._poll_timer = QTimer(self)
|
||||||
self._poll_timer.setInterval(2000)
|
self._poll_timer.setInterval(_MEDIA_POLL_MS)
|
||||||
self._poll_timer.timeout.connect(self._poll_media_state)
|
self._poll_timer.timeout.connect(self._poll_media_state)
|
||||||
self._poll_timer.start()
|
self._poll_timer.start()
|
||||||
|
|
||||||
self.titleChanged.connect(self.titleUpdated.emit)
|
# Steuerelement erst nach dem Aufbau des Fensters erzeugen.
|
||||||
|
QTimer.singleShot(0, self._create_control)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Aufbau
|
||||||
|
|
||||||
|
def _create_control(self) -> None:
|
||||||
|
self._view.CreateControl()
|
||||||
|
handle = self._handle()
|
||||||
|
# Aus dem frei stehenden WinForms-Fenster ein sichtbares Kindfenster des Qt-Widgets machen.
|
||||||
|
style = _user32.GetWindowLongPtrW(handle, _GWL_STYLE)
|
||||||
|
style = (style & ~_WS_POPUP & ~_WS_CAPTION) | _WS_CHILD | _WS_VISIBLE | _WS_CLIPSIBLINGS | _WS_CLIPCHILDREN
|
||||||
|
_user32.SetWindowLongPtrW(handle, _GWL_STYLE, style)
|
||||||
|
_user32.SetParent(handle, int(self.winId()))
|
||||||
|
_user32.ShowWindow(handle, _SW_SHOW)
|
||||||
|
self._fit_to_widget()
|
||||||
|
self._view.EnsureCoreWebView2Async(None)
|
||||||
|
|
||||||
|
def _handle(self) -> int:
|
||||||
|
return int(self._view.Handle.ToInt64())
|
||||||
|
|
||||||
|
def _fit_to_widget(self) -> None:
|
||||||
|
if _user32 is None or self._view is None or not self._view.IsHandleCreated:
|
||||||
|
return
|
||||||
|
ratio = self.devicePixelRatioF()
|
||||||
|
_user32.MoveWindow(self._handle(), 0, 0, int(self.width() * ratio), int(self.height() * ratio), True)
|
||||||
|
|
||||||
|
def resizeEvent(self, event) -> None: # noqa: N802 (Qt-Override)
|
||||||
|
super().resizeEvent(event)
|
||||||
|
self._fit_to_widget()
|
||||||
|
|
||||||
|
def _on_core_initialized(self, _sender, args) -> None:
|
||||||
|
try:
|
||||||
|
self._setup_core(args)
|
||||||
|
except Exception: # pythonnet verschluckt Fehler in Ereignis-Handlern sonst still
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def _setup_core(self, args) -> None:
|
||||||
|
if not args.IsSuccess:
|
||||||
|
print(f"[webview2] Initialisierung fehlgeschlagen: {args.InitializationException}", flush=True)
|
||||||
|
self.loadFinished.emit(False)
|
||||||
|
return
|
||||||
|
core = self._view.CoreWebView2
|
||||||
|
self._core = core
|
||||||
|
settings = core.Settings
|
||||||
|
settings.IsStatusBarEnabled = False
|
||||||
|
settings.AreDefaultScriptDialogsEnabled = True
|
||||||
|
core.NavigationCompleted += lambda _s, a: self.loadFinished.emit(bool(a.IsSuccess))
|
||||||
|
core.SourceChanged += lambda _s, _a: self._on_source_changed()
|
||||||
|
core.DocumentTitleChanged += lambda _s, _a: self.titleUpdated.emit(str(core.DocumentTitle or ""))
|
||||||
|
core.NewWindowRequested += self._on_new_window
|
||||||
|
core.ContainsFullScreenElementChanged += lambda _s, _a: self._on_fullscreen_changed()
|
||||||
|
|
||||||
|
self._apply_device_name_permission()
|
||||||
|
if self._queued_router is not None:
|
||||||
|
self._install_router(self._queued_router)
|
||||||
|
self._queued_router = None
|
||||||
|
for url in self._queued_urls:
|
||||||
|
core.Navigate(url)
|
||||||
|
self._queued_urls.clear()
|
||||||
|
|
||||||
|
def _on_source_changed(self) -> None:
|
||||||
|
self._current_url = QUrl(str(self._core.Source))
|
||||||
|
self.urlChanged.emit(self._current_url)
|
||||||
|
|
||||||
|
def _on_new_window(self, _sender, args) -> None:
|
||||||
|
# Links mit target=_blank im selben Tab oeffnen (kein zusaetzliches Fenster).
|
||||||
|
args.Handled = True
|
||||||
|
self.setUrl(QUrl(str(args.Uri)))
|
||||||
|
|
||||||
|
def _on_fullscreen_changed(self) -> None:
|
||||||
|
window = self.window()
|
||||||
|
if self._core.ContainsFullScreenElement:
|
||||||
|
window.showFullScreen()
|
||||||
|
else:
|
||||||
|
window.showNormal()
|
||||||
|
self._fit_to_widget()
|
||||||
|
|
||||||
|
# ------------------------------------------------ asynchrone .NET-Aufgaben
|
||||||
|
|
||||||
|
def _await(self, task: Any, callback: Callable[[Any], None] | None = None) -> None:
|
||||||
|
"""Wartet ohne Blockieren auf eine .NET-Task und ruft danach callback(Ergebnis) auf."""
|
||||||
|
self._pending.append((task, callback))
|
||||||
|
|
||||||
|
def _drain_tasks(self) -> None:
|
||||||
|
if not self._pending:
|
||||||
|
return
|
||||||
|
still_pending = []
|
||||||
|
for task, callback in self._pending:
|
||||||
|
if not task.IsCompleted:
|
||||||
|
still_pending.append((task, callback))
|
||||||
|
continue
|
||||||
|
if callback is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
callback(None if task.IsFaulted or task.IsCanceled else task.Result)
|
||||||
|
except Exception: # Fehler im Rueckruf duerfen die Warteschlange nicht blockieren
|
||||||
|
pass
|
||||||
|
self._pending = still_pending
|
||||||
|
|
||||||
|
def _run_js(self, script: str, callback: Callable[[Any], None] | None = None) -> None:
|
||||||
|
if self._core is None:
|
||||||
|
return
|
||||||
|
task = self._core.ExecuteScriptAsync(script)
|
||||||
|
self._await(task, (lambda raw: callback(_decode_script_result(raw))) if callback else None)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Navigation
|
||||||
|
|
||||||
|
def url(self) -> QUrl:
|
||||||
|
return self._current_url
|
||||||
|
|
||||||
|
def setUrl(self, url: QUrl) -> None: # noqa: N802 (Qt-kompatibler Name)
|
||||||
|
target = url.toString()
|
||||||
|
self._current_url = url
|
||||||
|
if self._core is None:
|
||||||
|
self._queued_urls = [target]
|
||||||
|
else:
|
||||||
|
self._core.Navigate(target)
|
||||||
|
|
||||||
def go_home(self) -> None:
|
def go_home(self) -> None:
|
||||||
self.load(QUrl(self._home_url))
|
self.setUrl(QUrl(self._home_url))
|
||||||
|
|
||||||
|
def back(self) -> None:
|
||||||
|
if self._core is not None and self._core.CanGoBack:
|
||||||
|
self._core.GoBack()
|
||||||
|
|
||||||
|
def forward(self) -> None:
|
||||||
|
if self._core is not None and self._core.CanGoForward:
|
||||||
|
self._core.GoForward()
|
||||||
|
|
||||||
|
def reload(self) -> None:
|
||||||
|
if self._core is not None:
|
||||||
|
self._core.Reload()
|
||||||
|
|
||||||
|
# ------------------------------------------------------ Audio-Ausgabe
|
||||||
|
|
||||||
def set_audio_output(self, device_name: str) -> None:
|
def set_audio_output(self, device_name: str) -> None:
|
||||||
"""Legt den Ton dieses Tabs auf das Ausgabegeraet `device_name` (leer =
|
"""Legt den Ton dieses Tabs auf das Ausgabegeraet `device_name` (leer =
|
||||||
Systemstandard), siehe audio_routing.py. Wirkt sofort auf die geoeffnete Seite und
|
Systemstandard), siehe audio_routing.py. Wirkt sofort auf die geoeffnete Seite und
|
||||||
bleibt bei Seitenwechseln/Neuladen erhalten (Skript pro Seite)."""
|
bleibt bei Seitenwechseln erhalten (Skript pro Dokument)."""
|
||||||
page = self.page()
|
|
||||||
scripts = page.scripts()
|
|
||||||
for old in scripts.find(AUDIO_SCRIPT_NAME):
|
|
||||||
scripts.remove(old)
|
|
||||||
|
|
||||||
source = build_router_script(device_name)
|
source = build_router_script(device_name)
|
||||||
if device_name:
|
if self._core is None:
|
||||||
script = QWebEngineScript()
|
self._queued_router = source if device_name else None
|
||||||
script.setName(AUDIO_SCRIPT_NAME)
|
else:
|
||||||
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
self._install_router(source if device_name else None)
|
||||||
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
|
# Bereits geladene Seite sofort umstellen - auch zurueck auf den Systemstandard.
|
||||||
script.setRunsOnSubFrames(False)
|
|
||||||
script.setSourceCode(source)
|
|
||||||
scripts.insert(script)
|
|
||||||
|
|
||||||
# Bereits geladene Seite sofort umstellen - auch zurueck auf den Systemstandard,
|
|
||||||
# falls vorher ein eigenes Geraet gewaehlt war. Ohne jemals gewaehltes Geraet wird die
|
|
||||||
# Seite gar nicht angefasst.
|
|
||||||
if device_name or self._audio_output_set:
|
if device_name or self._audio_output_set:
|
||||||
page.runJavaScript(source)
|
self._run_js(source)
|
||||||
self._audio_output_set = bool(device_name)
|
self._audio_output_set = bool(device_name)
|
||||||
|
|
||||||
|
def _install_router(self, source: str | None) -> None:
|
||||||
|
old_id, self._router_script_id = self._router_script_id, None
|
||||||
|
if old_id:
|
||||||
|
self._core.RemoveScriptToExecuteOnDocumentCreated(old_id)
|
||||||
|
if source:
|
||||||
|
task = self._core.AddScriptToExecuteOnDocumentCreatedAsync(_in_document(source))
|
||||||
|
self._await(task, self._remember_router_id)
|
||||||
|
|
||||||
|
def _remember_router_id(self, script_id: Any) -> None:
|
||||||
|
self._router_script_id = str(script_id) if script_id else None
|
||||||
|
|
||||||
|
def set_device_names_allowed(self, allowed: bool) -> None:
|
||||||
|
"""Erlaubt der Seite, die Namen der Ausgabegeraete zu sehen (siehe audio_routing.py)."""
|
||||||
|
self._allow_device_names = allowed
|
||||||
|
self._apply_device_name_permission()
|
||||||
|
|
||||||
|
def _apply_device_name_permission(self) -> None:
|
||||||
|
if self._core is None:
|
||||||
|
return
|
||||||
|
state = self._runtime.PermissionState.Allow if self._allow_device_names else self._runtime.PermissionState.Default
|
||||||
|
try:
|
||||||
|
for origin in _YOUTUBE_ORIGINS:
|
||||||
|
self._core.Profile.SetPermissionStateAsync(self._runtime.PermissionKind.Microphone, origin, state)
|
||||||
|
except Exception as error: # aeltere Runtime ohne diese API: Seite laedt trotzdem
|
||||||
|
print(f"[webview2] Geraetenamen-Freigabe nicht setzbar: {error}", flush=True)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- Wiedergabe
|
||||||
|
|
||||||
def toggle_playback(self) -> None:
|
def toggle_playback(self) -> None:
|
||||||
self.page().runJavaScript(TOGGLE_PLAYBACK_JS)
|
self._run_js(TOGGLE_PLAYBACK_JS)
|
||||||
|
|
||||||
def next_track(self) -> None:
|
def next_track(self) -> None:
|
||||||
self.page().runJavaScript(NEXT_TRACK_JS)
|
self._run_js(NEXT_TRACK_JS)
|
||||||
|
|
||||||
def previous_track(self) -> None:
|
def previous_track(self) -> None:
|
||||||
self.page().runJavaScript(PREV_TRACK_JS)
|
self._run_js(PREV_TRACK_JS)
|
||||||
|
|
||||||
def run_media_command(self, command: str, value: float = 0) -> None:
|
def run_media_command(self, command: str, value: float = 0) -> None:
|
||||||
"""Fuehrt einen Fernsteuerungsbefehl (siehe media_control.py) aus und liest den
|
"""Fuehrt einen Fernsteuerungsbefehl (siehe media_control.py) aus und liest den
|
||||||
Wiedergabestatus kurz danach neu aus, damit z.B. das Stream Dock den neuen Zustand
|
Wiedergabestatus kurz danach neu aus, damit z.B. das Stream Dock den neuen Zustand
|
||||||
sofort statt erst beim naechsten 2-Sekunden-Poll sieht."""
|
sofort statt erst beim naechsten 2-Sekunden-Poll sieht."""
|
||||||
self.page().runJavaScript(build_control_js(command, value))
|
self._run_js(build_control_js(command, value))
|
||||||
QTimer.singleShot(_REFRESH_AFTER_COMMAND_MS, self._poll_media_state)
|
QTimer.singleShot(_REFRESH_AFTER_COMMAND_MS, self._poll_media_state)
|
||||||
|
|
||||||
def list_playlists(self, callback) -> None:
|
def list_playlists(self, callback) -> None:
|
||||||
"""Ruft callback(list | None) mit den Playlists aus der Seitenleiste auf."""
|
"""Ruft callback(list | None) mit den Playlists aus der Seitenleiste auf."""
|
||||||
|
if self._core is None:
|
||||||
def on_result(result) -> None:
|
|
||||||
try:
|
|
||||||
callback(json.loads(result) if isinstance(result, str) and result else None)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
callback(None)
|
callback(None)
|
||||||
|
return
|
||||||
self.page().runJavaScript(LIST_PLAYLISTS_JS, on_result)
|
self._run_js(LIST_PLAYLISTS_JS, lambda result: callback(result if isinstance(result, list) else None))
|
||||||
|
|
||||||
def play_playlist(self, playlist_id: str) -> None:
|
def play_playlist(self, playlist_id: str) -> None:
|
||||||
"""Oeffnet die Playlist (ID vorher in remote_control validiert); Autoplay startet
|
"""Oeffnet die Playlist (ID vorher in remote_control validiert); Autoplay startet
|
||||||
die Wiedergabe (PlaybackRequiresUserGesture ist aus)."""
|
die Wiedergabe."""
|
||||||
self.load(QUrl(MUSIC_PLAYLIST_URL.format(playlist_id=playlist_id)))
|
self.setUrl(QUrl(MUSIC_PLAYLIST_URL.format(playlist_id=playlist_id)))
|
||||||
|
|
||||||
def _on_full_screen_requested(self, request) -> None:
|
|
||||||
# Erlaubt echtes Fullscreen-Video (z.B. per YouTube-Fullscreen-Button).
|
|
||||||
request.accept()
|
|
||||||
window = self.window()
|
|
||||||
if request.toggleOn():
|
|
||||||
window.showFullScreen()
|
|
||||||
else:
|
|
||||||
window.showNormal()
|
|
||||||
|
|
||||||
def _poll_media_state(self) -> None:
|
def _poll_media_state(self) -> None:
|
||||||
self.page().runJavaScript(MEDIA_PROBE_JS, self._on_media_probe_result)
|
self._run_js(MEDIA_PROBE_JS, self._on_media_probe_result)
|
||||||
|
|
||||||
def _on_media_probe_result(self, result) -> None:
|
|
||||||
# runJavaScript() liefert JS-Objekte ueber diese Bruecke nicht zuverlaessig
|
|
||||||
# als dict (siehe media_probe.py) - das Probe-Skript gibt daher einen
|
|
||||||
# JSON-String zurueck, der hier geparst wird.
|
|
||||||
info = None
|
|
||||||
if isinstance(result, str) and result:
|
|
||||||
try:
|
|
||||||
info = json.loads(result)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
info = None
|
|
||||||
elif isinstance(result, dict):
|
|
||||||
info = result
|
|
||||||
|
|
||||||
|
def _on_media_probe_result(self, info: Any) -> None:
|
||||||
if os.environ.get("PLAYTUBE_DEBUG"):
|
if os.environ.get("PLAYTUBE_DEBUG"):
|
||||||
print(f"[media-probe:{self._home_url}] roh={result!r} geparst={info!r}", flush=True)
|
print(f"[media-probe:{self._home_url}] geparst={info!r}", flush=True)
|
||||||
|
|
||||||
if isinstance(info, dict):
|
if isinstance(info, dict):
|
||||||
self.mediaInfoChanged.emit(info)
|
self.mediaInfoChanged.emit(info)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- Aufraeumen
|
||||||
|
|
||||||
|
def closeEvent(self, event) -> None: # noqa: N802 (Qt-Override)
|
||||||
|
self.dispose()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def dispose(self) -> None:
|
||||||
|
self._task_timer.stop()
|
||||||
|
self._poll_timer.stop()
|
||||||
|
if self._view is not None:
|
||||||
|
self._view.Dispose()
|
||||||
|
self._view = None
|
||||||
|
|||||||
+7
-3
@@ -8,15 +8,18 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
APP_NAME = "Playtube"
|
APP_NAME = "Playtube Edge"
|
||||||
APP_AUMID = "Playtube.DesktopClient" # Windows AppUserModelID
|
APP_AUMID = "Playtube.Edge.DesktopClient" # Windows AppUserModelID
|
||||||
|
|
||||||
# Der Entwicklungsmodus (python main.py) benutzt einen eigenen APPDATA-Ordner
|
# Der Entwicklungsmodus (python main.py) benutzt einen eigenen APPDATA-Ordner
|
||||||
# ("PlaytubeDev" statt "Playtube"), damit Login-Profil und Config sich NIE mit einer
|
# ("PlaytubeDev" statt "Playtube"), damit Login-Profil und Config sich NIE mit einer
|
||||||
# gepackten/installierten Playtube.exe ueberschneiden (frueher fuehrte das dazu, dass
|
# gepackten/installierten Playtube.exe ueberschneiden (frueher fuehrte das dazu, dass
|
||||||
# ein lokaler Test-Build und die echte Installation sich dieselbe config.json bzw.
|
# ein lokaler Test-Build und die echte Installation sich dieselbe config.json bzw.
|
||||||
# denselben Browser-Profil-Lock geteilt haben).
|
# denselben Browser-Profil-Lock geteilt haben).
|
||||||
_DATA_DIR_NAME = APP_NAME if getattr(sys, "frozen", False) else f"{APP_NAME}Dev"
|
#
|
||||||
|
# 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] = {
|
DEFAULT_CONFIG: dict[str, Any] = {
|
||||||
"app_name": APP_NAME,
|
"app_name": APP_NAME,
|
||||||
@@ -28,6 +31,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||||||
# Wenn nichts laeuft: Idle-Status anzeigen statt Presence komplett zu leeren.
|
# Wenn nichts laeuft: Idle-Status anzeigen statt Presence komplett zu leeren.
|
||||||
"show_idle_presence": True,
|
"show_idle_presence": True,
|
||||||
},
|
},
|
||||||
|
# Die Edge-Variante aktualisiert sich nur ueber eigene Releases (Tag "...-edge", siehe updater.py).
|
||||||
"updates": {
|
"updates": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"check_interval_hours": 6,
|
"check_interval_hours": 6,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from PySide6.QtWidgets import (
|
|||||||
|
|
||||||
from . import __version__ as APP_VERSION
|
from . import __version__ as APP_VERSION
|
||||||
from .audio_routing import sync_audio_permissions
|
from .audio_routing import sync_audio_permissions
|
||||||
from .browser import BrowserTab, get_shared_profile
|
from .browser import BrowserTab
|
||||||
from .config import APP_NAME
|
from .config import APP_NAME
|
||||||
from .discord_rpc import DiscordRPCWorker
|
from .discord_rpc import DiscordRPCWorker
|
||||||
from .remote_control import build_state as build_remote_state
|
from .remote_control import build_state as build_remote_state
|
||||||
@@ -57,9 +57,9 @@ class MainWindow(QMainWindow):
|
|||||||
self._tabs.setDocumentMode(True)
|
self._tabs.setDocumentMode(True)
|
||||||
self.setCentralWidget(self._tabs)
|
self.setCentralWidget(self._tabs)
|
||||||
|
|
||||||
self._sync_audio_permission()
|
|
||||||
self._youtube_tab = BrowserTab(config["home_youtube"], self)
|
self._youtube_tab = BrowserTab(config["home_youtube"], self)
|
||||||
self._music_tab = BrowserTab(config["home_music"], self)
|
self._music_tab = BrowserTab(config["home_music"], self)
|
||||||
|
self._sync_audio_permission()
|
||||||
# Zusaetzlich nach JEDEM Seitenaufbau erneut erteilen: eine nur VOR dem Laden erteilte
|
# Zusaetzlich nach JEDEM Seitenaufbau erneut erteilen: eine nur VOR dem Laden erteilte
|
||||||
# Freigabe greift auf der YouTube-Startseite nicht (per Test ermittelt), eine danach
|
# Freigabe greift auf der YouTube-Startseite nicht (per Test ermittelt), eine danach
|
||||||
# erteilte schon (siehe audio_routing.py).
|
# erteilte schon (siehe audio_routing.py).
|
||||||
@@ -254,7 +254,7 @@ class MainWindow(QMainWindow):
|
|||||||
def _sync_audio_permission(self) -> None:
|
def _sync_audio_permission(self) -> None:
|
||||||
"""Erteilt bzw. entzieht die Freigabe der Geraetenamen (siehe audio_routing.py) -
|
"""Erteilt bzw. entzieht die Freigabe der Geraetenamen (siehe audio_routing.py) -
|
||||||
nur solange mindestens ein Tab ein eigenes Geraet nutzt."""
|
nur solange mindestens ein Tab ein eigenes Geraet nutzt."""
|
||||||
sync_audio_permissions(get_shared_profile(), any(self._audio_outputs()))
|
sync_audio_permissions((self._youtube_tab, self._music_tab), any(self._audio_outputs()))
|
||||||
|
|
||||||
def _on_tab_load_finished(self, _ok: bool) -> None:
|
def _on_tab_load_finished(self, _ok: bool) -> None:
|
||||||
if any(self._audio_outputs()):
|
if any(self._audio_outputs()):
|
||||||
@@ -392,7 +392,7 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_update_check_failed(self) -> None:
|
def _on_update_check_failed(self) -> None:
|
||||||
self._settings_tab.set_check_done()
|
self._settings_tab.set_check_done()
|
||||||
self._settings_tab.set_update_status(
|
self._settings_tab.set_update_status(
|
||||||
"Update-Prüfung fehlgeschlagen (keine Internetverbindung oder GitHub nicht erreichbar)."
|
"Update-Prüfung fehlgeschlagen (keine Internetverbindung oder Update-Server nicht erreichbar)."
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_update_available(self, version: str, notes: str, download_url: str) -> None:
|
def _on_update_available(self, version: str, notes: str, download_url: str) -> None:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from PySide6.QtWidgets import (
|
|||||||
from . import __version__ as APP_VERSION
|
from . import __version__ as APP_VERSION
|
||||||
from .audio_routing import SYSTEM_DEFAULT_LABEL, list_output_devices
|
from .audio_routing import SYSTEM_DEFAULT_LABEL, list_output_devices
|
||||||
from .config import APP_NAME, save_config
|
from .config import APP_NAME, save_config
|
||||||
from .updater import GITHUB_REPO
|
from .updater import REPO_URL
|
||||||
|
|
||||||
# Helle Schrift fuer Versionsnummer, Update-Status und GitHub-Link. palette(text) ist die
|
# Helle Schrift fuer Versionsnummer, Update-Status und GitHub-Link. palette(text) ist die
|
||||||
# Standard-Textfarbe (im dunklen Design weiss, im hellen dunkel) - das fruehere
|
# Standard-Textfarbe (im dunklen Design weiss, im hellen dunkel) - das fruehere
|
||||||
@@ -180,7 +180,7 @@ class SettingsTab(QWidget):
|
|||||||
button_row.addStretch(1)
|
button_row.addStretch(1)
|
||||||
outer.addLayout(button_row)
|
outer.addLayout(button_row)
|
||||||
|
|
||||||
info = QLabel(f"github.com/{GITHUB_REPO}")
|
info = QLabel(REPO_URL.removeprefix("https://"))
|
||||||
info.setStyleSheet(_LIGHT_TEXT_STYLE)
|
info.setStyleSheet(_LIGHT_TEXT_STYLE)
|
||||||
outer.addWidget(info)
|
outer.addWidget(info)
|
||||||
|
|
||||||
|
|||||||
+30
-12
@@ -1,7 +1,7 @@
|
|||||||
"""Auto-Update ueber GitHub Releases (https://github.com/fojadrachi/Playtube).
|
"""Auto-Update ueber die Releases des eigenen Gitea-Servers (siehe UPDATE_SERVER).
|
||||||
|
|
||||||
Ablauf:
|
Ablauf:
|
||||||
1. UpdateChecker prueft im Hintergrund die GitHub-Releases-API auf eine neuere
|
1. UpdateChecker prueft im Hintergrund die Gitea-Releases-API auf eine neuere
|
||||||
Version als die aktuell laufende (playtube.__version__).
|
Version als die aktuell laufende (playtube.__version__).
|
||||||
2. Bei Fund fragt die UI (siehe mainwindow.py) nach Bestaetigung.
|
2. Bei Fund fragt die UI (siehe mainwindow.py) nach Bestaetigung.
|
||||||
3. UpdateInstaller installiert das Update:
|
3. UpdateInstaller installiert das Update:
|
||||||
@@ -53,17 +53,24 @@ from PySide6.QtCore import QThread, Signal
|
|||||||
|
|
||||||
from . import __version__ as CURRENT_VERSION
|
from . import __version__ as CURRENT_VERSION
|
||||||
|
|
||||||
GITHUB_REPO = "fojadrachi/Playtube"
|
# Eigener Gitea-Server (kein GitHub). Die Releases dort werden von .gitea/workflows/release.yml
|
||||||
_API_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
|
# bzw. packaging/publish_release.ps1 veroeffentlicht.
|
||||||
_USER_AGENT = "Playtube-Updater"
|
UPDATE_SERVER = "https://git.fojadrachi.de"
|
||||||
|
REPO_SLUG = "Fojadrachi/Playtube"
|
||||||
|
REPO_URL = f"{UPDATE_SERVER}/{REPO_SLUG}"
|
||||||
|
# Edge-Variante: eigene Releases mit Tag "vX.Y.Z-edge" (als Vorabversion markiert, damit die
|
||||||
|
# Qt-Version sie nie als "neueste Version" sieht) - deshalb die Liste statt /releases/latest.
|
||||||
|
_API_URL = f"{UPDATE_SERVER}/api/v1/repos/{REPO_SLUG}/releases?limit=30"
|
||||||
|
_EDGE_TAG_SUFFIX = "-edge"
|
||||||
|
_USER_AGENT = "PlaytubeEdge-Updater"
|
||||||
|
|
||||||
_LINUX_BINARY_NAME = "Playtube"
|
_LINUX_BINARY_NAME = "PlaytubeEdge"
|
||||||
_WINDOWS_BINARY_NAME = "Playtube.exe"
|
_WINDOWS_BINARY_NAME = "PlaytubeEdge.exe"
|
||||||
|
|
||||||
# Muss mit "AppId" in packaging/playtube.iss uebereinstimmen. Unter dieser ID legt Inno
|
# Muss mit "AppId" in packaging/playtube.iss uebereinstimmen. Unter dieser ID legt Inno
|
||||||
# Setup den Deinstallations-Eintrag ("Apps & Features") an - nach einem Patch-Update
|
# Setup den Deinstallations-Eintrag ("Apps & Features") an - nach einem Patch-Update
|
||||||
# wird dort die angezeigte Versionsnummer nachgezogen (siehe _install_patch_windows).
|
# wird dort die angezeigte Versionsnummer nachgezogen (siehe _install_patch_windows).
|
||||||
_INNO_APP_ID = "{87FBA502-2B84-4C42-A66B-2F03F490912D}"
|
_INNO_APP_ID = "{4F0F3698-DAF3-4028-B0A4-896D31292C28}"
|
||||||
|
|
||||||
_STAGING_PREFIX = "playtube_update_"
|
_STAGING_PREFIX = "playtube_update_"
|
||||||
|
|
||||||
@@ -100,14 +107,21 @@ def cleanup_old_staging() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def fetch_latest_release() -> dict[str, Any] | None:
|
def fetch_latest_release() -> dict[str, Any] | None:
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(_API_URL, headers={"User-Agent": _USER_AGENT, "Accept": "application/json"})
|
||||||
_API_URL, headers={"User-Agent": _USER_AGENT, "Accept": "application/vnd.github+json"}
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
releases = json.loads(resp.read().decode("utf-8"))
|
||||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError, ValueError):
|
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
if not isinstance(releases, list):
|
||||||
|
return None
|
||||||
|
# Neueste Edge-Version = hoechste Versionsnummer unter den "-edge"-Releases (ohne Entwuerfe).
|
||||||
|
edge_releases = [
|
||||||
|
release for release in releases
|
||||||
|
if isinstance(release, dict) and not release.get("draft")
|
||||||
|
and str(release.get("tag_name", "")).endswith(_EDGE_TAG_SUFFIX)
|
||||||
|
]
|
||||||
|
return max(edge_releases, key=lambda r: _parse_version(str(r.get("tag_name", ""))), default=None)
|
||||||
|
|
||||||
|
|
||||||
def is_installed_via_setup() -> bool:
|
def is_installed_via_setup() -> bool:
|
||||||
@@ -125,6 +139,10 @@ def _asset_kind(name: str) -> str | None:
|
|||||||
*.play (unsere eigene Dateiendung, technisch ein ganz normales .zip), Voll -> *.zip.
|
*.play (unsere eigene Dateiendung, technisch ein ganz normales .zip), Voll -> *.zip.
|
||||||
Linux: *.tar.gz, "patch" im Namen kennzeichnet das Patch-Paket."""
|
Linux: *.tar.gz, "patch" im Namen kennzeichnet das Patch-Paket."""
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
|
# Zweite Absicherung neben dem "-edge"-Tag: nur Dateien der Edge-Variante kommen in Frage,
|
||||||
|
# nie ein Paket der Qt-Version ("Playtube-...").
|
||||||
|
if "playtubeedge" not in name:
|
||||||
|
return None
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
if name.endswith(".exe") and "setup" in name:
|
if name.endswith(".exe") and "setup" in name:
|
||||||
return _KIND_SETUP
|
return _KIND_SETUP
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""Laedt Microsoft Edge WebView2 (per pythonnet / .NET Framework) fuer den Browser-Tab.
|
||||||
|
|
||||||
|
Die DLLs (Microsoft.Web.WebView2.Core/WinForms, WebView2Loader) liegen im Projektordner
|
||||||
|
vendor/webview2 (offizielles NuGet-Paket "Microsoft.Web.WebView2"). Die eigentliche
|
||||||
|
Browser-Engine ist die auf Windows 10/11 vorinstallierte "WebView2 Runtime" (Edge) - sie
|
||||||
|
bringt AAC/H.264 mit, die in QtWebEngine fehlen.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
|
class WebView2Unavailable(RuntimeError):
|
||||||
|
"""WebView2 konnte nicht geladen werden (DLLs oder Runtime fehlen)."""
|
||||||
|
|
||||||
|
|
||||||
|
def _app_base_dir() -> Path:
|
||||||
|
return Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
|
||||||
|
def _vendor_dir() -> Path:
|
||||||
|
return _app_base_dir() / "vendor" / "webview2"
|
||||||
|
|
||||||
|
|
||||||
|
def _unblock_downloaded_files(folders: tuple[Path, ...]) -> None:
|
||||||
|
"""Entfernt die "Mark of the Web" (Zone.Identifier) von den DLLs, die .NET laden soll.
|
||||||
|
|
||||||
|
Wer die ZIP aus dem Internet laedt und entpackt, bekommt alle Dateien mit dieser
|
||||||
|
Markierung; .NET verweigert dann das Laden von Python.Runtime.dll ("Failed to resolve
|
||||||
|
Python.Runtime.Loader.Initialize"). Die Dateien liegen im eigenen App-Ordner - das
|
||||||
|
Entfernen der Markierung betrifft nur sie."""
|
||||||
|
if os.name != "nt":
|
||||||
|
return
|
||||||
|
for folder in folders:
|
||||||
|
for path in folder.rglob("*"):
|
||||||
|
if path.suffix.lower() in (".dll", ".exe"):
|
||||||
|
try:
|
||||||
|
os.remove(f"{path}:Zone.Identifier")
|
||||||
|
except OSError:
|
||||||
|
pass # keine Markierung vorhanden (Normalfall) oder Dateisystem ohne Datenstroeme
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def load() -> SimpleNamespace:
|
||||||
|
"""Laedt .NET + WebView2 einmalig und gibt die benoetigten Klassen zurueck."""
|
||||||
|
vendor = _vendor_dir()
|
||||||
|
core_dll = vendor / "Microsoft.Web.WebView2.Core.dll"
|
||||||
|
winforms_dll = vendor / "Microsoft.Web.WebView2.WinForms.dll"
|
||||||
|
if not core_dll.exists() or not winforms_dll.exists():
|
||||||
|
raise WebView2Unavailable(f"WebView2-DLLs fehlen in {vendor}")
|
||||||
|
|
||||||
|
base = _app_base_dir()
|
||||||
|
_unblock_downloaded_files((vendor, base / "pythonnet", base / "clr_loader"))
|
||||||
|
|
||||||
|
# WebView2Loader.dll (nativ) wird ueber den Suchpfad bzw. runtimes/win-x64/native gefunden.
|
||||||
|
os.environ["PATH"] = f"{vendor}{os.pathsep}{os.environ.get('PATH', '')}"
|
||||||
|
try:
|
||||||
|
from pythonnet import load as load_runtime
|
||||||
|
|
||||||
|
load_runtime("netfx")
|
||||||
|
import clr
|
||||||
|
|
||||||
|
clr.AddReference("System.Windows.Forms")
|
||||||
|
clr.AddReference("System.Drawing")
|
||||||
|
clr.AddReference(str(core_dll))
|
||||||
|
clr.AddReference(str(winforms_dll))
|
||||||
|
|
||||||
|
from Microsoft.Web.WebView2.Core import (
|
||||||
|
CoreWebView2PermissionKind,
|
||||||
|
CoreWebView2PermissionState,
|
||||||
|
)
|
||||||
|
from Microsoft.Web.WebView2.WinForms import CoreWebView2CreationProperties, WebView2
|
||||||
|
except Exception as error: # pythonnet wirft je nach Fehler unterschiedliche Typen
|
||||||
|
raise WebView2Unavailable(f"WebView2 konnte nicht geladen werden: {error}") from error
|
||||||
|
|
||||||
|
return SimpleNamespace(
|
||||||
|
WebView2=WebView2,
|
||||||
|
CreationProperties=CoreWebView2CreationProperties,
|
||||||
|
PermissionKind=CoreWebView2PermissionKind,
|
||||||
|
PermissionState=CoreWebView2PermissionState,
|
||||||
|
)
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
PySide6>=6.10
|
PySide6>=6.10
|
||||||
pypresence>=4.3
|
pypresence>=4.3
|
||||||
|
pythonnet>=3.1
|
||||||
|
|||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (C) Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* The name of Microsoft Corporation, or the names of its contributors
|
||||||
|
may not be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
BIN
Binary file not shown.
Binary file not shown.
Vendored
+85
@@ -0,0 +1,85 @@
|
|||||||
|
NOTICES AND INFORMATION
|
||||||
|
Do Not Translate or Localize
|
||||||
|
|
||||||
|
This software incorporates material from third parties. Microsoft makes certain
|
||||||
|
open source code available at https://3rdpartysource.microsoft.com, or you may
|
||||||
|
send a check or money order for US $5.00, including the product name, the open
|
||||||
|
source component name, and version number, to:
|
||||||
|
|
||||||
|
Source Code Compliance Team
|
||||||
|
Microsoft Corporation
|
||||||
|
One Microsoft Way
|
||||||
|
Redmond, WA 98052
|
||||||
|
USA
|
||||||
|
|
||||||
|
Notwithstanding any other terms, you may reverse engineer this software to the
|
||||||
|
extent required to debug changes to any libraries licensed under the GNU Lesser
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
Antlr3.Runtime 3.5.2-rc1 - BSD 3-Clause
|
||||||
|
|
||||||
|
[The "BSD license"]
|
||||||
|
Copyright (c) 2011 The ANTLR Project
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||||
|
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||||
|
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
StringTemplate4 4.0.9-rc1 - BSD 3-Clause
|
||||||
|
|
||||||
|
[The "BSD license"]
|
||||||
|
Copyright (c) 2011 The ANTLR Project
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||||
|
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||||
|
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
---------------------------------------------------------
|
||||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user