From 2cd5e2fb0c0e0a7a9dd1b1ed2f01ad4da7087595 Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:05:24 +0200 Subject: [PATCH] =?UTF-8?q?Phase=205=20abgeschlossen=20-=20SQLite-Setup:?= =?UTF-8?q?=20`src/main/store/db.ts`=20(better-sqlite3,=20Migration)=20-?= =?UTF-8?q?=20Lokaler=20VOD-Verlauf=20(Resume)=20-=20mpv=20`time-pos`-Obse?= =?UTF-8?q?rver=20schreibt=20alle=20~5s=20in=20DB=20-=20Auto-Resume:=20bei?= =?UTF-8?q?m=20VOD-Start=20Position=20aus=20DB=20lesen=20=E2=86=92=20`--st?= =?UTF-8?q?art=3D`=20=20-Resume-Indikator=20auf=20VOD-Karte=20=20-=20Co?= =?UTF-8?q?mpleted-Indikator=20(dunkles=20Overlay=20+=20Checkmark)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE_PROGRESS.md | 159 +-- package-lock.json | 1309 ++++++++++++++++---- package.json | 3 + plan.md | 90 +- src/main/ipc/handlers.ts | 9 +- src/main/playback/mpvController.ts | 49 +- src/main/playback/playbackService.ts | 152 +-- src/main/playback/streamlink.ts | 63 +- src/main/store/db.ts | 30 + src/main/store/historyRepo.ts | 80 ++ src/preload/index.ts | 15 +- src/renderer/src/screens/ChannelScreen.tsx | 112 +- src/renderer/src/styles/global.css | 35 + src/renderer/src/types/t4sd.d.ts | 11 +- todo.md | 16 +- 15 files changed, 1602 insertions(+), 531 deletions(-) create mode 100644 src/main/store/db.ts create mode 100644 src/main/store/historyRepo.ts diff --git a/CLAUDE_PROGRESS.md b/CLAUDE_PROGRESS.md index a5f7d7d..fa4b7a9 100644 --- a/CLAUDE_PROGRESS.md +++ b/CLAUDE_PROGRESS.md @@ -1,145 +1,90 @@ # Twitch4SteamDeck — Fortschritts-Snapshot -> Stand: **Ende 2026-04-07 (Session 2)**. Diese Datei dient als Übergabe zwischen Sessions. +> Stand: **2026-04-08 (Session 3)**. Diese Datei dient als Übergabe zwischen Sessions. > **Lies zuerst:** diese Datei + `todo.md` + `plan.md`. -> Architekturplan-Quelle: `C:\Users\andre\.claude\plans\smooth-shimmying-parasol.md` (im Repo gespiegelt). -> -> **Aktueller Stand:** Phase 0–2 abgeschlossen & verifiziert. Phase 3 Code-complete, noch NICHT verifiziert (mpv PATH-Problem beim ersten Start). + +**Aktueller Stand:** Phase 0–5 abgeschlossen & verifiziert. Als nächstes: Phase 6 (Flatpak-Packaging). --- ## Bereits abgeschlossen ### ✅ Phase 0 — Projekt-Skelett -Siehe vorherige Sessions. - ### ✅ Phase 1 — Twitch OAuth Device Code Flow -Siehe vorherige Sessions. End-to-End verifiziert 2026-04-07. - ### ✅ Phase 2 — Twitch Helix-Client + Sidebar-Layout -Siehe vorherige Sessions. End-to-End verifiziert 2026-04-07. +Alle drei End-to-End verifiziert 2026-04-07. --- -### 🔄 Phase 3 — Streamlink + mpv (Live-Wiedergabe) — Code complete, nicht verifiziert +### ✅ Phase 3 — Streamlink + mpv (Live-Wiedergabe) -#### Neu erstellte Dateien (Session 2, 2026-04-07): +Verifiziert 2026-04-08. -**Main-Prozess:** -- `src/main/playback/types.ts` — `PlaybackEvent`, `Quality` -- `src/main/playback/streamlink.ts` — `spawnStreamlink()` mit `resolveStreamlinkBin()` (sucht - `.exe` in `%LOCALAPPDATA%\Programs\Streamlink\bin\` usw., Fallback `streamlink`) -- `src/main/playback/mpvController.ts` — `getMpvIpcPath()`, `MpvController` (Named Pipe Win32 - / Unix Socket), connect mit Retry, `quit()`, `disconnect()` -- `src/main/playback/playbackService.ts` — orchestriert streamlink + mpv, `startLive()`, `stop()`, - `stopAll()`, emittiert `'playback-event'` -- `src/main/ipc/handlers.ts` — erweitert um `playback:start-live`, `playback:stop`, - `playback:event` (main→renderer); `registerIpcHandlers` nimmt jetzt `PlaybackService` als 3. Arg -- `src/main/index.ts` — instanziiert `PlaybackService`, registriert - `app.on('before-quit', () => playback.stopAll())` - -**Preload + Typen:** -- `src/preload/index.ts` — `window.t4sd.playback.{startLive, stop, onEvent}` hinzugefügt -- `src/renderer/src/types/t4sd.d.ts` — `PlaybackEvent`-Typ + `playback`-API hinzugefügt - -**Renderer:** -- `src/renderer/src/screens/ChannelScreen.tsx` — Detail-Screen: Thumbnail/Avatar/Name/Titel/Spiel/ - Viewer, Button „▶ Live ansehen", Zustandsanzeige (idle/starting/playing/error), - Escape/B → Stop oder Zurück -- `src/renderer/src/screens/AppShell.tsx` — `selectedChannel`-State, Routing zu `ChannelScreen`, - `onSelectChannel` an `FollowingScreen` weitergegeben -- `src/renderer/src/screens/FollowingScreen.tsx` — `onSelectChannel`-Prop verkabelt, - `onSelect` der Karte ruft jetzt `onSelectChannel(ch)` auf (nicht mehr nur `console.log`) -- `src/renderer/src/styles/global.css` — `.channel-screen` + alle Unter-Klassen ergänzt - -#### Architektur-Entscheidungen in Phase 3: -- **Variante 2:** streamlink spawnt mpv selbst via `--player mpv --player-args "..."`. - Node steuert nur den streamlink-Prozess direkt. -- **mpv IPC** via Named Pipe (Win32) oder Unix Socket (Linux), verbunden nach `spawn` im Hintergrund - (nicht blockierend, nicht kritisch für MVP). Wird für `quit()` beim Stop genutzt. -- **`resolveStreamlinkBin()`** sucht die `.exe` unter bekannten Windows-Installationspfaden, +#### Architektur-Entscheidungen: +- `--twitch-api-header` wird **nicht** übergeben — Twitch lehnt den Device-Code-Token für + streamlinks interne GQL-API ab. Öffentliche Streams funktionieren ohne Auth-Header. +- `resolveMpvBin()` löst den mpv-Pfad explizit auf (`C:\Program Files\MPV Player\mpv.exe`), da Electron den User-PATH nicht vollständig erbt. +- `--twitch-disable-ads` ist von streamlink deaktiviert worden (deprecated) — kein Ad-Bypass + im Moment. Post-MVP-Problem. --- -## Offener Punkt beim Start morgen — mpv PATH-Problem +### ✅ Phase 4 — VOD-Browsing + -Wiedergabe -### Was passiert ist: -Streamlink startet (ENOENT behoben via `resolveStreamlinkBin()`), aber streamlink -kann mpv nicht finden, weil **mpv nicht im PATH ist, den streamlink von Electron erbt**. +Verifiziert 2026-04-08. -Fehlerbild: `[streamlink] exit code 1`, kein mpv-Fenster. - -### Wahrscheinlichste Lösung (zuerst probieren): -**VS Code (und damit das Electron-Dev-Fenster) neu starten.** Nach dem `setx PATH ...` -für mpv muss VS Code neu gestartet werden, damit es den neuen PATH erbt. - -``` -1. VS Code schließen -2. VS Code neu öffnen -3. npm run dev -4. Kanal auswählen → "Live ansehen" -``` - -### Falls das nicht reicht — Fallback: -mpv-Pfad ebenfalls resolven wie streamlink. In `streamlink.ts` eine `resolveMpvBin()`-Funktion -ergänzen und den vollen Pfad an `--player` übergeben: - -```typescript -// In streamlink.ts hinzufügen: -function resolveMpvBin(): string { - if (process.platform === 'win32') { - const candidates = [ - join(process.env.LOCALAPPDATA ?? '', 'Programs', 'mpv', 'mpv.exe'), - // shinchiro winget installiert nach: %LOCALAPPDATA%\Microsoft\WinGet\Packages\... - // Dort nach mpv.exe suchen wenn oben nicht gefunden - ] - for (const p of candidates) { - if (existsSync(p)) return p - } - } - return 'mpv' -} -``` - -Dann in `spawnStreamlink()`: -```typescript -'--player', resolveMpvBin(), // statt '--player', 'mpv' -``` - -**Wichtig:** `where mpv` in der Shell ausführen um den exakten Pfad zu kennen, bevor -du `resolveMpvBin()` implementierst. - -### Falls auch das nicht reicht: -streamlink selbst via `--player-args` testen ohne `--input-ipc-server` (vereinfachter Test), -um zu isolieren ob das Problem mpv-start oder IPC-Argument ist. +#### Architektur-Entscheidungen: +- **VODs:** streamlink `--stream-url` liefert HLS-URL → mpv wird **direkt** mit der URL + gespawnt (nicht via `--player`-Flag). Grund: Piped-Streams sind nicht seekable. + Direktes mpv-Spawnen ermöglicht Seeking + funktionierendes IPC. +- **Live-Streams:** weiterhin via `--player mpv --player-args "--fullscreen"`. Kein IPC + nötig (kein Seeking, kein Position-Tracking bei Live). +- VOD-Shelf im `ChannelScreen` (untere Hälfte, horizontal, oberer Hero-Bereich fix). + Navigation: ↓ → Shelf, ←/→ zwischen VODs, ↑ → zurück, Enter → abspielen. --- -## Verifikations-Checkliste Phase 3 (noch offen) +### ✅ Phase 5 — Lokaler VOD-Verlauf (Resume) -- [ ] 5 min Live-Stream ohne Werbe-Unterbrechung auf Windows (Dev-Setup) -- [ ] Beim Stop: mpv-Fenster schließt sich sauber, streamlink-Prozess terminiert -- [ ] Keine Zombie-Prozesse im Task-Manager nach Stop -- [ ] Logout/Login und erneutes Starten funktioniert ohne App-Neustart +Verifiziert 2026-04-08. + +#### Neue Dateien: +- `src/main/store/db.ts` — SQLite via better-sqlite3, WAL-Modus, Migration +- `src/main/store/historyRepo.ts` — `upsertVod`, `updatePosition`, `markCompleted`, + `getPosition`, `getProgressMap` + +#### Architektur-Entscheidungen: +- mpv IPC funktioniert jetzt zuverlässig (da mpv direkt gespawnt wird). + `MpvController.observeTimePos()` parst eingehende JSON-Lines, Throttling auf 5s. +- Resume-Position wird beim `startVod()`-Aufruf automatisch aus DB gelesen. +- Auto-Complete bei >95% Fortschritt. +- `„Continue Watching"-Reihe` wurde bewusst weggelassen (vom User nicht gewünscht). +- VOD-Karte zeigt: Fortschrittsbalken (lila), `0:16 von 6:22`, `Vor X Min.`, + Completed-Overlay mit Kreis-Checkmark. --- -## Erste Anweisung morgen an Claude (copy-paste) +## Nächster Schritt: Phase 6 — Flatpak-Packaging -> „Lies CLAUDE_PROGRESS.md, todo.md und plan.md. Phase 3 ist code-complete. Das mpv -> PATH-Problem war der letzte offene Punkt — ich habe VS Code neugestartet (oder den -> Fallback umgesetzt). Sag mir ob der Stream jetzt läuft." +Ziel: App läuft auf dem Steam Deck als Flatpak. + +Wichtige offene Punkte: +- mpv + streamlink müssen als Flatpak-Module gebündelt werden +- `--hwdec=vaapi` für Linux (Steam Deck) statt `auto` (Windows) +- IPC-Pfad: Unix Socket `/tmp/twitch4sd-mpv.sock` (bereits implementiert für non-win32) +- better-sqlite3 muss für Linux/ARM64 (Steam Deck) cross-compiliert werden +- Flatpak-Manifest: `flatpak/tv.twitch4steamdeck.App.yml` --- -## Technische Baseline (Sanity-Check) +## Technische Baseline - Electron 33, electron-vite, TypeScript 5.5, React 18 -- streamlink 8.2.1 installiert unter `%LOCALAPPDATA%\Programs\Streamlink\bin\streamlink.exe` -- mpv v0.41.0 installiert, via `setx` zum PATH hinzugefügt (VS Code-Neustart nötig!) -- IPC-Pattern: `ipcMain.handle` → `ipcRenderer.invoke` → `contextBridge` → `window.t4sd.*` -- Main→Renderer Events via `webContents.send` + `ipcRenderer.on` +- better-sqlite3 12.x (native, rebuilt für Electron via `@electron/rebuild`) +- streamlink 8.x unter `%LOCALAPPDATA%\Programs\Streamlink\bin\streamlink.exe` +- mpv v0.41.0 unter `C:\Program Files\MPV Player\mpv.exe` +- Dev-Host: **Windows**. Target: **Steam Deck (Linux / Flatpak)** ### Wichtiges Projekt-Memo - Bei **allen Architekturänderungen** `plan.md` updaten. diff --git a/package-lock.json b/package-lock.json index 8e558ca..c08356b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "better-sqlite3": "^12.8.0", "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@electron/rebuild": "^4.0.3", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20.14.0", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.3", @@ -541,60 +544,77 @@ } }, "node_modules/@electron/rebuild": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", - "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", "got": "^11.7.0", - "node-abi": "^3.45.0", - "node-api-version": "^0.2.0", - "node-gyp": "^9.0.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", - "tar": "^6.0.5", + "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" + "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/@electron/rebuild/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/rebuild/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@electron/rebuild/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/@electron/rebuild/node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", "dev": true, "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "semver": "^7.6.3" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=22.12.0" } }, "node_modules/@electron/rebuild/node_modules/semver": { @@ -610,14 +630,31 @@ "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=18" + } + }, + "node_modules/@electron/rebuild/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/@electron/universal": { @@ -1211,6 +1248,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1339,18 +1399,41 @@ "node": ">= 10.0.0" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "license": "ISC", "dependencies": { - "@gar/promisify": "^1.1.3", "semver": "^7.3.5" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/fs/node_modules/semver": { @@ -1830,6 +1913,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -2022,11 +2115,14 @@ "license": "MIT" }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/agent-base": { "version": "7.1.4", @@ -2171,6 +2267,150 @@ "electron-builder-squirrel-windows": "25.1.8" } }, + "node_modules/app-builder-lib/node_modules/@electron/rebuild": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", + "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/app-builder-lib/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/app-builder-lib/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2186,6 +2426,35 @@ "node": ">=12" } }, + "node_modules/app-builder-lib/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/app-builder-lib/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/app-builder-lib/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -2199,6 +2468,143 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/app-builder-lib/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-lib/node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/app-builder-lib/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/app-builder-lib/node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/app-builder-lib/node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/app-builder-lib/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -2212,6 +2618,60 @@ "node": ">=10" } }, + "node_modules/app-builder-lib/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/app-builder-lib/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/app-builder-lib/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2400,7 +2860,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -2430,11 +2889,33 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -2519,7 +3000,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -2645,33 +3125,27 @@ } }, "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/cacache/node_modules/balanced-match": { @@ -2691,48 +3165,122 @@ "balanced-match": "^1.0.0" } }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=12" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } + "license": "ISC" }, "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/cacheable-lookup": { @@ -3225,7 +3773,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -3241,7 +3788,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3250,6 +3796,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -3332,7 +3887,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3794,7 +4348,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -3937,6 +4490,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -4000,6 +4562,30 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -4104,9 +4690,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "8.1.0", @@ -4263,6 +4847,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4613,7 +5203,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -4673,7 +5262,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/ip-address": { @@ -5063,83 +5657,36 @@ } }, "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "license": "ISC", "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" + "ssri": "^12.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/make-fetch-happen/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" } }, "node_modules/matcher": { @@ -5242,7 +5789,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5262,36 +5808,69 @@ } }, "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">= 8" + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.1.6", + "minipass": "^7.0.3", "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "minizlib": "^3.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" }, "optionalDependencies": { "encoding": "^0.1.13" } }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/minipass-flush": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", @@ -5372,6 +5951,12 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5398,10 +5983,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -5412,7 +6003,6 @@ "version": "3.89.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -5425,7 +6015,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5466,29 +6055,71 @@ } }, "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^12.13 || ^14.13 || >=16" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" } }, "node_modules/node-gyp/node_modules/semver": { @@ -5504,6 +6135,49 @@ "node": ">=10" } }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -5512,19 +6186,19 @@ "license": "MIT" }, "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^1.0.0" + "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/normalize-path": { @@ -5583,7 +6257,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5683,16 +6356,13 @@ } }, "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5806,6 +6476,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -5859,6 +6542,43 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5902,7 +6622,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -6015,6 +6734,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -6067,7 +6801,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -6284,7 +7017,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -6408,6 +7140,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -6477,31 +7254,18 @@ } }, "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/source-map": { @@ -6544,16 +7308,26 @@ "optional": true }, "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "license": "ISC", "dependencies": { - "minipass": "^3.1.1" + "minipass": "^7.0.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/stat-mode": { @@ -6570,7 +7344,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -6632,6 +7405,15 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -6677,13 +7459,29 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -6761,6 +7559,23 @@ "node": ">= 10.0.0" } }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -6791,6 +7606,18 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -6827,29 +7654,29 @@ "license": "MIT" }, "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "license": "ISC", "dependencies": { - "unique-slug": "^3.0.0" + "unique-slug": "^5.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/universalify": { @@ -6914,7 +7741,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/verror": { @@ -7076,7 +7902,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xmlbuilder": { diff --git a/package.json b/package.json index bd2e956..f93e896 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,14 @@ "package": "electron-vite build && electron-builder --linux" }, "dependencies": { + "better-sqlite3": "^12.8.0", "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@electron/rebuild": "^4.0.3", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20.14.0", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.3", diff --git a/plan.md b/plan.md index d1194bf..6555e68 100644 --- a/plan.md +++ b/plan.md @@ -151,60 +151,66 @@ Header: `Client-Id: `, `Authorization: Bearer `. `getFollowedWithLiveStatus()` merged Followed + Live + Avatare zu `FollowedChannelInfo[]` und sortiert live-zuerst, dann alphabetisch. -### Streamlink + mpv (⏳ Phase 3) +### Streamlink + mpv (✅ Phase 3 + 4) -**Kommandos (Linux / Flatpak-Target):** +**Zwei unterschiedliche Spawn-Strategien (implementiert):** + +**Live-Streams** (`spawnStreamlink`): ``` -streamlink \ - --twitch-disable-ads \ - --twitch-api-header=Authorization=OAuth \ - --stdout twitch.tv/ best - | mpv - \ - --input-ipc-server=/tmp/twitch4sd-mpv.sock \ - --fullscreen --hwdec=vaapi +streamlink --player --player-args "--fullscreen" twitch.tv/ best ``` +- streamlink spawnt mpv als Kind-Prozess, kein IPC +- `--twitch-disable-ads` wurde von streamlink deaktiviert (deprecated) — kein Ad-Bypass +- `--twitch-api-header=Authorization=OAuth` wird **nicht** übergeben: Twitch lehnt + Device-Code-Flow-Token für streamlinks interne GQL-API ab -**Windows-Entwicklung (Ist-Umgebung):** -- mpv: `winget install mpv` oder Binary von mpv.io ins PATH -- streamlink: `winget install streamlink` oder `pip install streamlink` -- IPC-Socket: Named Pipe `\\.\pipe\twitch4sd-mpv` (nicht `/tmp/...sock`) -- `--hwdec=vaapi` → auf Windows `--hwdec=d3d11va` oder `auto` -- Pipe stdout → stdin funktioniert auch unter Windows (Node `spawn` mit stdio pipe) +**VODs** (`getStreamUrl` + `spawnMpv`): +``` +# Schritt 1: HLS-URL abrufen +streamlink --stream-url https://www.twitch.tv/videos/ best +# → gibt z.B. https://…/playlist.m3u8 zurück -**Architektur-Entscheidung für `mpvController`:** -- `process.platform === 'win32'` → Named Pipe path, sonst Unix Socket -- Nach `spawn` kurz warten (100–200 ms) bis der IPC-Endpoint bereit ist, dann `net.createConnection` -- JSON-IPC: pro Zeile ein Command `{"command": ["set_property", "pause", true]}\n` -- Property-Observer für `time-pos` → Events in Main-Prozess → IPC zum Renderer (für Phase 5 Verlauf) +# Schritt 2: mpv direkt mit HLS-URL starten +mpv --input-ipc-server= --fullscreen --hwdec= --start= +``` +- mpv wird direkt gespawnt (nicht via streamlink) → HLS-URL ist seekable → Seeking/Fortschrittsbalken funktioniert +- IPC funktioniert zuverlässig (direkter mpv-Prozess, kein Pfad-Escaping durch streamlink) +- `--hwdec=auto` auf Windows, `--hwdec=vaapi` auf Linux/Steam Deck -**Spezielle Streamlink-Flags zu prüfen:** -- `--twitch-low-latency` (reduziert Buffer, kann aber stottern verursachen) -- `--twitch-disable-hosting` -- TTV.LOL-Plugin: `pip install streamlink-ttvlol` — Fallback falls `--twitch-disable-ads` allein - keine Werbe-Umgehung mehr leistet. +**IPC-Pfade:** +- Windows: Named Pipe `\\.\pipe\twitch4sd-mpv` +- Linux/Flatpak: Unix Socket `/tmp/twitch4sd-mpv.sock` -**IPC zwischen Renderer und Main (Phase 3):** -- `playback:start-live` → `{channelLogin, quality}` → startet Prozess-Kette, gibt `playbackId` zurück -- `playback:stop` → sendet `quit` an mpv, killt streamlink -- `playback:event` (main → renderer) → `{kind: 'started' | 'stopped' | 'error', message?}` +**mpvController** (`src/main/playback/mpvController.ts`): +- `connect(retries, delayMs)` — verbindet nach Spawn +- `observeTimePos(cb)` — registriert `observe_property 1 time-pos`, parst JSON-Lines +- `quit()` — sendet `{"command":["quit"]}` via IPC +- Wird bei Live-Streams instanziiert aber nie verbunden (harmlos) -### VOD-History-DB (⏳ Phase 5) +**IPC zwischen Renderer und Main:** +- `playback:start-live` → `(channelLogin, quality?)` → `PlaybackService.startLive()` +- `playback:start-vod` → `(vodId, channelLogin, title, durationSeconds)` → `PlaybackService.startVod()` +- `playback:stop` → `PlaybackService.stop()` +- `playback:event` (main → renderer) → `{kind: 'started'|'stopped'|'error', message?}` + +### VOD-History-DB (✅ Phase 5) ```sql CREATE TABLE vod_history ( - vod_id TEXT PRIMARY KEY, - channel_login TEXT NOT NULL, - channel_display TEXT, - title TEXT, - thumbnail_url TEXT, - duration_seconds INTEGER, - resume_position_seconds INTEGER NOT NULL DEFAULT 0, - watched_at INTEGER NOT NULL, - completed INTEGER NOT NULL DEFAULT 0 + vod_id TEXT PRIMARY KEY, + channel_login TEXT NOT NULL, + title TEXT, + duration_seconds INTEGER, + resume_position_seconds INTEGER NOT NULL DEFAULT 0, + watched_at INTEGER NOT NULL, + completed INTEGER NOT NULL DEFAULT 0 ); -CREATE INDEX idx_history_watched ON vod_history(watched_at DESC); +CREATE INDEX IF NOT EXISTS idx_history_watched ON vod_history(watched_at DESC); ``` -„Continue Watching": `SELECT … WHERE completed=0 ORDER BY watched_at DESC LIMIT 20`. -Complete-Threshold: `resume_position / duration > 0.95`. +- DB-Pfad: `app.getPath('userData')/history.db`, WAL-Modus +- Complete-Threshold: `resume_position / duration > 0.95` +- `„Continue Watching"-Reihe` bewusst weggelassen (nicht benötigt) +- IPC: `history:get-progress` → `(vodIds: string[]) → Record` +- VOD-Karte zeigt: Fortschrittsbalken, `0:16 von 6:22`, `Vor X Min.`, Completed-Overlay ### Big-Screen-UI / Gamepad - Spatial-Navigation-Lib oder eigene Mini-Implementierung. diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index e757329..a4327e7 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -3,6 +3,7 @@ import type { AuthEvent, AuthService } from '../auth/authService' import type { HelixClient } from '../twitch/helixClient' import type { PlaybackService } from '../playback/playbackService' import type { PlaybackEvent } from '../playback/types' +import { getProgressMap } from '../store/historyRepo' export const IPC = { authStatus: 'auth:get-status', @@ -15,6 +16,7 @@ export const IPC = { twitchGetFollowed: 'twitch:get-followed', twitchGetVideos: 'twitch:get-videos', + historyGetProgress: 'history:get-progress', playbackStartLive: 'playback:start-live', playbackStartVod: 'playback:start-vod', @@ -47,6 +49,10 @@ export function registerIpcHandlers( IPC.twitchGetVideos, (_e, broadcasterId: string) => helix.getVideos(broadcasterId) ) + ipcMain.handle( + IPC.historyGetProgress, + (_e, vodIds: string[]) => getProgressMap(vodIds) + ) ipcMain.handle( IPC.playbackStartLive, @@ -54,7 +60,8 @@ export function registerIpcHandlers( ) ipcMain.handle( IPC.playbackStartVod, - (_e, vodId: string, startSeconds?: number) => playback.startVod(vodId, startSeconds) + (_e, vodId: string, channelLogin: string, title: string, durationSeconds: number) => + playback.startVod(vodId, channelLogin, title, durationSeconds) ) ipcMain.handle(IPC.playbackStop, () => playback.stop()) diff --git a/src/main/playback/mpvController.ts b/src/main/playback/mpvController.ts index c562d39..0b359de 100644 --- a/src/main/playback/mpvController.ts +++ b/src/main/playback/mpvController.ts @@ -11,6 +11,7 @@ export function getMpvIpcPath(): string { export class MpvController { private socket: net.Socket | null = null + private buf = '' constructor(private ipcPath: string) {} @@ -33,22 +34,51 @@ export class MpvController { const sock = net.createConnection({ path: this.ipcPath }) sock.once('connect', () => { this.socket = sock - sock.on('error', () => { - /* ignore socket errors after connect */ - }) + sock.on('error', () => { /* ignore socket errors after connect */ }) resolve() }) sock.once('error', reject) }) } + /** Beobachtet time-pos; ruft cb mit Sekunden auf. Throttling liegt beim Aufrufer. */ + observeTimePos(cb: (seconds: number) => void): void { + if (!this.socket?.writable) return + + // Observer registrieren + this._send({ command: ['observe_property', 1, 'time-pos'] }) + + this.socket.on('data', (chunk: Buffer) => { + this.buf += chunk.toString() + const lines = this.buf.split('\n') + this.buf = lines.pop() ?? '' + for (const line of lines) { + if (!line.trim()) continue + try { + const msg = JSON.parse(line) as { + event?: string + name?: string + data?: unknown + } + if ( + msg.event === 'property-change' && + msg.name === 'time-pos' && + typeof msg.data === 'number' + ) { + cb(msg.data) + } + } catch { + /* ignore malformed lines */ + } + } + }) + } + quit(): void { if (this.socket?.writable) { try { - this.socket.write(JSON.stringify({ command: ['quit'] }) + '\n') - } catch { - /* ignore */ - } + this._send({ command: ['quit'] }) + } catch { /* ignore */ } } this.socket?.destroy() this.socket = null @@ -57,5 +87,10 @@ export class MpvController { disconnect(): void { this.socket?.destroy() this.socket = null + this.buf = '' + } + + private _send(msg: object): void { + this.socket?.write(JSON.stringify(msg) + '\n') } } diff --git a/src/main/playback/playbackService.ts b/src/main/playback/playbackService.ts index 9b5e2c3..e6274b7 100644 --- a/src/main/playback/playbackService.ts +++ b/src/main/playback/playbackService.ts @@ -1,11 +1,14 @@ import { EventEmitter } from 'node:events' import type { ChildProcess } from 'node:child_process' -import { spawnStreamlink } from './streamlink' +import { spawnStreamlink, getStreamUrl, spawnMpv } from './streamlink' import { getMpvIpcPath, MpvController } from './mpvController' import type { PlaybackEvent } from './types' +import * as history from '../store/historyRepo' + +const POSITION_WRITE_INTERVAL_MS = 5_000 + interface CurrentPlayback { - channelLogin: string - streamlink: ChildProcess + process: ChildProcess mpv: MpvController } @@ -18,117 +21,116 @@ export class PlaybackService extends EventEmitter { } async startLive(channelLogin: string, quality = 'best'): Promise { - if (this.current) { - await this.stop() - } + if (this.current) await this.stop() - const sl = spawnStreamlink({ - url: `twitch.tv/${channelLogin}`, - quality, - mpvIpcPath: this.ipcPath - }) + const proc = spawnStreamlink(`twitch.tv/${channelLogin}`, quality) + this._attachLogs(proc) - sl.stdout?.on('data', (d: Buffer) => { - console.log('[streamlink stdout]', d.toString().trim()) - }) - - sl.stderr?.on('data', (d: Buffer) => { - console.log('[streamlink stderr]', d.toString().trim()) - }) - - sl.on('error', (e: Error) => { - console.error('[streamlink] spawn error:', e) - if (this.current?.streamlink === sl) this.current = null + proc.on('error', (e: Error) => { + if (this.current?.process === proc) this.current = null this.emit('playback-event', { kind: 'error', message: `streamlink Fehler: ${e.message}` } satisfies PlaybackEvent) }) - sl.on('exit', (code) => { - if (this.current?.streamlink === sl) this.current = null - if (code !== 0 && code !== null) { - console.warn('[streamlink] exit code', code) - } + proc.on('exit', (code) => { + if (this.current?.process === proc) this.current = null + if (code !== 0 && code !== null) console.warn('[streamlink] exit code', code) this.emit('playback-event', { kind: 'stopped' } satisfies PlaybackEvent) }) + // Für Live: mpv wird von streamlink gestartet, kein direkter IPC nötig const mpv = new MpvController(this.ipcPath) - this.current = { channelLogin, streamlink: sl, mpv } - - this.emit('playback-event', { - kind: 'started', - channelLogin - } satisfies PlaybackEvent) - - // IPC-Verbindung zu mpv im Hintergrund (nicht blockierend, nicht kritisch) - mpv.connect().catch(() => {}) + this.current = { process: proc, mpv } + this.emit('playback-event', { kind: 'started', channelLogin } satisfies PlaybackEvent) } - async startVod(vodId: string, startSeconds = 0): Promise { - if (this.current) { - await this.stop() - } + async startVod( + vodId: string, + channelLogin: string, + title: string, + durationSeconds: number + ): Promise { + if (this.current) await this.stop() - const sl = spawnStreamlink({ - url: `https://www.twitch.tv/videos/${vodId}`, - quality: 'best', - mpvIpcPath: this.ipcPath, - startSeconds + const resumePos = history.getPosition(vodId) + history.upsertVod({ + vod_id: vodId, + channel_login: channelLogin, + title, + duration_seconds: durationSeconds, + watched_at: Date.now() }) - sl.stdout?.on('data', (d: Buffer) => { - console.log('[streamlink stdout]', d.toString().trim()) - }) - - sl.stderr?.on('data', (d: Buffer) => { - console.log('[streamlink stderr]', d.toString().trim()) - }) - - sl.on('error', (e: Error) => { - console.error('[streamlink] spawn error:', e) - if (this.current?.streamlink === sl) this.current = null + let hlsUrl: string + try { + hlsUrl = await getStreamUrl(`https://www.twitch.tv/videos/${vodId}`, 'best') + } catch (e) { this.emit('playback-event', { kind: 'error', - message: `streamlink Fehler: ${e.message}` + message: `Stream-URL konnte nicht abgerufen werden: ${e}` + } satisfies PlaybackEvent) + return + } + + const proc = spawnMpv(hlsUrl, { ipcPath: this.ipcPath, startSeconds: resumePos }) + this._attachLogs(proc) + + proc.on('error', (e: Error) => { + if (this.current?.process === proc) this.current = null + this.emit('playback-event', { + kind: 'error', + message: `mpv Fehler: ${e.message}` } satisfies PlaybackEvent) }) - sl.on('exit', (code) => { - if (this.current?.streamlink === sl) this.current = null - if (code !== 0 && code !== null) { - console.warn('[streamlink] exit code', code) - } + proc.on('exit', (code) => { + if (this.current?.process === proc) this.current = null + if (code !== 0 && code !== null) console.warn('[mpv] exit code', code) this.emit('playback-event', { kind: 'stopped' } satisfies PlaybackEvent) }) const mpv = new MpvController(this.ipcPath) - this.current = { channelLogin: vodId, streamlink: sl, mpv } - + this.current = { process: proc, mpv } this.emit('playback-event', { kind: 'started' } satisfies PlaybackEvent) - mpv.connect().catch(() => {}) + + // IPC verbinden und Position tracken + mpv.connect().then(() => { + let lastWrite = 0 + mpv.observeTimePos((seconds) => { + const now = Date.now() + if (now - lastWrite < POSITION_WRITE_INTERVAL_MS) return + lastWrite = now + const pos = Math.floor(seconds) + history.updatePosition(vodId, pos) + if (durationSeconds > 0 && pos / durationSeconds > 0.95) { + history.markCompleted(vodId) + } + }) + }).catch(() => { + console.warn('[mpv] IPC nicht verfügbar — Resume-Tracking deaktiviert') + }) } async stop(): Promise { if (!this.current) return - const { streamlink: sl, mpv } = this.current + const { process: proc, mpv } = this.current this.current = null - try { - mpv.quit() - } catch { - /* ignore */ - } + try { mpv.quit() } catch { /* ignore */ } mpv.disconnect() await new Promise((r) => setTimeout(r, 400)) - - if (!sl.killed) { - sl.kill() - } + if (!proc.killed) proc.kill() } stopAll(): void { this.stop().catch(() => {}) } + + private _attachLogs(proc: ChildProcess): void { + proc.stdout?.on('data', (d: Buffer) => console.log('[player stdout]', d.toString().trim())) + proc.stderr?.on('data', (d: Buffer) => console.log('[player stderr]', d.toString().trim())) + } } diff --git a/src/main/playback/streamlink.ts b/src/main/playback/streamlink.ts index 7b5fc95..67cebb8 100644 --- a/src/main/playback/streamlink.ts +++ b/src/main/playback/streamlink.ts @@ -33,33 +33,52 @@ function resolveMpvBin(): string { const STREAMLINK_BIN = resolveStreamlinkBin() const MPV_BIN = resolveMpvBin() -export interface StreamlinkOptions { - url: string - quality: string - mpvIpcPath: string +/** Startet streamlink so dass es mpv als Player verwendet (für Live-Streams). */ +export function spawnStreamlink(url: string, quality: string): ChildProcess { + return spawn(STREAMLINK_BIN, ['--player', MPV_BIN, '--player-args', '--fullscreen', url, quality], { + stdio: ['ignore', 'pipe', 'pipe'] + }) +} + +/** + * Fragt streamlink nach der direkten HLS-URL für einen VOD. + * mpv kann diese URL dann nativ laden und darin seeked. + */ +export function getStreamUrl(url: string, quality: string): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(STREAMLINK_BIN, ['--stream-url', url, quality], { + stdio: ['ignore', 'pipe', 'pipe'] + }) + let out = '' + proc.stdout?.on('data', (d: Buffer) => { out += d.toString() }) + proc.on('error', reject) + proc.on('exit', (code) => { + const hlsUrl = out.trim() + if (code === 0 && hlsUrl) { + resolve(hlsUrl) + } else { + reject(new Error(`streamlink --stream-url exit ${code}`)) + } + }) + }) +} + +export interface MpvOptions { + ipcPath: string startSeconds?: number } -export function spawnStreamlink({ - url, - quality, - mpvIpcPath, - startSeconds -}: StreamlinkOptions): ChildProcess { +/** Startet mpv direkt mit einer URL (HLS oder lokal). IPC-Socket wird gesetzt. */ +export function spawnMpv(url: string, { ipcPath, startSeconds }: MpvOptions): ChildProcess { const hwdec = process.platform === 'win32' ? 'auto' : 'vaapi' - let playerArgs = `--input-ipc-server=${mpvIpcPath} --fullscreen --hwdec=${hwdec}` - if (startSeconds && startSeconds > 0) { - playerArgs += ` --start=${startSeconds}` - } - const args = [ - '--player', - MPV_BIN, - '--player-args', - playerArgs, url, - quality + `--input-ipc-server=${ipcPath}`, + '--fullscreen', + `--hwdec=${hwdec}` ] - - return spawn(STREAMLINK_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] }) + if (startSeconds && startSeconds > 0) { + args.push(`--start=${startSeconds}`) + } + return spawn(MPV_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] }) } diff --git a/src/main/store/db.ts b/src/main/store/db.ts new file mode 100644 index 0000000..f2d3503 --- /dev/null +++ b/src/main/store/db.ts @@ -0,0 +1,30 @@ +import Database from 'better-sqlite3' +import { app } from 'electron' +import { join } from 'node:path' + +let _db: Database.Database | null = null + +export function getDb(): Database.Database { + if (!_db) { + const dbPath = join(app.getPath('userData'), 'history.db') + _db = new Database(dbPath) + _db.pragma('journal_mode = WAL') + migrate(_db) + } + return _db +} + +function migrate(db: Database.Database): void { + db.exec(` + CREATE TABLE IF NOT EXISTS vod_history ( + vod_id TEXT PRIMARY KEY, + channel_login TEXT NOT NULL, + title TEXT, + duration_seconds INTEGER, + resume_position_seconds INTEGER NOT NULL DEFAULT 0, + watched_at INTEGER NOT NULL, + completed INTEGER NOT NULL DEFAULT 0 + ); + CREATE INDEX IF NOT EXISTS idx_history_watched ON vod_history(watched_at DESC); + `) +} diff --git a/src/main/store/historyRepo.ts b/src/main/store/historyRepo.ts new file mode 100644 index 0000000..021ceab --- /dev/null +++ b/src/main/store/historyRepo.ts @@ -0,0 +1,80 @@ +import { getDb } from './db' + +export interface VodHistoryEntry { + vod_id: string + channel_login: string + title: string | null + duration_seconds: number | null + resume_position_seconds: number + watched_at: number + completed: number +} + +export function upsertVod(entry: Omit): void { + const db = getDb() + db.prepare(` + INSERT INTO vod_history (vod_id, channel_login, title, duration_seconds, watched_at) + VALUES (@vod_id, @channel_login, @title, @duration_seconds, @watched_at) + ON CONFLICT(vod_id) DO UPDATE SET + watched_at = excluded.watched_at, + title = excluded.title, + duration_seconds = excluded.duration_seconds + `).run(entry) +} + +export function updatePosition(vodId: string, positionSeconds: number): void { + getDb() + .prepare( + `UPDATE vod_history SET resume_position_seconds = ? WHERE vod_id = ?` + ) + .run(positionSeconds, vodId) +} + +export function markCompleted(vodId: string): void { + getDb() + .prepare(`UPDATE vod_history SET completed = 1 WHERE vod_id = ?`) + .run(vodId) +} + +export interface VodProgress { + resumePositionSeconds: number + watchedAt: number + completed: boolean +} + +/** Gibt für alle übergebenen vodIds vorhandene History-Einträge zurück. */ +export function getProgressMap(vodIds: string[]): Record { + if (vodIds.length === 0) return {} + const placeholders = vodIds.map(() => '?').join(',') + const rows = getDb() + .prepare( + `SELECT vod_id, resume_position_seconds, watched_at, completed + FROM vod_history WHERE vod_id IN (${placeholders})` + ) + .all(...vodIds) as { + vod_id: string + resume_position_seconds: number + watched_at: number + completed: number + }[] + const map: Record = {} + for (const r of rows) { + map[r.vod_id] = { + resumePositionSeconds: r.resume_position_seconds, + watchedAt: r.watched_at, + completed: r.completed === 1 + } + } + return map +} + +export function getPosition(vodId: string): number { + const row = getDb() + .prepare( + `SELECT resume_position_seconds, completed FROM vod_history WHERE vod_id = ?` + ) + .get(vodId) as { resume_position_seconds: number; completed: number } | undefined + // Completed VODs: start from beginning + if (!row || row.completed) return 0 + return row.resume_position_seconds +} diff --git a/src/preload/index.ts b/src/preload/index.ts index db885b5..96e270d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -10,6 +10,7 @@ const IPC = { twitchGetFollowed: 'twitch:get-followed', twitchGetVideos: 'twitch:get-videos', + historyGetProgress: 'history:get-progress', playbackStartLive: 'playback:start-live', playbackStartVod: 'playback:start-vod', @@ -60,6 +61,12 @@ export interface VodInfo { thumbnailUrl: string } +export interface VodProgress { + resumePositionSeconds: number + watchedAt: number + completed: boolean +} + const api = { appVersion: process.env.npm_package_version ?? '0.0.0', auth: { @@ -80,11 +87,15 @@ const api = { getVideos: (broadcasterId: string): Promise => ipcRenderer.invoke(IPC.twitchGetVideos, broadcasterId) }, + history: { + getProgress: (vodIds: string[]): Promise> => + ipcRenderer.invoke(IPC.historyGetProgress, vodIds) + }, playback: { startLive: (channelLogin: string, quality?: string): Promise => ipcRenderer.invoke(IPC.playbackStartLive, channelLogin, quality), - startVod: (vodId: string, startSeconds?: number): Promise => - ipcRenderer.invoke(IPC.playbackStartVod, vodId, startSeconds), + startVod: (vodId: string, channelLogin: string, title: string, durationSeconds: number): Promise => + ipcRenderer.invoke(IPC.playbackStartVod, vodId, channelLogin, title, durationSeconds), stop: (): Promise => ipcRenderer.invoke(IPC.playbackStop), onEvent: (cb: (event: PlaybackEvent) => void): (() => void) => { const listener = (_e: IpcRendererEvent, event: PlaybackEvent): void => cb(event) diff --git a/src/renderer/src/screens/ChannelScreen.tsx b/src/renderer/src/screens/ChannelScreen.tsx index d0dd789..e1cb734 100644 --- a/src/renderer/src/screens/ChannelScreen.tsx +++ b/src/renderer/src/screens/ChannelScreen.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import type { FollowedChannelInfo, PlaybackEvent, VodInfo } from '../types/t4sd' +import type { FollowedChannelInfo, PlaybackEvent, VodInfo, VodProgress } from '../types/t4sd' interface Props { channel: FollowedChannelInfo @@ -21,9 +21,7 @@ function formatViewers(n: number): string { function formatDuration(s: number): string { const h = Math.floor(s / 3600) const m = Math.floor((s % 3600) / 60) - const sec = s % 60 - if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` - return `${m}:${String(sec).padStart(2, '0')}` + return `${h}:${String(m).padStart(2, '0')}` } function formatDate(iso: string): string { @@ -34,6 +32,19 @@ function formatDate(iso: string): string { }) } +function formatWatchedAt(ms: number): string { + const diff = Date.now() - ms + const mins = Math.floor(diff / 60_000) + const hours = Math.floor(diff / 3_600_000) + const days = Math.floor(diff / 86_400_000) + if (mins < 2) return 'Gerade eben' + if (mins < 60) return `Vor ${mins} Min.` + if (hours < 24) return `Vor ${hours} Std.` + if (days === 1) return 'Gestern' + if (days < 7) return `Vor ${days} Tagen` + return new Date(ms).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) +} + function resolveThumbnail(url: string | undefined, w = 1280, h = 720): string | undefined { return url?.replace('{width}', String(w)).replace('{height}', String(h)) } @@ -43,6 +54,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { const [errorMsg, setErrorMsg] = useState('') const [vods, setVods] = useState([]) const [vodsLoading, setVodsLoading] = useState(true) + const [progressMap, setProgressMap] = useState>({}) const [focusRegion, setFocusRegion] = useState('hero') const [shelfIndex, setShelfIndex] = useState(0) const watchBtnRef = useRef(null) @@ -50,7 +62,11 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { useEffect(() => { window.t4sd.twitch .getVideos(channel.broadcasterId) - .then(setVods) + .then((loaded) => { + setVods(loaded) + const ids = loaded.map((v) => v.id) + return window.t4sd.history.getProgress(ids).then(setProgressMap) + }) .catch(() => {}) .finally(() => setVodsLoading(false)) }, [channel.broadcasterId]) @@ -106,13 +122,20 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { setPlayState('playing') } else if (ev.kind === 'stopped') { setPlayState('idle') + // Progress nach Stop neu laden + if (vods.length > 0) { + void window.t4sd.history + .getProgress(vods.map((v) => v.id)) + .then(setProgressMap) + .catch(() => {}) + } } else if (ev.kind === 'error') { setPlayState('error') setErrorMsg(ev.message ?? 'Unbekannter Fehler') } }) return unsub - }, []) + }, [vods]) useEffect(() => { watchBtnRef.current?.focus() @@ -133,7 +156,12 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { setPlayState('starting') setErrorMsg('') try { - await window.t4sd.playback.startVod(vod.id) + await window.t4sd.playback.startVod( + vod.id, + channel.broadcasterLogin, + vod.title, + vod.durationSeconds + ) } catch (e) { setPlayState('error') setErrorMsg(String(e)) @@ -252,26 +280,58 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { className="channel-screen__shelf-track" style={{ transform: `translateX(-${trackOffset}px)` }} > - {vods.map((vod, i) => ( -
void handleWatchVod(vod)} - > -
- {vod.thumbnailUrl && !vod.thumbnailUrl.includes('404_processing') ? ( - - ) : ( -
- )} - - {formatDuration(vod.durationSeconds)} - + {vods.map((vod, i) => { + const prog = progressMap[vod.id] + const pct = + prog && !prog.completed && vod.durationSeconds > 0 + ? Math.min(100, (prog.resumePositionSeconds / vod.durationSeconds) * 100) + : 0 + return ( +
void handleWatchVod(vod)} + > +
+ {vod.thumbnailUrl && !vod.thumbnailUrl.includes('404_processing') ? ( + + ) : ( +
+ )} + + {prog && !prog.completed && prog.resumePositionSeconds > 0 + ? `${formatDuration(prog.resumePositionSeconds)} von ${formatDuration(vod.durationSeconds)}` + : formatDuration(vod.durationSeconds)} + + {pct > 0 && ( +
+
+
+ )} + {prog?.completed && ( +
+ + + + +
+ )} +
+

{vod.title}

+

+ {formatDate(vod.createdAt)} + {prog && !prog.completed && ( + + {' · '}{formatWatchedAt(prog.watchedAt)} + + )} +

-

{vod.title}

-

{formatDate(vod.createdAt)}

-
- ))} + ) + })}
)} diff --git a/src/renderer/src/styles/global.css b/src/renderer/src/styles/global.css index b8db0be..b18d284 100644 --- a/src/renderer/src/styles/global.css +++ b/src/renderer/src/styles/global.css @@ -691,6 +691,21 @@ body, border-radius: 4px; } +.channel-screen__vod-progress { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background: rgba(255, 255, 255, 0.2); +} + +.channel-screen__vod-progress-bar { + height: 100%; + background: var(--color-accent); + border-radius: 0 2px 2px 0; +} + .channel-screen__vod-title { margin: 0; font-size: 15px; @@ -708,6 +723,26 @@ body, color: var(--color-fg-dim); } +.channel-screen__vod-watched { + color: var(--color-accent); +} + +.channel-screen__vod-completed { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.55); + display: flex; + align-items: center; + justify-content: center; +} + +.channel-screen__vod-completed svg { + width: 48px; + height: 48px; + color: #fff; + filter: drop-shadow(0 2px 6px rgba(0,0,0,0.6)); +} + /* ── Account Screen ───────────────────────────────── */ .account { flex: 1; diff --git a/src/renderer/src/types/t4sd.d.ts b/src/renderer/src/types/t4sd.d.ts index 3f1ea67..568f225 100644 --- a/src/renderer/src/types/t4sd.d.ts +++ b/src/renderer/src/types/t4sd.d.ts @@ -43,6 +43,12 @@ export interface VodInfo { thumbnailUrl: string } +export interface VodProgress { + resumePositionSeconds: number + watchedAt: number + completed: boolean +} + export interface T4sdApi { appVersion: string auth: { @@ -57,9 +63,12 @@ export interface T4sdApi { getFollowed: () => Promise getVideos: (broadcasterId: string) => Promise } + history: { + getProgress: (vodIds: string[]) => Promise> + } playback: { startLive: (channelLogin: string, quality?: string) => Promise - startVod: (vodId: string, startSeconds?: number) => Promise + startVod: (vodId: string, channelLogin: string, title: string, durationSeconds: number) => Promise stop: () => Promise onEvent: (cb: (event: PlaybackEvent) => void) => () => void } diff --git a/todo.md b/todo.md index 3a6c56d..3c19583 100644 --- a/todo.md +++ b/todo.md @@ -79,12 +79,16 @@ - [x] mpv `--start=` beim Spawn (Parameter vorbereitet für Phase 5) - [x] **User:** VOD-Wiedergabe + Stop-Verhalten verifiziert (2026-04-08) -## Phase 5 — Lokaler VOD-Verlauf -- [ ] SQLite-Setup: `src/main/store/db.ts` (better-sqlite3, Migrations) -- [ ] `historyRepo.ts` mit `vod_history`-Tabelle (siehe plan.md) -- [ ] mpv `time-pos`-Observer schreibt alle ~5s in DB -- [ ] „Continue Watching"-Reihe auf HomeScreen -- [ ] Auto-Complete bei >95% Wiedergabefortschritt +## Phase 5 — Lokaler VOD-Verlauf (Resume) +- [x] SQLite-Setup: `src/main/store/db.ts` (better-sqlite3, Migration) +- [x] `historyRepo.ts` mit `vod_history`-Tabelle +- [x] mpv `time-pos`-Observer schreibt alle ~5s in DB +- [x] Auto-Resume: beim VOD-Start Position aus DB lesen → `--start=` +- [x] Auto-Complete bei >95% Wiedergabefortschritt +- [x] Resume-Indikator auf VOD-Karte (Fortschrittsbalken + „0:16 von 6:22" + „Vor X Min.") +- [x] Completed-Indikator (dunkles Overlay + Checkmark) +- [x] **User:** Verifiziert (2026-04-08) +- ~~„Continue Watching"-Reihe~~ — entfällt (nicht benötigt) ## Phase 6 — Flatpak-Packaging - [ ] `flatpak/tv.twitch4steamdeck.App.yml` Manifest (Runtime, Module, Permissions)