Compare commits
No commits in common. "935125a83ed2b4dd65e4afe93a18d26f5d5ab123" and "d04779d0ac641a6f392fb19c6746611efd4a37b3" have entirely different histories.
935125a83e
...
d04779d0ac
72
.claude/settings.local.json
Normal file
72
.claude/settings.local.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(ren \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch-YouTube\\\\Twitch\\\\Twitch_VOD_Manager_V_3.2.1.pyw\" \"Twitch_VOD_Manager_V_3.2.2.pyw\")",
|
||||||
|
"Bash(cmd /c ren:*)",
|
||||||
|
"Bash(npm install)",
|
||||||
|
"Bash(npm run build:win:*)",
|
||||||
|
"Bash(npm run build:portable:*)",
|
||||||
|
"Bash(npx electron-builder --win portable --config.win.signAndEditExecutable=false)",
|
||||||
|
"Bash(dir \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch-YouTube\\\\Twitch\\\\TwitchVODManager-Electron\\\\dist\")",
|
||||||
|
"Bash(python -m py_compile:*)",
|
||||||
|
"Bash(dir \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\*.exe\")",
|
||||||
|
"Bash(C:UsersploetAppDataRoamingPythonPython311Scriptspyinstaller.exe:*)",
|
||||||
|
"Bash(python -m PyInstaller:*)",
|
||||||
|
"Bash(dir \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\dist\\\\Twitch_VOD_Manager.exe\")",
|
||||||
|
"Bash(powershell -command:*)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(start \"\" \"Twitch_VOD_Manager.exe\")",
|
||||||
|
"Bash(tasklist:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(pip install:*)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(ping:*)",
|
||||||
|
"Bash(move:*)",
|
||||||
|
"Bash(start Twitch_VOD_Manager.exe)",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe\" \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\installer.iss\")",
|
||||||
|
"Bash(\"/c/Users/ploet/AppData/Local/Programs/Inno Setup 6/ISCC.exe\" \"C:/Users/ploet/Desktop/Twitch Downloader/installer.iss\")",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch_VOD_Manager_Setup_test.exe\")",
|
||||||
|
"Bash(\"/c/Program Files/Twitch VOD Manager/unins000.exe\" /VERYSILENT /SUPPRESSMSGBOXES)",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch_VOD_Manager_Setup_NEW.exe\")",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.8.exe\")",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.10.exe\")",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.12.exe\")",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.14.exe\")",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.16.exe\")",
|
||||||
|
"Bash(start \"\" \"/c/Users/ploet/Desktop/Twitch Downloader/installer_output/Twitch_VOD_Manager_Setup_3.4.18.exe\")",
|
||||||
|
"Bash(\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe\" installer.iss)",
|
||||||
|
"Bash(\"C:/Program Files \\(x86\\)/Inno Setup 6/ISCC.exe\" \"C:/Users/ploet/Desktop/Twitch Downloader/installer.iss\")",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(where:*)",
|
||||||
|
"Bash(cmd.exe /c \"\"\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe\"\" \"\"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\installer.iss\"\"\")",
|
||||||
|
"Bash(cmd.exe /c \"dir \"\"C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\"\"\")",
|
||||||
|
"Bash(powershell.exe -Command \"Test-Path ''C:\\\\Program Files \\(x86\\)\\\\Inno Setup 6\\\\ISCC.exe''\")",
|
||||||
|
"Bash(powershell.exe:*)",
|
||||||
|
"Bash(git init:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git rm:*)",
|
||||||
|
"Bash(git commit -m \"$\\(cat <<''EOF''\nInitial commit: Twitch VOD Manager v3.5.3\n\n- Main application with auto-update functionality\n- PyInstaller spec for building standalone EXE\n- Inno Setup installer script with silent update support\n- Server version.json for update checking\n\nFeatures:\n- Download Twitch VODs\n- Auto-update with silent installation\n- Settings stored in ProgramData\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||||
|
"Bash(git config:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(gh auth:*)",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(npm run dist:win:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Bash(gh release create v3.6.0 \"release/Twitch VOD Manager Setup 3.6.0.exe\" --title \"Twitch VOD Manager v3.6.0\" --notes \"$\\(cat <<''EOF''\n## What''s New in v3.6.0\n\n### New Features\n- **Video Cutter** - Cut and trim your downloaded VODs with a visual timeline\n - Preview frames at any position\n - Set precise start and end times\n - Fast cutting using stream copy \\(no re-encoding\\)\n \n- **Video Merge** - Combine multiple video files into one\n - Add multiple videos and reorder them\n - Fast merging with stream copy\n \n- **Part-based Downloads** - Split long VODs into manageable segments\n - Configure segment length in settings\n - Automatically splits downloads into parts\n\n### Improvements\n- Enhanced download progress with speed and ETA display\n- New navigation tabs for better organization\n- Updated UI with new tool icons\n\n### Technical\n- FFmpeg/FFprobe integration for video processing\n- Improved IPC communication between main and renderer\n- Version bump to 3.6.0\nEOF\n\\)\")",
|
||||||
|
"Bash(powershell:*)",
|
||||||
|
"Bash(winget install:*)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\GitHub CLI\\\\gh.exe\" auth status)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\GitHub CLI\\\\gh.exe\" release create:*)",
|
||||||
|
"Bash(npm start)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\GitHub CLI\\\\gh.exe\" release create v3.6.1 \"release/Twitch VOD Manager Setup 3.6.1.exe\" --title \"Twitch VOD Manager v3.6.1\" --notes \"## What''s New in v3.6.1\n\n### New Feature: Clip erstellen\n- **Time-Range Downloads** - Download specific portions of VODs\n - Click ''Clip'' button on any VOD\n - Use sliders or time inputs to select start/end times\n - Set custom part numbers for continuations\n - Downloads only the selected time range\n\n### All Features \\(v3.6.x\\)\n- VOD Downloads \\(full or part-based\\)\n- Clip Downloads from Twitch\n- Video Cutter for local files \\(FFmpeg\\)\n- Video Merge \\(combine multiple videos\\)\n- Time-Range VOD Downloads \\(new\\)\n- Multiple Themes \\(Twitch/Discord/YouTube/Apple\\)\n- Auto-Update Check\n\n### Technical\n- Uses streamlink --hls-start-offset and --hls-duration for precise downloads\n- CustomClip data structure for queue items\")",
|
||||||
|
"Bash(explorer \"C:\\\\Users\\\\ploet\\\\Desktop\\\\Twitch Downloader\\\\typescript-version\\\\release\")",
|
||||||
|
"Bash(wmic:*)",
|
||||||
|
"Bash(reg query \"HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\" /s)",
|
||||||
|
"Bash(dir /b /ad \"C:\\\\Users\\\\ploet\\\\AppData\\\\Local\")",
|
||||||
|
"Bash(start \"\" \"C:\\\\Program Files \\(x86\\)\\\\Twitch VOD Manager\\\\unins000.exe\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
33
.gitignore
vendored
33
.gitignore
vendored
@ -1,5 +1,32 @@
|
|||||||
node_modules/
|
# Build artifacts
|
||||||
|
build/
|
||||||
dist/
|
dist/
|
||||||
release/
|
installer_output/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyw
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Config with credentials
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# Temp files
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
nul
|
||||||
|
download_queue.json
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Docs workspace
|
||||||
|
docs/node_modules/
|
||||||
|
docs/dist/
|
||||||
|
docs/.astro/
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
Twitch_VOD_Manager.exe
|
||||||
|
|
||||||
|
# Legacy artifacts (not used by TypeScript app)
|
||||||
|
server_files/
|
||||||
|
installer.iss
|
||||||
|
|||||||
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Twitch VOD Manager
|
||||||
|
|
||||||
|
Twitch VOD Manager is a desktop app for browsing Twitch VODs, queueing downloads, creating clips, cutting local videos, and merging files.
|
||||||
|
|
||||||
|
The current codebase is TypeScript + Electron and ships Windows installer releases with in-app auto-update support.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Full docs workspace: `docs/`
|
||||||
|
- Docs index: `docs/src/pages/index.astro`
|
||||||
|
|
||||||
|
Key guides:
|
||||||
|
|
||||||
|
- [Getting Started](docs/src/pages/getting-started.mdx)
|
||||||
|
- [Features](docs/src/pages/features.mdx)
|
||||||
|
- [Configuration](docs/src/pages/configuration.mdx)
|
||||||
|
- [Troubleshooting](docs/src/pages/troubleshooting.mdx)
|
||||||
|
- [Development](docs/src/pages/development.mdx)
|
||||||
|
- [Release Process](docs/src/pages/release-process.mdx)
|
||||||
|
|
||||||
|
## Main Features
|
||||||
|
|
||||||
|
- Streamer list with Twitch Helix VOD browser
|
||||||
|
- Queue-based VOD downloads
|
||||||
|
- Clip extraction workflow from VOD metadata
|
||||||
|
- Local video cutter with preview frame extraction
|
||||||
|
- Local video merge workflow
|
||||||
|
- GitHub release based in-app updates
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Windows 10/11
|
||||||
|
- Node.js 18+ and npm (for local development)
|
||||||
|
- `streamlink` in `PATH`
|
||||||
|
- `ffmpeg` and `ffprobe` in `PATH`
|
||||||
|
|
||||||
|
Optional (recommended for authenticated mode):
|
||||||
|
|
||||||
|
- Twitch app `Client ID` and `Client Secret`
|
||||||
|
|
||||||
|
## Run from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "typescript-version"
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build installer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "typescript-version"
|
||||||
|
npm run dist:win
|
||||||
|
```
|
||||||
|
|
||||||
|
Output artifacts are generated in `typescript-version/release/`.
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
- `typescript-version/` - Electron app source and build config
|
||||||
|
- `docs/` - Astro + MDX documentation site
|
||||||
|
|
||||||
|
## Auto-Update Notes
|
||||||
|
|
||||||
|
For updates to reach installed clients, each release must include:
|
||||||
|
|
||||||
|
- `latest.yml`
|
||||||
|
- `Twitch-VOD-Manager-Setup-<version>.exe`
|
||||||
|
- `Twitch-VOD-Manager-Setup-<version>.exe.blockmap`
|
||||||
|
|
||||||
|
See [Release Process](docs/src/pages/release-process.mdx) for the full checklist.
|
||||||
30
docs/README.md
Normal file
30
docs/README.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Twitch VOD Manager Docs
|
||||||
|
|
||||||
|
Documentation site for users and contributors, built with Astro + MDX.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing docs
|
||||||
|
|
||||||
|
- Add pages in `src/pages/` (`.astro` or `.mdx`)
|
||||||
|
- Shared layout lives in `src/layouts/BaseLayout.astro`
|
||||||
|
- Global styles live in `src/styles/global.css`
|
||||||
|
- Keep command examples copy-paste ready
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- User setup and troubleshooting
|
||||||
|
- Feature documentation
|
||||||
|
- Developer architecture and release workflow
|
||||||
7
docs/astro.config.mjs
Normal file
7
docs/astro.config.mjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [mdx()],
|
||||||
|
site: 'https://github.com/Sucukdeluxe/Twitch-VOD-Manager'
|
||||||
|
});
|
||||||
6199
docs/package-lock.json
generated
Normal file
6199
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
docs/package.json
Normal file
15
docs/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "twitch-vod-manager-docs",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^4.3.0",
|
||||||
|
"astro": "^5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
docs/src/layouts/BaseLayout.astro
Normal file
62
docs/src/layouts/BaseLayout.astro
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description = 'Twitch VOD Manager documentation' } = Astro.props;
|
||||||
|
|
||||||
|
const nav = [
|
||||||
|
{ href: '/', label: 'Overview' },
|
||||||
|
{ href: '/getting-started', label: 'Getting Started' },
|
||||||
|
{ href: '/features', label: 'Features' },
|
||||||
|
{ href: '/configuration', label: 'Configuration' },
|
||||||
|
{ href: '/troubleshooting', label: 'Troubleshooting' },
|
||||||
|
{ href: '/development', label: 'Development' },
|
||||||
|
{ href: '/release-process', label: 'Release Process' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const pathname = Astro.url.pathname.endsWith('/') ? Astro.url.pathname : `${Astro.url.pathname}/`;
|
||||||
|
|
||||||
|
const isActive = (href: string): boolean => {
|
||||||
|
if (href === '/') {
|
||||||
|
return pathname === '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedHref = href.endsWith('/') ? href : `${href}/`;
|
||||||
|
return pathname.startsWith(normalizedHref);
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="layout">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<h1 class="brand">Twitch VOD Manager</h1>
|
||||||
|
<p class="tagline">Product and developer documentation</p>
|
||||||
|
<nav>
|
||||||
|
<ul class="nav-list">
|
||||||
|
{nav.map((item) => (
|
||||||
|
<li>
|
||||||
|
<a href={item.href} class={isActive(item.href) ? 'active' : undefined}>{item.label}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main class="content">
|
||||||
|
<article class="doc">
|
||||||
|
<slot />
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
docs/src/pages/configuration.mdx
Normal file
41
docs/src/pages/configuration.mdx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Configuration
|
||||||
|
description: File locations and configuration keys used by the app.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
The app stores runtime data in:
|
||||||
|
|
||||||
|
`C:\ProgramData\Twitch_VOD_Manager`
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `config.json` | User settings and Twitch credentials |
|
||||||
|
| `download_queue.json` | Persistent download queue state |
|
||||||
|
|
||||||
|
Default download path:
|
||||||
|
|
||||||
|
`%USERPROFILE%\Desktop\Twitch_VODs`
|
||||||
|
|
||||||
|
## Main Config Keys
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `client_id` | Twitch application client id |
|
||||||
|
| `client_secret` | Twitch application client secret |
|
||||||
|
| `download_path` | Base output folder |
|
||||||
|
| `streamers` | Sidebar streamer list |
|
||||||
|
| `theme` | UI theme (`twitch`, `discord`, `youtube`, `apple`) |
|
||||||
|
| `download_mode` | Full VOD or parts mode |
|
||||||
|
| `part_minutes` | Split length in minutes for parts mode |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Credentials are optional. If empty, the app uses public mode for streamer/VOD discovery.
|
||||||
|
- Credentials are currently stored in plain text config file.
|
||||||
|
- The app trims credential fields before saving to reduce whitespace issues.
|
||||||
|
- Legacy keys can exist in config from older versions; unknown keys are ignored.
|
||||||
72
docs/src/pages/development.mdx
Normal file
72
docs/src/pages/development.mdx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Development
|
||||||
|
description: Local development setup and architecture for contributors.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## Local Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "typescript-version"
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Layer | File(s) | Responsibility |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Main process | `src/main.ts` | IPC handlers, Twitch API, downloads, ffmpeg/streamlink execution, updater |
|
||||||
|
| Preload bridge | `src/preload.ts` | Safe API surface exposed to renderer |
|
||||||
|
| Renderer shell | `src/index.html` + `src/styles.css` | UI markup and styling |
|
||||||
|
| Renderer modules | `src/renderer*.ts` | UI logic by feature (streamers, queue, settings, updates, shared state) |
|
||||||
|
|
||||||
|
## Renderer Module Split
|
||||||
|
|
||||||
|
- `renderer-shared.ts`: common state + DOM helper functions
|
||||||
|
- `renderer-streamers.ts`: streamer list + VOD loading
|
||||||
|
- `renderer-queue.ts`: queue rendering + start/stop behavior
|
||||||
|
- `renderer-settings.ts`: credentials, folder, and theme handling
|
||||||
|
- `renderer-updates.ts`: update banner and download/install flow
|
||||||
|
- `renderer.ts`: app init + clip/cutter/merge orchestration
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build TypeScript only
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run app in dev mode
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Quick UI smoke test
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
|
# Template guide + live preview checks
|
||||||
|
npm run test:e2e:guide
|
||||||
|
|
||||||
|
# Full end-to-end validation pass
|
||||||
|
npm run test:e2e:full
|
||||||
|
|
||||||
|
# Release validation suite (build + smoke + guide + full)
|
||||||
|
npm run test:e2e:release
|
||||||
|
|
||||||
|
# Extra stress pass (runs release suite 3x)
|
||||||
|
npm run test:e2e:stress
|
||||||
|
|
||||||
|
# Build Windows installer
|
||||||
|
npm run dist:win
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docs Workspace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "docs"
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Docs site is Astro + MDX and can be updated independently from app runtime code.
|
||||||
44
docs/src/pages/features.mdx
Normal file
44
docs/src/pages/features.mdx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Features
|
||||||
|
description: Complete feature overview for Twitch VOD Manager.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
## Twitch VOD Browser
|
||||||
|
|
||||||
|
- Add streamers to a persistent sidebar list.
|
||||||
|
- Fetches user + archive VODs through Twitch Helix API when credentials are configured.
|
||||||
|
- Falls back to public Twitch GraphQL mode when no credentials are set.
|
||||||
|
- Automatic reconnect/re-auth if the app is temporarily disconnected.
|
||||||
|
|
||||||
|
## Download Queue
|
||||||
|
|
||||||
|
- Add full VODs to queue with one click.
|
||||||
|
- Queue supports start/stop and removal of completed entries.
|
||||||
|
- Download process persists in local queue file.
|
||||||
|
|
||||||
|
## Clip Creation from VODs
|
||||||
|
|
||||||
|
- Open **Clip** dialog from any VOD card.
|
||||||
|
- Set start/end time and optional part number.
|
||||||
|
- Queue clip jobs with metadata for naming strategy.
|
||||||
|
|
||||||
|
## Video Cutter (Local Files)
|
||||||
|
|
||||||
|
- Select any local video and inspect duration/fps/resolution.
|
||||||
|
- Extract preview frames via `ffprobe`/`ffmpeg`.
|
||||||
|
- Export cut segment as new MP4 file.
|
||||||
|
|
||||||
|
## Video Merge (Local Files)
|
||||||
|
|
||||||
|
- Choose multiple local video files.
|
||||||
|
- Reorder via up/down controls.
|
||||||
|
- Merge into one output file.
|
||||||
|
|
||||||
|
## In-App Auto-Update
|
||||||
|
|
||||||
|
- Uses GitHub release artifacts through `electron-updater`.
|
||||||
|
- Detects new versions, downloads update, then installs on restart.
|
||||||
|
- Requires release assets: installer, blockmap, and `latest.yml`.
|
||||||
52
docs/src/pages/getting-started.mdx
Normal file
52
docs/src/pages/getting-started.mdx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Getting Started
|
||||||
|
description: Install and configure Twitch VOD Manager quickly.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Windows 10/11 (installer and paths are currently Windows-first)
|
||||||
|
|
||||||
|
The app can auto-install missing runtime tools (`streamlink`, `ffmpeg`, `ffprobe`) into:
|
||||||
|
|
||||||
|
`C:\ProgramData\Twitch_VOD_Manager\tools`
|
||||||
|
|
||||||
|
Manual installation is still supported.
|
||||||
|
|
||||||
|
Optional but recommended:
|
||||||
|
|
||||||
|
- Twitch API app with `Client ID` and `Client Secret` (for authenticated Helix mode)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Download the latest setup from GitHub Releases.
|
||||||
|
2. Run `Twitch-VOD-Manager-Setup-<version>.exe`.
|
||||||
|
3. Launch the app.
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
1. Open **Einstellungen**.
|
||||||
|
2. Choose your download path.
|
||||||
|
3. Optional: enter `Client ID` and `Client Secret` for authenticated mode.
|
||||||
|
4. Click **Speichern & Verbinden**.
|
||||||
|
5. Add a streamer in the header input and press `+`.
|
||||||
|
|
||||||
|
If everything is correct, VOD cards appear.
|
||||||
|
|
||||||
|
- With credentials: status is `Verbunden`.
|
||||||
|
- Without credentials: status shows `Ohne Login (Public Modus)` and VOD downloads still work.
|
||||||
|
|
||||||
|
## Verify Tools
|
||||||
|
|
||||||
|
In PowerShell or CMD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
streamlink --version
|
||||||
|
ffmpeg -version
|
||||||
|
ffprobe -version
|
||||||
|
```
|
||||||
|
|
||||||
|
If one command fails, install the missing tool and restart the app.
|
||||||
51
docs/src/pages/index.astro
Normal file
51
docs/src/pages/index.astro
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Twitch VOD Manager Docs" description="Official documentation for Twitch VOD Manager users and contributors.">
|
||||||
|
<h1>Twitch VOD Manager Documentation</h1>
|
||||||
|
<p>
|
||||||
|
This documentation covers end-user setup, Twitch API configuration, troubleshooting,
|
||||||
|
and developer workflows for the TypeScript/Electron app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="chips">
|
||||||
|
<span class="chip">Electron</span>
|
||||||
|
<span class="chip">TypeScript</span>
|
||||||
|
<span class="chip">Streamlink</span>
|
||||||
|
<span class="chip">FFmpeg</span>
|
||||||
|
<span class="chip">Auto-Update</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-grid">
|
||||||
|
<a class="card" href="/getting-started">
|
||||||
|
<h3>Getting Started</h3>
|
||||||
|
<p>Install the app, set Twitch API credentials, and run your first VOD download.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="/features">
|
||||||
|
<h3>Features</h3>
|
||||||
|
<p>Learn VOD browsing, queue processing, clip creation, cutter, merge, and updates.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="/configuration">
|
||||||
|
<h3>Configuration</h3>
|
||||||
|
<p>Understand config paths, available settings, and how local files are stored.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="/troubleshooting">
|
||||||
|
<h3>Troubleshooting</h3>
|
||||||
|
<p>Fix common issues like "Keine VODs", missing tools, and update problems.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="/development">
|
||||||
|
<h3>Development</h3>
|
||||||
|
<p>Set up local development, architecture overview, and code structure details.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="/release-process">
|
||||||
|
<h3>Release Process</h3>
|
||||||
|
<p>Ship new installer builds and GitHub releases compatible with auto-updater.</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
64
docs/src/pages/release-process.mdx
Normal file
64
docs/src/pages/release-process.mdx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Release Process
|
||||||
|
description: Standardized release checklist for auto-update compatible builds.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Release Process
|
||||||
|
|
||||||
|
This project uses GitHub Releases + `electron-updater`.
|
||||||
|
|
||||||
|
## 1) Bump Version
|
||||||
|
|
||||||
|
Update app version consistently:
|
||||||
|
|
||||||
|
- `typescript-version/package.json`
|
||||||
|
- `typescript-version/package-lock.json`
|
||||||
|
- `typescript-version/src/main.ts` (`APP_VERSION`)
|
||||||
|
- Optional visible fallback text in `src/index.html`
|
||||||
|
|
||||||
|
## 2) Build Installer Artifacts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "typescript-version"
|
||||||
|
npm run dist:win
|
||||||
|
```
|
||||||
|
|
||||||
|
`dist:win` already runs the full release validation gate (`test:e2e:release`) before packaging.
|
||||||
|
|
||||||
|
For extra confidence before major releases, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:e2e:stress
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected outputs in `typescript-version/release/`:
|
||||||
|
|
||||||
|
- `latest.yml`
|
||||||
|
- `Twitch-VOD-Manager-Setup-<version>.exe`
|
||||||
|
- `Twitch-VOD-Manager-Setup-<version>.exe.blockmap`
|
||||||
|
|
||||||
|
## 3) Commit + Push
|
||||||
|
|
||||||
|
Commit code/version changes and push to `master`.
|
||||||
|
|
||||||
|
## 4) Create GitHub Release
|
||||||
|
|
||||||
|
Tag format: `v<version>` (example: `v3.7.6`).
|
||||||
|
|
||||||
|
Attach exactly the 3 update artifacts from step 2.
|
||||||
|
|
||||||
|
## 5) Verify Update Path
|
||||||
|
|
||||||
|
- Open app on older version.
|
||||||
|
- Trigger update check.
|
||||||
|
- Confirm banner appears and update can be downloaded/installed.
|
||||||
|
|
||||||
|
## Quick Checklist
|
||||||
|
|
||||||
|
- [ ] Version bumped everywhere
|
||||||
|
- [ ] `npm run build` passes
|
||||||
|
- [ ] `npm run dist:win` passes
|
||||||
|
- [ ] Release tag created
|
||||||
|
- [ ] `latest.yml` + `.exe` + `.blockmap` uploaded
|
||||||
|
- [ ] Manual update flow verified
|
||||||
17
docs/src/pages/roadmap.mdx
Normal file
17
docs/src/pages/roadmap.mdx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Migration Roadmap
|
||||||
|
---
|
||||||
|
|
||||||
|
# Migration Roadmap
|
||||||
|
|
||||||
|
## UI Stack
|
||||||
|
|
||||||
|
- Renderer wird schrittweise von HTML-Monolith zu TypeScript-Modulen migriert.
|
||||||
|
- Styles sind bereits in eine eigene CSS-Datei ausgelagert.
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
1. Renderer in weitere Feature-Module aufteilen (Cutter/Merge/Clips).
|
||||||
|
2. Komponenten-Ansatz einführen (Astro UI docs, später optional Rust-backend tooling).
|
||||||
|
3. API- und Release-Prozess in MDX dokumentieren.
|
||||||
64
docs/src/pages/troubleshooting.mdx
Normal file
64
docs/src/pages/troubleshooting.mdx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
layout: ../layouts/BaseLayout.astro
|
||||||
|
title: Troubleshooting
|
||||||
|
description: Fix common Twitch VOD Manager issues quickly.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## "Keine VODs" after adding streamer
|
||||||
|
|
||||||
|
1. Ensure the streamer exists and has archive VODs.
|
||||||
|
2. Press **Aktualisieren**.
|
||||||
|
3. If you use credentials, re-save `Client ID` + `Client Secret` in **Einstellungen**.
|
||||||
|
4. Check bottom status bar:
|
||||||
|
- `Verbunden` in authenticated mode
|
||||||
|
- `Ohne Login (Public Modus)` in credential-free mode
|
||||||
|
|
||||||
|
The app retries auth automatically on expired tokens and can continue in public mode without credentials.
|
||||||
|
|
||||||
|
## "Streamer nicht gefunden"
|
||||||
|
|
||||||
|
- Use login name only (no URL, no `@`).
|
||||||
|
- Names are normalized to lowercase.
|
||||||
|
- Verify streamer spelling directly on Twitch.
|
||||||
|
|
||||||
|
## Clip/Cutter/Merge duration shows 0 or preview fails
|
||||||
|
|
||||||
|
Usually caused by broken `ffprobe` detection.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffprobe -version
|
||||||
|
```
|
||||||
|
|
||||||
|
If missing, install FFmpeg package that includes `ffprobe` and restart app.
|
||||||
|
|
||||||
|
## "Streamlink not found"
|
||||||
|
|
||||||
|
Install streamlink and verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
streamlink --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart the app.
|
||||||
|
|
||||||
|
## Auto-update does not trigger
|
||||||
|
|
||||||
|
Release must include all of:
|
||||||
|
|
||||||
|
- `latest.yml`
|
||||||
|
- `Twitch-VOD-Manager-Setup-<version>.exe`
|
||||||
|
- `Twitch-VOD-Manager-Setup-<version>.exe.blockmap`
|
||||||
|
|
||||||
|
Tag version must match app version (example: `v3.7.6`).
|
||||||
|
|
||||||
|
## Debug log for failed downloads
|
||||||
|
|
||||||
|
From `v3.7.8`, detailed downloader logs are written to:
|
||||||
|
|
||||||
|
`C:\ProgramData\Twitch_VOD_Manager\debug.log`
|
||||||
|
|
||||||
|
If a download instantly switches from `Stoppen` back to `Start`, check the latest lines in that file for streamlink exit reasons.
|
||||||
205
docs/src/styles/global.css
Normal file
205
docs/src/styles/global.css
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||||
|
--bg-1: #0f1217;
|
||||||
|
--bg-2: #161c26;
|
||||||
|
--bg-3: #1d2734;
|
||||||
|
--text: #eef3fb;
|
||||||
|
--muted: #aab8cf;
|
||||||
|
--line: #2b3546;
|
||||||
|
--link: #7db6ff;
|
||||||
|
--link-hover: #a8cdff;
|
||||||
|
--chip: #22344b;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: radial-gradient(circle at top, #1a2432 0%, var(--bg-1) 55%);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 290px minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 18px;
|
||||||
|
align-self: start;
|
||||||
|
background: linear-gradient(180deg, var(--bg-2), #131922);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 20px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list a {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--line);
|
||||||
|
background: rgba(125, 182, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list a.active {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(125, 182, 255, 0.16);
|
||||||
|
border-color: rgba(125, 182, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc {
|
||||||
|
background: linear-gradient(180deg, var(--bg-2), #121822);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin-top: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
li {
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #0c1118;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #111a25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #172233, #111824);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
border: 1px solid rgba(125, 182, 255, 0.35);
|
||||||
|
background: var(--chip);
|
||||||
|
color: #dbe9ff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
docs/tsconfig.json
Normal file
3
docs/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict"
|
||||||
|
}
|
||||||
@ -1,159 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
|
|
||||||
const NPM_EXECUTABLE = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
||||||
const BASE_URL = String(process.env.GITEA_BASE_URL || "https://git.24-music.de").replace(/\/+$/, "");
|
|
||||||
const OWNER = String(process.env.GITEA_REPO_OWNER || "Administrator").trim();
|
|
||||||
const REPO = String(process.env.GITEA_REPO_NAME || "Twitch-VOD-Manager").trim();
|
|
||||||
|
|
||||||
function run(command, args, options = {}) {
|
|
||||||
const result = spawnSync(command, args, {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
encoding: "utf8",
|
|
||||||
input: options.input,
|
|
||||||
stdio: options.capture ? ["pipe", "pipe", "pipe"] : "inherit"
|
|
||||||
});
|
|
||||||
if (result.status !== 0) {
|
|
||||||
const stderr = String(result.stderr || "").trim();
|
|
||||||
const stdout = String(result.stdout || "").trim();
|
|
||||||
const details = [stderr, stdout].filter(Boolean).join("\n");
|
|
||||||
throw new Error(`Command failed: ${command} ${args.join(" ")}${details ? `\n${details}` : ""}`);
|
|
||||||
}
|
|
||||||
return String(result.stdout || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const args = argv.slice(2);
|
|
||||||
if (args.includes("--help") || args.includes("-h")) {
|
|
||||||
return { help: true };
|
|
||||||
}
|
|
||||||
const dryRun = args.includes("--dry-run");
|
|
||||||
const version = args.find((arg) => arg !== "--dry-run") || "";
|
|
||||||
const notes = args.filter((arg) => arg !== "--dry-run").slice(1).join(" ").trim();
|
|
||||||
return { help: false, dryRun, version, notes };
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureVersion(version) {
|
|
||||||
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version || "").trim())) {
|
|
||||||
throw new Error("Invalid version format. Expected e.g. 4.2.0");
|
|
||||||
}
|
|
||||||
return String(version).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthHeader() {
|
|
||||||
const token = String(process.env.GITEA_TOKEN || process.env.FORGEJO_TOKEN || "").trim();
|
|
||||||
if (token) {
|
|
||||||
return `token ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = run("git", ["credential", "fill"], {
|
|
||||||
capture: true,
|
|
||||||
input: `protocol=https\nhost=${new URL(BASE_URL).host}\n\n`
|
|
||||||
});
|
|
||||||
const map = new Map();
|
|
||||||
for (const line of output.split(/\r?\n/)) {
|
|
||||||
if (!line.includes("=")) continue;
|
|
||||||
const [key, value] = line.split("=", 2);
|
|
||||||
map.set(key, value);
|
|
||||||
}
|
|
||||||
const username = map.get("username");
|
|
||||||
const password = map.get("password");
|
|
||||||
if (!username || !password) {
|
|
||||||
throw new Error("Missing Gitea credentials. Set GITEA_TOKEN or configure git credential helper.");
|
|
||||||
}
|
|
||||||
return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiRequest(method, url, authHeader, body, contentType = "application/json") {
|
|
||||||
const headers = { Accept: "application/json", Authorization: authHeader };
|
|
||||||
if (body !== undefined) headers["Content-Type"] = contentType;
|
|
||||||
const response = await fetch(url, { method, headers, body });
|
|
||||||
const text = await response.text();
|
|
||||||
let parsed = null;
|
|
||||||
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
|
|
||||||
return { ok: response.ok, status: response.status, body: parsed };
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureAssets(version) {
|
|
||||||
const releaseDir = path.join(process.cwd(), "release");
|
|
||||||
const files = [
|
|
||||||
`Twitch-VOD-Manager-Setup-${version}.exe`,
|
|
||||||
`Twitch-VOD-Manager-Setup-${version}.exe.blockmap`,
|
|
||||||
"latest.yml"
|
|
||||||
];
|
|
||||||
for (const file of files) {
|
|
||||||
const fullPath = path.join(releaseDir, file);
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
throw new Error(`Missing release artifact: ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { releaseDir, files };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOrGetRelease(baseApi, tag, authHeader, notes) {
|
|
||||||
const existing = await apiRequest("GET", `${baseApi}/releases/tags/${encodeURIComponent(tag)}`, authHeader);
|
|
||||||
if (existing.ok) return existing.body;
|
|
||||||
const payload = {
|
|
||||||
tag_name: tag,
|
|
||||||
target_commitish: "main",
|
|
||||||
name: tag,
|
|
||||||
body: notes || `Release ${tag}`,
|
|
||||||
draft: false,
|
|
||||||
prerelease: false
|
|
||||||
};
|
|
||||||
const created = await apiRequest("POST", `${baseApi}/releases`, authHeader, JSON.stringify(payload));
|
|
||||||
if (!created.ok) {
|
|
||||||
throw new Error(`Failed to create release (${created.status}): ${JSON.stringify(created.body)}`);
|
|
||||||
}
|
|
||||||
return created.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadAssets(baseApi, releaseId, authHeader, releaseDir, files) {
|
|
||||||
for (const fileName of files) {
|
|
||||||
const filePath = path.join(releaseDir, fileName);
|
|
||||||
const fileData = fs.readFileSync(filePath);
|
|
||||||
const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`;
|
|
||||||
const response = await apiRequest("POST", uploadUrl, authHeader, fileData, "application/octet-stream");
|
|
||||||
if (response.ok || response.status === 409 || response.status === 422) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(response.body)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = parseArgs(process.argv);
|
|
||||||
if (args.help) {
|
|
||||||
process.stdout.write("Usage: npm run release:gitea -- <version> [release notes] [--dry-run]\n");
|
|
||||||
process.stdout.write("Env: GITEA_BASE_URL, GITEA_REPO_OWNER, GITEA_REPO_NAME, GITEA_TOKEN\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = ensureVersion(args.version);
|
|
||||||
const tag = `v${version}`;
|
|
||||||
const authHeader = getAuthHeader();
|
|
||||||
const baseApi = `${BASE_URL}/api/v1/repos/${OWNER}/${REPO}`;
|
|
||||||
|
|
||||||
run("git", ["fetch", "--tags"]);
|
|
||||||
if (!args.dryRun) {
|
|
||||||
run("git", ["push", "origin", "main"]);
|
|
||||||
run("git", ["push", "origin", tag]);
|
|
||||||
}
|
|
||||||
|
|
||||||
run(NPM_EXECUTABLE, ["run", "dist:win"]);
|
|
||||||
const assets = ensureAssets(version);
|
|
||||||
if (args.dryRun) {
|
|
||||||
process.stdout.write(`Dry run complete for ${tag}\n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const release = await createOrGetRelease(baseApi, tag, authHeader, args.notes);
|
|
||||||
await uploadAssets(baseApi, release.id, authHeader, assets.releaseDir, assets.files);
|
|
||||||
process.stdout.write(`Release published: ${release.html_url || `${BASE_URL}/${OWNER}/${REPO}/releases/tag/${tag}`}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
process.stderr.write(`${String(error?.message || error)}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
const {
|
|
||||||
normalizeUpdateVersion,
|
|
||||||
compareUpdateVersions,
|
|
||||||
isNewerUpdateVersion
|
|
||||||
} = require(path.join(process.cwd(), 'dist', 'update-version-utils.js'));
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
const failures = [];
|
|
||||||
|
|
||||||
const assert = (condition, message) => {
|
|
||||||
if (!condition) failures.push(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const comparisons = [
|
|
||||||
{ left: '4.1.18', right: '4.1.10', expected: 1 },
|
|
||||||
{ left: '4.1.10', right: '4.1.18', expected: -1 },
|
|
||||||
{ left: 'v4.1.12', right: '4.1.12', expected: 0 },
|
|
||||||
{ left: '4.1.12', right: '4.1.12.1', expected: -1 },
|
|
||||||
{ left: '4.2.0', right: '4.1.999', expected: 1 },
|
|
||||||
{ left: '4.1.12-beta', right: '4.1.12', expected: 0 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const compareResults = comparisons.map((testCase) => {
|
|
||||||
const actual = compareUpdateVersions(testCase.left, testCase.right);
|
|
||||||
const pass = actual === testCase.expected;
|
|
||||||
assert(pass, `compare failed: ${testCase.left} vs ${testCase.right} expected ${testCase.expected}, got ${actual}`);
|
|
||||||
return { ...testCase, actual, pass };
|
|
||||||
});
|
|
||||||
|
|
||||||
const skipVersionScenarios = [
|
|
||||||
{
|
|
||||||
name: 'old downloaded, newer available',
|
|
||||||
downloaded: '4.1.11',
|
|
||||||
latestKnown: '4.1.18',
|
|
||||||
expectedNeedsNewer: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'already latest downloaded',
|
|
||||||
downloaded: '4.1.18',
|
|
||||||
latestKnown: '4.1.18',
|
|
||||||
expectedNeedsNewer: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'downgrade should not trigger',
|
|
||||||
downloaded: '4.1.18',
|
|
||||||
latestKnown: '4.1.11',
|
|
||||||
expectedNeedsNewer: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const scenarioResults = skipVersionScenarios.map((scenario) => {
|
|
||||||
const needsNewer = isNewerUpdateVersion(scenario.latestKnown, scenario.downloaded);
|
|
||||||
const pass = needsNewer === scenario.expectedNeedsNewer;
|
|
||||||
assert(pass, `${scenario.name} expected ${scenario.expectedNeedsNewer}, got ${needsNewer}`);
|
|
||||||
return { ...scenario, needsNewer, pass };
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalizationChecks = {
|
|
||||||
fromVPrefix: normalizeUpdateVersion('v4.1.12') === '4.1.12',
|
|
||||||
trimmed: normalizeUpdateVersion(' 4.1.12 ') === '4.1.12'
|
|
||||||
};
|
|
||||||
|
|
||||||
assert(normalizationChecks.fromVPrefix, 'normalize did not remove v prefix');
|
|
||||||
assert(normalizationChecks.trimmed, 'normalize did not trim whitespace');
|
|
||||||
|
|
||||||
const summary = {
|
|
||||||
checks: {
|
|
||||||
compareResults,
|
|
||||||
scenarioResults,
|
|
||||||
normalizationChecks
|
|
||||||
},
|
|
||||||
failures
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(JSON.stringify(summary, null, 2));
|
|
||||||
|
|
||||||
if (failures.length) {
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
@ -1,216 +0,0 @@
|
|||||||
let updateCheckInProgress = false;
|
|
||||||
let updateDownloadInProgress = false;
|
|
||||||
let manualUpdateCheckPending = false;
|
|
||||||
let latestUpdateVersion = '';
|
|
||||||
|
|
||||||
function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
|
||||||
const toastFn = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
|
||||||
if (typeof toastFn === 'function') {
|
|
||||||
toastFn(message, type);
|
|
||||||
} else if (type === 'warn') {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCheckButtonCheckingState(enabled: boolean): void {
|
|
||||||
const btn = byId<HTMLButtonElement>('checkUpdateBtn');
|
|
||||||
btn.disabled = enabled;
|
|
||||||
btn.textContent = enabled ? UI_TEXT.updates.checking : UI_TEXT.static.checkUpdates;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showUpdateBanner(): void {
|
|
||||||
byId('updateBanner').style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDownloadPendingUi(): void {
|
|
||||||
showUpdateBanner();
|
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
|
||||||
button.textContent = UI_TEXT.updates.downloading;
|
|
||||||
button.disabled = true;
|
|
||||||
byId('updateProgress').style.display = 'block';
|
|
||||||
const bar = byId('updateProgressBar');
|
|
||||||
bar.classList.add('downloading');
|
|
||||||
bar.style.width = '30%';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDownloadReadyUi(version: string): void {
|
|
||||||
showUpdateBanner();
|
|
||||||
updateReady = true;
|
|
||||||
updateDownloadInProgress = false;
|
|
||||||
latestUpdateVersion = version || latestUpdateVersion;
|
|
||||||
|
|
||||||
const bar = byId('updateProgressBar');
|
|
||||||
bar.classList.remove('downloading');
|
|
||||||
bar.style.width = '100%';
|
|
||||||
|
|
||||||
byId('updateText').textContent = `Version ${latestUpdateVersion || '?'} ${UI_TEXT.updates.ready}`;
|
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
|
||||||
button.textContent = UI_TEXT.updates.installNow;
|
|
||||||
button.disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUpdateSilent(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await window.api.checkUpdate();
|
|
||||||
} catch {
|
|
||||||
// ignore silent updater errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUpdate(): Promise<void> {
|
|
||||||
manualUpdateCheckPending = true;
|
|
||||||
setCheckButtonCheckingState(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.api.checkUpdate();
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
notifyUpdate(UI_TEXT.updates.checkFailed, 'warn');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const skippedReason = result?.skipped;
|
|
||||||
if (skippedReason === 'ready-to-install') {
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
notifyUpdate(UI_TEXT.updates.readyToInstall, 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skippedReason === 'in-progress' || skippedReason === 'throttled') {
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
notifyUpdate(UI_TEXT.updates.checkInProgress, 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (!updateReady && byId('updateBanner').style.display !== 'flex') {
|
|
||||||
notifyUpdate(UI_TEXT.updates.latest, 'info');
|
|
||||||
}
|
|
||||||
}, 2500);
|
|
||||||
} catch {
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
notifyUpdate(UI_TEXT.updates.checkFailed, 'warn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadUpdate(): void {
|
|
||||||
if (updateReady) {
|
|
||||||
void window.api.installUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateDownloadInProgress) {
|
|
||||||
notifyUpdate(UI_TEXT.updates.downloadInProgress, 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDownloadInProgress = true;
|
|
||||||
setDownloadPendingUi();
|
|
||||||
|
|
||||||
void window.api.downloadUpdate().then((result) => {
|
|
||||||
if (result?.error) {
|
|
||||||
updateDownloadInProgress = false;
|
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
|
||||||
button.textContent = UI_TEXT.updates.downloadNow;
|
|
||||||
button.disabled = false;
|
|
||||||
byId('updateProgressBar').classList.remove('downloading');
|
|
||||||
notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result?.skipped === 'ready-to-install') {
|
|
||||||
setDownloadReadyUi(latestUpdateVersion);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result?.skipped === 'in-progress') {
|
|
||||||
notifyUpdate(UI_TEXT.updates.downloadInProgress, 'info');
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
updateDownloadInProgress = false;
|
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
|
||||||
button.textContent = UI_TEXT.updates.downloadNow;
|
|
||||||
button.disabled = false;
|
|
||||||
byId('updateProgressBar').classList.remove('downloading');
|
|
||||||
notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.api.onUpdateChecking(() => {
|
|
||||||
updateCheckInProgress = true;
|
|
||||||
if (manualUpdateCheckPending) {
|
|
||||||
setCheckButtonCheckingState(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateAvailable((info: UpdateInfo) => {
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
updateReady = false;
|
|
||||||
updateDownloadInProgress = true;
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
latestUpdateVersion = info.version;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
|
|
||||||
showUpdateBanner();
|
|
||||||
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`;
|
|
||||||
byId('updateButton').textContent = UI_TEXT.updates.downloading;
|
|
||||||
byId<HTMLButtonElement>('updateButton').disabled = true;
|
|
||||||
byId('updateProgress').style.display = 'block';
|
|
||||||
byId('updateProgressBar').classList.add('downloading');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateNotAvailable(() => {
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
|
|
||||||
if (manualUpdateCheckPending) {
|
|
||||||
notifyUpdate(UI_TEXT.updates.latest, 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
|
||||||
updateDownloadInProgress = true;
|
|
||||||
const bar = byId('updateProgressBar');
|
|
||||||
bar.classList.remove('downloading');
|
|
||||||
bar.style.width = progress.percent + '%';
|
|
||||||
|
|
||||||
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
|
||||||
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
|
||||||
byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
|
||||||
setDownloadReadyUi(info.version);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.api.onUpdateError(() => {
|
|
||||||
updateCheckInProgress = false;
|
|
||||||
const wasDownloading = updateDownloadInProgress;
|
|
||||||
updateDownloadInProgress = false;
|
|
||||||
manualUpdateCheckPending = false;
|
|
||||||
setCheckButtonCheckingState(false);
|
|
||||||
|
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
|
||||||
if (!updateReady) {
|
|
||||||
button.textContent = UI_TEXT.updates.downloadNow;
|
|
||||||
button.disabled = false;
|
|
||||||
byId('updateProgressBar').classList.remove('downloading');
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyUpdate(wasDownloading ? UI_TEXT.updates.downloadFailed : UI_TEXT.updates.checkFailed, 'warn');
|
|
||||||
});
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
export function normalizeUpdateVersion(version: string | null | undefined): string {
|
|
||||||
return (version || '').trim().replace(/^v/i, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseVersionPart(part: string): number {
|
|
||||||
const numeric = Number(part.replace(/[^0-9].*$/, ''));
|
|
||||||
return Number.isFinite(numeric) ? numeric : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compareUpdateVersions(left: string | null | undefined, right: string | null | undefined): number {
|
|
||||||
const a = normalizeUpdateVersion(left);
|
|
||||||
const b = normalizeUpdateVersion(right);
|
|
||||||
|
|
||||||
if (!a && !b) return 0;
|
|
||||||
if (!a) return -1;
|
|
||||||
if (!b) return 1;
|
|
||||||
|
|
||||||
const aParts = a.split('.').map(parseVersionPart);
|
|
||||||
const bParts = b.split('.').map(parseVersionPart);
|
|
||||||
const maxLength = Math.max(aParts.length, bParts.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLength; i += 1) {
|
|
||||||
const av = aParts[i] || 0;
|
|
||||||
const bv = bParts[i] || 0;
|
|
||||||
if (av > bv) return 1;
|
|
||||||
if (av < bv) return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNewerUpdateVersion(candidate: string | null | undefined, baseline: string | null | undefined): boolean {
|
|
||||||
return compareUpdateVersions(candidate, baseline) > 0;
|
|
||||||
}
|
|
||||||
5
typescript-version/.gitignore
vendored
Normal file
5
typescript-version/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
release/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.0",
|
"version": "4.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.0",
|
"version": "4.1.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
@ -682,6 +682,7 @@
|
|||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@ -836,7 +837,6 @@
|
|||||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@ -856,7 +856,6 @@
|
|||||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@ -879,7 +878,6 @@
|
|||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@ -895,8 +893,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@ -904,7 +901,6 @@
|
|||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@ -1015,7 +1011,6 @@
|
|||||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
@ -1386,7 +1381,6 @@
|
|||||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
@ -1487,7 +1481,6 @@
|
|||||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
@ -1501,7 +1494,6 @@
|
|||||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
@ -1860,7 +1852,6 @@
|
|||||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
@ -1874,7 +1865,6 @@
|
|||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@ -1890,7 +1880,6 @@
|
|||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -1904,7 +1893,6 @@
|
|||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@ -2329,8 +2317,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@ -2818,8 +2805,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.7",
|
||||||
@ -2954,7 +2940,6 @@
|
|||||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
@ -2968,7 +2953,6 @@
|
|||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@ -2984,8 +2968,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@ -2993,7 +2976,6 @@
|
|||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@ -3010,16 +2992,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.escaperegexp": {
|
"node_modules/lodash.escaperegexp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
@ -3032,8 +3012,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isequal": {
|
"node_modules/lodash.isequal": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@ -3047,16 +3026,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3241,7 +3218,6 @@
|
|||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -3375,8 +3351,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@ -3466,7 +3441,6 @@
|
|||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@ -3482,7 +3456,6 @@
|
|||||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
@ -3565,8 +3538,7 @@
|
|||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -3764,7 +3736,6 @@
|
|||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@ -3878,7 +3849,6 @@
|
|||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@ -4042,8 +4012,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/verror": {
|
"node_modules/verror": {
|
||||||
"version": "1.10.1",
|
"version": "1.10.1",
|
||||||
@ -4194,7 +4163,6 @@
|
|||||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
@ -4210,7 +4178,6 @@
|
|||||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.0",
|
"version": "4.1.5",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "xRangerDE",
|
"author": "xRangerDE",
|
||||||
@ -8,16 +8,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "npm run build && electron .",
|
"start": "npm run build && electron .",
|
||||||
"test:e2e:update-logic": "node scripts/smoke-test-update-version-logic.js",
|
|
||||||
"test:e2e": "npm exec --yes --package=playwright -- node scripts/smoke-test.js",
|
"test:e2e": "npm exec --yes --package=playwright -- node scripts/smoke-test.js",
|
||||||
"test:e2e:guide": "npm exec --yes --package=playwright -- node scripts/smoke-test-template-guide.js",
|
"test:e2e:guide": "npm exec --yes --package=playwright -- node scripts/smoke-test-template-guide.js",
|
||||||
"test:e2e:full": "npm exec --yes --package=playwright -- node scripts/smoke-test-full.js",
|
"test:e2e:full": "npm exec --yes --package=playwright -- node scripts/smoke-test-full.js",
|
||||||
"test:e2e:release": "npm run build && npm run test:e2e:update-logic && npm run test:e2e && npm run test:e2e:guide && npm run test:e2e:full",
|
"test:e2e:release": "npm run build && npm run test:e2e && npm run test:e2e:guide && npm run test:e2e:full",
|
||||||
"test:e2e:stress": "npm run test:e2e:release && npm run test:e2e:release && npm run test:e2e:release",
|
"test:e2e:stress": "npm run test:e2e:release && npm run test:e2e:release && npm run test:e2e:release",
|
||||||
"pack": "npm run build && electron-builder --dir",
|
"pack": "npm run build && electron-builder --dir",
|
||||||
"dist": "npm run build && electron-builder",
|
"dist": "npm run build && electron-builder",
|
||||||
"dist:win": "npm run test:e2e:release && electron-builder --win",
|
"dist:win": "npm run test:e2e:release && electron-builder --win"
|
||||||
"release:gitea": "node scripts/release_gitea.mjs"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
@ -50,8 +48,9 @@
|
|||||||
"include": "build/installer.nsh"
|
"include": "build/installer.nsh"
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "generic",
|
"provider": "github",
|
||||||
"url": "https://git.24-music.de/Administrator/Twitch-VOD-Manager/releases/latest/download/"
|
"owner": "Sucukdeluxe",
|
||||||
|
"repo": "Twitch-VOD-Manager"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -457,7 +457,7 @@
|
|||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="updateTitle">Updates</h3>
|
<h3 id="updateTitle">Updates</h3>
|
||||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.13</p>
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.5</p>
|
||||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -502,7 +502,7 @@
|
|||||||
<div class="status-dot" id="statusDot"></div>
|
<div class="status-dot" id="statusDot"></div>
|
||||||
<span id="statusText">Nicht verbunden</span>
|
<span id="statusText">Nicht verbunden</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="versionText">v4.1.13</span>
|
<span id="versionText">v4.1.5</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -4,18 +4,12 @@ import * as fs from 'fs';
|
|||||||
import { spawn, ChildProcess, execSync, exec, spawnSync } from 'child_process';
|
import { spawn, ChildProcess, execSync, exec, spawnSync } from 'child_process';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import { compareUpdateVersions, isNewerUpdateVersion, normalizeUpdateVersion } from './update-version-utils';
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG & CONSTANTS
|
// CONFIG & CONSTANTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const APP_VERSION = app.getVersion();
|
const APP_VERSION = '4.1.5';
|
||||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||||
const GITEA_BASE_URL = (process.env.GITEA_BASE_URL || 'https://git.24-music.de').replace(/\/+$/, '');
|
|
||||||
const GITEA_REPO_OWNER = process.env.GITEA_REPO_OWNER || 'Administrator';
|
|
||||||
const GITEA_REPO_NAME = process.env.GITEA_REPO_NAME || 'Twitch-VOD-Manager';
|
|
||||||
const GITEA_RELEASES_API_LATEST_URL = `${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/releases/latest`;
|
|
||||||
const GITEA_RELEASES_DOWNLOAD_BASE_URL = `${GITEA_BASE_URL}/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/releases/download`;
|
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
|
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
|
||||||
@ -36,14 +30,6 @@ const MIN_FREE_DISK_BYTES = 128 * 1024 * 1024;
|
|||||||
const TOOL_PATH_REFRESH_TTL_MS = 10 * 1000;
|
const TOOL_PATH_REFRESH_TTL_MS = 10 * 1000;
|
||||||
const DEBUG_LOG_FLUSH_INTERVAL_MS = 1000;
|
const DEBUG_LOG_FLUSH_INTERVAL_MS = 1000;
|
||||||
const DEBUG_LOG_BUFFER_FLUSH_LINES = 48;
|
const DEBUG_LOG_BUFFER_FLUSH_LINES = 48;
|
||||||
const DEBUG_LOG_READ_TAIL_BYTES = 512 * 1024;
|
|
||||||
const DEBUG_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
||||||
const DEBUG_LOG_TRIM_TO_BYTES = 4 * 1024 * 1024;
|
|
||||||
const AUTO_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
||||||
const AUTO_UPDATE_STARTUP_CHECK_DELAY_MS = 5000;
|
|
||||||
const AUTO_UPDATE_MIN_CHECK_GAP_MS = 45 * 1000;
|
|
||||||
const AUTO_UPDATE_AUTO_DOWNLOAD = true;
|
|
||||||
const AUTO_UPDATE_CHECK_TIMEOUT_MS = 30 * 1000;
|
|
||||||
const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
||||||
const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096;
|
const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096;
|
||||||
const MAX_VOD_LIST_CACHE_ENTRIES = 512;
|
const MAX_VOD_LIST_CACHE_ENTRIES = 512;
|
||||||
@ -57,8 +43,6 @@ const TWITCH_WEB_CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
|
|||||||
|
|
||||||
type PerformanceMode = 'stability' | 'balanced' | 'speed';
|
type PerformanceMode = 'stability' | 'balanced' | 'speed';
|
||||||
type RetryErrorClass = 'network' | 'rate_limit' | 'auth' | 'tooling' | 'integrity' | 'io' | 'validation' | 'unknown';
|
type RetryErrorClass = 'network' | 'rate_limit' | 'auth' | 'tooling' | 'integrity' | 'io' | 'validation' | 'unknown';
|
||||||
type UpdateCheckSource = 'startup' | 'interval' | 'manual';
|
|
||||||
type UpdateDownloadSource = 'auto' | 'manual';
|
|
||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
if (!fs.existsSync(APPDATA_DIR)) {
|
if (!fs.existsSync(APPDATA_DIR)) {
|
||||||
@ -356,13 +340,10 @@ let mainWindow: BrowserWindow | null = null;
|
|||||||
let config = loadConfig();
|
let config = loadConfig();
|
||||||
let accessToken: string | null = null;
|
let accessToken: string | null = null;
|
||||||
let downloadQueue: QueueItem[] = loadQueue();
|
let downloadQueue: QueueItem[] = loadQueue();
|
||||||
let queueIdCounter = 0;
|
|
||||||
let lastQueueBroadcastFingerprint = '';
|
|
||||||
let isDownloading = false;
|
let isDownloading = false;
|
||||||
let currentProcess: ChildProcess | null = null;
|
let currentProcess: ChildProcess | null = null;
|
||||||
let currentDownloadCancelled = false;
|
let currentDownloadCancelled = false;
|
||||||
let pauseRequested = false;
|
let pauseRequested = false;
|
||||||
let activeQueueItemId: string | null = null;
|
|
||||||
let downloadStartTime = 0;
|
let downloadStartTime = 0;
|
||||||
let downloadedBytes = 0;
|
let downloadedBytes = 0;
|
||||||
const userIdLoginCache = new Map<string, string>();
|
const userIdLoginCache = new Map<string, string>();
|
||||||
@ -402,16 +383,6 @@ let bundledToolPathSignature = '';
|
|||||||
let bundledToolPathRefreshedAt = 0;
|
let bundledToolPathRefreshedAt = 0;
|
||||||
let debugLogFlushTimer: NodeJS.Timeout | null = null;
|
let debugLogFlushTimer: NodeJS.Timeout | null = null;
|
||||||
let pendingDebugLogLines: string[] = [];
|
let pendingDebugLogLines: string[] = [];
|
||||||
let autoUpdaterInitialized = false;
|
|
||||||
let autoUpdateCheckTimer: NodeJS.Timeout | null = null;
|
|
||||||
let autoUpdateStartupTimer: NodeJS.Timeout | null = null;
|
|
||||||
let autoUpdateCheckInProgress = false;
|
|
||||||
let autoUpdateReadyToInstall = false;
|
|
||||||
let autoUpdateDownloadInProgress = false;
|
|
||||||
let lastAutoUpdateCheckAt = 0;
|
|
||||||
let latestKnownUpdateVersion: string | null = null;
|
|
||||||
let downloadedUpdateVersion: string | null = null;
|
|
||||||
let twitchLoginInFlight: Promise<boolean> | null = null;
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// TOOL PATHS
|
// TOOL PATHS
|
||||||
@ -544,78 +515,11 @@ function flushPendingDebugLogLines(): void {
|
|||||||
const payload = pendingDebugLogLines.join('');
|
const payload = pendingDebugLogLines.join('');
|
||||||
pendingDebugLogLines = [];
|
pendingDebugLogLines = [];
|
||||||
fs.appendFileSync(DEBUG_LOG_FILE, payload);
|
fs.appendFileSync(DEBUG_LOG_FILE, payload);
|
||||||
trimDebugLogFileIfNeeded();
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore debug log errors
|
// ignore debug log errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimDebugLogFileIfNeeded(): void {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(DEBUG_LOG_FILE)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = fs.statSync(DEBUG_LOG_FILE);
|
|
||||||
if (stats.size <= DEBUG_LOG_MAX_BYTES) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bytesToKeep = Math.min(DEBUG_LOG_TRIM_TO_BYTES, stats.size);
|
|
||||||
const startOffset = Math.max(0, stats.size - bytesToKeep);
|
|
||||||
const buffer = Buffer.allocUnsafe(bytesToKeep);
|
|
||||||
|
|
||||||
let fileHandle: number | null = null;
|
|
||||||
try {
|
|
||||||
fileHandle = fs.openSync(DEBUG_LOG_FILE, 'r');
|
|
||||||
fs.readSync(fileHandle, buffer, 0, bytesToKeep, startOffset);
|
|
||||||
} finally {
|
|
||||||
if (fileHandle !== null) {
|
|
||||||
fs.closeSync(fileHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLineBreak = buffer.indexOf(0x0a);
|
|
||||||
const trimmed = firstLineBreak >= 0 && firstLineBreak + 1 < buffer.length
|
|
||||||
? buffer.subarray(firstLineBreak + 1)
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
fs.writeFileSync(DEBUG_LOG_FILE, trimmed);
|
|
||||||
} catch {
|
|
||||||
// ignore debug log errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDebugLogTailFromDisk(): string {
|
|
||||||
const stats = fs.statSync(DEBUG_LOG_FILE);
|
|
||||||
if (stats.size <= 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const bytesToRead = Math.min(stats.size, DEBUG_LOG_READ_TAIL_BYTES);
|
|
||||||
if (bytesToRead === stats.size) {
|
|
||||||
return fs.readFileSync(DEBUG_LOG_FILE, 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = Buffer.allocUnsafe(bytesToRead);
|
|
||||||
let fileHandle: number | null = null;
|
|
||||||
try {
|
|
||||||
fileHandle = fs.openSync(DEBUG_LOG_FILE, 'r');
|
|
||||||
fs.readSync(fileHandle, buffer, 0, bytesToRead, stats.size - bytesToRead);
|
|
||||||
} finally {
|
|
||||||
if (fileHandle !== null) {
|
|
||||||
fs.closeSync(fileHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLineBreak = buffer.indexOf(0x0a);
|
|
||||||
const slice = firstLineBreak >= 0 && firstLineBreak + 1 < buffer.length
|
|
||||||
? buffer.subarray(firstLineBreak + 1)
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
return slice.toString('utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDebugLogFlushTimer(): void {
|
function startDebugLogFlushTimer(): void {
|
||||||
if (debugLogFlushTimer) {
|
if (debugLogFlushTimer) {
|
||||||
return;
|
return;
|
||||||
@ -647,7 +551,7 @@ function readDebugLog(lines = 200): string {
|
|||||||
return 'Debug-Log ist leer.';
|
return 'Debug-Log ist leer.';
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = readDebugLogTailFromDisk();
|
const text = fs.readFileSync(DEBUG_LOG_FILE, 'utf-8');
|
||||||
const rows = text.split(/\r?\n/).filter(Boolean);
|
const rows = text.split(/\r?\n/).filter(Boolean);
|
||||||
return rows.slice(-lines).join('\n') || 'Debug-Log ist leer.';
|
return rows.slice(-lines).join('\n') || 'Debug-Log ist leer.';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1502,38 +1406,6 @@ function getQueueCounts(queueData: QueueItem[] = downloadQueue): RuntimeMetricsS
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateQueueItemId(): string {
|
|
||||||
queueIdCounter = (queueIdCounter + 1) % 1000;
|
|
||||||
return `${Date.now()}-${queueIdCounter}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueueBroadcastFingerprint(queueData: QueueItem[] = downloadQueue): string {
|
|
||||||
return queueData.map((item) => [
|
|
||||||
item.id,
|
|
||||||
item.status,
|
|
||||||
Math.round((Number(item.progress) || 0) * 10),
|
|
||||||
item.currentPart || 0,
|
|
||||||
item.totalParts || 0,
|
|
||||||
item.speed || '',
|
|
||||||
item.eta || '',
|
|
||||||
item.last_error || ''
|
|
||||||
].join(':')).join('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitQueueUpdated(force = false): void {
|
|
||||||
const nextFingerprint = getQueueBroadcastFingerprint(downloadQueue);
|
|
||||||
if (!force && nextFingerprint === lastQueueBroadcastFingerprint) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastQueueBroadcastFingerprint = nextFingerprint;
|
|
||||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasQueueItemId(id: string): boolean {
|
|
||||||
return downloadQueue.some((item) => item.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRuntimeMetricsSnapshot(): RuntimeMetricsSnapshot {
|
function getRuntimeMetricsSnapshot(): RuntimeMetricsSnapshot {
|
||||||
return {
|
return {
|
||||||
...runtimeMetrics,
|
...runtimeMetrics,
|
||||||
@ -1731,22 +1603,6 @@ async function twitchLogin(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestTwitchLogin(): Promise<boolean> {
|
|
||||||
if (twitchLoginInFlight) {
|
|
||||||
return twitchLoginInFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
let loginPromise: Promise<boolean>;
|
|
||||||
loginPromise = twitchLogin().finally(() => {
|
|
||||||
if (twitchLoginInFlight === loginPromise) {
|
|
||||||
twitchLoginInFlight = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
twitchLoginInFlight = loginPromise;
|
|
||||||
return loginPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureTwitchAuth(forceRefresh = false): Promise<boolean> {
|
async function ensureTwitchAuth(forceRefresh = false): Promise<boolean> {
|
||||||
if (!config.client_id || !config.client_secret) {
|
if (!config.client_id || !config.client_secret) {
|
||||||
accessToken = null;
|
accessToken = null;
|
||||||
@ -1757,7 +1613,7 @@ async function ensureTwitchAuth(forceRefresh = false): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await requestTwitchLogin();
|
return await twitchLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLogin(input: string): string {
|
function normalizeLogin(input: string): string {
|
||||||
@ -2768,7 +2624,7 @@ async function processQueue(): Promise<void> {
|
|||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
pauseRequested = false;
|
pauseRequested = false;
|
||||||
mainWindow?.webContents.send('download-started');
|
mainWindow?.webContents.send('download-started');
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
while (isDownloading && !pauseRequested) {
|
while (isDownloading && !pauseRequested) {
|
||||||
const item = pickNextPendingQueueItem();
|
const item = pickNextPendingQueueItem();
|
||||||
@ -2786,12 +2642,11 @@ async function processQueue(): Promise<void> {
|
|||||||
runtimeMetrics.downloadsStarted += 1;
|
runtimeMetrics.downloadsStarted += 1;
|
||||||
runtimeMetrics.activeItemId = item.id;
|
runtimeMetrics.activeItemId = item.id;
|
||||||
runtimeMetrics.activeItemTitle = item.title;
|
runtimeMetrics.activeItemTitle = item.title;
|
||||||
activeQueueItemId = item.id;
|
|
||||||
|
|
||||||
currentDownloadCancelled = false;
|
currentDownloadCancelled = false;
|
||||||
item.status = 'downloading';
|
item.status = 'downloading';
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
item.last_error = '';
|
item.last_error = '';
|
||||||
|
|
||||||
@ -2845,21 +2700,13 @@ async function processQueue(): Promise<void> {
|
|||||||
totalParts: item.totalParts
|
totalParts: item.totalParts
|
||||||
} as DownloadProgress);
|
} as DownloadProgress);
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
await sleep(retryDelaySeconds * 1000);
|
await sleep(retryDelaySeconds * 1000);
|
||||||
} else {
|
} else {
|
||||||
runtimeMetrics.retriesExhausted += 1;
|
runtimeMetrics.retriesExhausted += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasQueueItemId(item.id)) {
|
|
||||||
appendDebugLog('queue-item-finished-removed', { itemId: item.id });
|
|
||||||
runtimeMetrics.activeItemId = null;
|
|
||||||
runtimeMetrics.activeItemTitle = null;
|
|
||||||
activeQueueItemId = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
|
const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
|
||||||
item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
|
item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
|
||||||
item.progress = finalResult.success ? 100 : item.progress;
|
item.progress = finalResult.success ? 100 : item.progress;
|
||||||
@ -2873,7 +2720,6 @@ async function processQueue(): Promise<void> {
|
|||||||
|
|
||||||
runtimeMetrics.activeItemId = null;
|
runtimeMetrics.activeItemId = null;
|
||||||
runtimeMetrics.activeItemTitle = null;
|
runtimeMetrics.activeItemTitle = null;
|
||||||
activeQueueItemId = null;
|
|
||||||
|
|
||||||
appendDebugLog('queue-item-finished', {
|
appendDebugLog('queue-item-finished', {
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
@ -2882,17 +2728,16 @@ async function processQueue(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
pauseRequested = false;
|
pauseRequested = false;
|
||||||
runtimeMetrics.activeItemId = null;
|
runtimeMetrics.activeItemId = null;
|
||||||
runtimeMetrics.activeItemTitle = null;
|
runtimeMetrics.activeItemTitle = null;
|
||||||
activeQueueItemId = null;
|
|
||||||
|
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
mainWindow?.webContents.send('download-finished');
|
mainWindow?.webContents.send('download-finished');
|
||||||
appendDebugLog('queue-finished', { items: downloadQueue.length });
|
appendDebugLog('queue-finished', { items: downloadQueue.length });
|
||||||
}
|
}
|
||||||
@ -2924,19 +2769,6 @@ function createWindow(): void {
|
|||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../src/index.html'));
|
mainWindow.loadFile(path.join(__dirname, '../src/index.html'));
|
||||||
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
|
||||||
emitQueueUpdated(true);
|
|
||||||
if (isDownloading) {
|
|
||||||
mainWindow?.webContents.send('download-started');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoUpdateReadyToInstall && downloadedUpdateVersion) {
|
|
||||||
mainWindow?.webContents.send('update-downloaded', {
|
|
||||||
version: downloadedUpdateVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
@ -2950,198 +2782,26 @@ function createWindow(): void {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// AUTO-UPDATER (electron-updater)
|
// AUTO-UPDATER (electron-updater)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
function hasNewerKnownUpdateThanDownloaded(): boolean {
|
|
||||||
if (!latestKnownUpdateVersion || !downloadedUpdateVersion) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNewerUpdateVersion(latestKnownUpdateVersion, downloadedUpdateVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestUpdateCheck(source: UpdateCheckSource, force = false): Promise<{ started: boolean; reason?: string }> {
|
|
||||||
if (autoUpdateCheckInProgress) {
|
|
||||||
return { started: false, reason: 'in-progress' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (!force && lastAutoUpdateCheckAt > 0 && (now - lastAutoUpdateCheckAt) < AUTO_UPDATE_MIN_CHECK_GAP_MS) {
|
|
||||||
return { started: false, reason: 'throttled' };
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateCheckInProgress = true;
|
|
||||||
lastAutoUpdateCheckAt = now;
|
|
||||||
appendDebugLog('update-check-start', { source });
|
|
||||||
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
const giteaRes = await axios.get(GITEA_RELEASES_API_LATEST_URL, {
|
|
||||||
timeout: 5000,
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'User-Agent': 'Twitch-VOD-Manager'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const tagName = giteaRes.data?.tag_name;
|
|
||||||
if (tagName) {
|
|
||||||
autoUpdater.setFeedURL({
|
|
||||||
provider: 'generic',
|
|
||||||
url: `${GITEA_RELEASES_DOWNLOAD_BASE_URL}/${tagName}`
|
|
||||||
});
|
|
||||||
appendDebugLog('gitea-feed-url-set', { tagName, owner: GITEA_REPO_OWNER, repo: GITEA_REPO_NAME });
|
|
||||||
}
|
|
||||||
} catch (apiErr) {
|
|
||||||
appendDebugLog('gitea-api-failed', String(apiErr));
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeoutHandle: NodeJS.Timeout | null = null;
|
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
autoUpdater.checkForUpdates(),
|
|
||||||
new Promise<never>((_, reject) => {
|
|
||||||
timeoutHandle = setTimeout(() => {
|
|
||||||
reject(new Error(`Update check timed out after ${AUTO_UPDATE_CHECK_TIMEOUT_MS}ms`));
|
|
||||||
}, AUTO_UPDATE_CHECK_TIMEOUT_MS);
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
if (timeoutHandle) {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
timeoutHandle = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { started: true };
|
|
||||||
} catch (err) {
|
|
||||||
appendDebugLog('update-check-failed', { source, error: String(err) });
|
|
||||||
console.error('Update check failed:', err);
|
|
||||||
return { started: false, reason: 'error' };
|
|
||||||
} finally {
|
|
||||||
autoUpdateCheckInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestUpdateDownload(source: UpdateDownloadSource): Promise<{ started: boolean; reason?: string }> {
|
|
||||||
if (autoUpdateReadyToInstall && !hasNewerKnownUpdateThanDownloaded()) {
|
|
||||||
return { started: false, reason: 'ready-to-install' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoUpdateDownloadInProgress) {
|
|
||||||
return { started: false, reason: 'in-progress' };
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateDownloadInProgress = true;
|
|
||||||
appendDebugLog('update-download-start', { source });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await autoUpdater.downloadUpdate();
|
|
||||||
return { started: true };
|
|
||||||
} catch (err) {
|
|
||||||
appendDebugLog('update-download-failed', { source, error: String(err) });
|
|
||||||
console.error('Download failed:', err);
|
|
||||||
return { started: false, reason: 'error' };
|
|
||||||
} finally {
|
|
||||||
autoUpdateDownloadInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAutoUpdatePolling(): void {
|
|
||||||
if (autoUpdateCheckTimer) {
|
|
||||||
clearInterval(autoUpdateCheckTimer);
|
|
||||||
autoUpdateCheckTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoUpdateStartupTimer) {
|
|
||||||
clearTimeout(autoUpdateStartupTimer);
|
|
||||||
autoUpdateStartupTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAutoUpdatePolling(): void {
|
|
||||||
if (!autoUpdateCheckTimer) {
|
|
||||||
autoUpdateCheckTimer = setInterval(() => {
|
|
||||||
void requestUpdateCheck('interval');
|
|
||||||
}, AUTO_UPDATE_CHECK_INTERVAL_MS);
|
|
||||||
|
|
||||||
autoUpdateCheckTimer.unref?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoUpdateStartupTimer) {
|
|
||||||
clearTimeout(autoUpdateStartupTimer);
|
|
||||||
autoUpdateStartupTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateStartupTimer = setTimeout(() => {
|
|
||||||
autoUpdateStartupTimer = null;
|
|
||||||
void requestUpdateCheck('startup', true);
|
|
||||||
}, AUTO_UPDATE_STARTUP_CHECK_DELAY_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupAutoUpdater() {
|
function setupAutoUpdater() {
|
||||||
if (autoUpdaterInitialized) {
|
|
||||||
startAutoUpdatePolling();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdaterInitialized = true;
|
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
autoUpdater.on('checking-for-update', () => {
|
autoUpdater.on('checking-for-update', () => {
|
||||||
console.log('Checking for updates...');
|
console.log('Checking for updates...');
|
||||||
mainWindow?.webContents.send('update-checking');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info) => {
|
autoUpdater.on('update-available', (info) => {
|
||||||
const incomingVersion = normalizeUpdateVersion(info.version);
|
console.log('Update available:', info.version);
|
||||||
const displayVersion = incomingVersion || info.version;
|
|
||||||
|
|
||||||
if (latestKnownUpdateVersion && compareUpdateVersions(incomingVersion, latestKnownUpdateVersion) < 0) {
|
|
||||||
appendDebugLog('update-available-ignored-older', {
|
|
||||||
incomingVersion: displayVersion,
|
|
||||||
knownVersion: latestKnownUpdateVersion
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
latestKnownUpdateVersion = incomingVersion || latestKnownUpdateVersion;
|
|
||||||
|
|
||||||
const hasAlreadyDownloadedThisVersion = Boolean(
|
|
||||||
autoUpdateReadyToInstall &&
|
|
||||||
downloadedUpdateVersion &&
|
|
||||||
compareUpdateVersions(downloadedUpdateVersion, incomingVersion) === 0
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Update available:', displayVersion);
|
|
||||||
if (!hasAlreadyDownloadedThisVersion) {
|
|
||||||
autoUpdateReadyToInstall = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateDownloadInProgress = false;
|
|
||||||
|
|
||||||
if (hasAlreadyDownloadedThisVersion) {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('update-downloaded', {
|
|
||||||
version: displayVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('update-available', {
|
mainWindow.webContents.send('update-available', {
|
||||||
version: displayVersion,
|
version: info.version,
|
||||||
releaseDate: info.releaseDate
|
releaseDate: info.releaseDate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AUTO_UPDATE_AUTO_DOWNLOAD) {
|
|
||||||
void requestUpdateDownload('auto');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-not-available', () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
console.log('No updates available');
|
console.log('No updates available');
|
||||||
mainWindow?.webContents.send('update-not-available');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('download-progress', (progress) => {
|
autoUpdater.on('download-progress', (progress) => {
|
||||||
@ -3157,31 +2817,22 @@ function setupAutoUpdater() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info) => {
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
const downloadedVersion = normalizeUpdateVersion(info.version) || info.version;
|
console.log('Update downloaded:', info.version);
|
||||||
console.log('Update downloaded:', downloadedVersion);
|
|
||||||
autoUpdateReadyToInstall = true;
|
|
||||||
autoUpdateDownloadInProgress = false;
|
|
||||||
downloadedUpdateVersion = downloadedVersion;
|
|
||||||
if (!latestKnownUpdateVersion || compareUpdateVersions(downloadedVersion, latestKnownUpdateVersion) > 0) {
|
|
||||||
latestKnownUpdateVersion = downloadedVersion;
|
|
||||||
}
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('update-downloaded', {
|
mainWindow.webContents.send('update-downloaded', {
|
||||||
version: downloadedVersion
|
version: info.version
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('error', (err) => {
|
autoUpdater.on('error', (err) => {
|
||||||
autoUpdateCheckInProgress = false;
|
|
||||||
autoUpdateDownloadInProgress = false;
|
|
||||||
const message = String(err);
|
|
||||||
appendDebugLog('auto-updater-error', message);
|
|
||||||
mainWindow?.webContents.send('update-error', { message });
|
|
||||||
console.error('Auto-updater error:', err);
|
console.error('Auto-updater error:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
startAutoUpdatePolling();
|
// Check for updates
|
||||||
|
autoUpdater.checkForUpdates().catch(err => {
|
||||||
|
console.error('Update check failed:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -3198,7 +2849,6 @@ ipcMain.handle('save-config', (_, newConfig: Partial<Config>) => {
|
|||||||
|
|
||||||
if (config.client_id !== previousClientId || config.client_secret !== previousClientSecret) {
|
if (config.client_id !== previousClientId || config.client_secret !== previousClientSecret) {
|
||||||
accessToken = null;
|
accessToken = null;
|
||||||
twitchLoginInFlight = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.metadata_cache_minutes !== previousCacheMinutes) {
|
if (config.metadata_cache_minutes !== previousCacheMinutes) {
|
||||||
@ -3241,40 +2891,27 @@ ipcMain.handle('add-to-queue', (_, item: Omit<QueueItem, 'id' | 'status' | 'prog
|
|||||||
|
|
||||||
const queueItem: QueueItem = {
|
const queueItem: QueueItem = {
|
||||||
...item,
|
...item,
|
||||||
id: generateQueueItemId(),
|
id: Date.now().toString(),
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
progress: 0
|
progress: 0
|
||||||
};
|
};
|
||||||
downloadQueue.push(queueItem);
|
downloadQueue.push(queueItem);
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
return downloadQueue;
|
return downloadQueue;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('remove-from-queue', (_, id: string) => {
|
ipcMain.handle('remove-from-queue', (_, id: string) => {
|
||||||
const wasActiveItem = activeQueueItemId === id;
|
|
||||||
|
|
||||||
if (wasActiveItem) {
|
|
||||||
currentDownloadCancelled = true;
|
|
||||||
if (currentProcess) {
|
|
||||||
currentProcess.kill();
|
|
||||||
}
|
|
||||||
activeQueueItemId = null;
|
|
||||||
runtimeMetrics.activeItemId = null;
|
|
||||||
runtimeMetrics.activeItemTitle = null;
|
|
||||||
appendDebugLog('queue-item-removed-active-cancelled', { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadQueue = downloadQueue.filter(item => item.id !== id);
|
downloadQueue = downloadQueue.filter(item => item.id !== id);
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
return downloadQueue;
|
return downloadQueue;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('clear-completed', () => {
|
ipcMain.handle('clear-completed', () => {
|
||||||
downloadQueue = downloadQueue.filter(item => item.status !== 'completed');
|
downloadQueue = downloadQueue.filter(item => item.status !== 'completed');
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
return downloadQueue;
|
return downloadQueue;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3288,7 +2925,7 @@ ipcMain.handle('reorder-queue', (_, orderIds: string[]) => {
|
|||||||
|
|
||||||
downloadQueue = withOrder;
|
downloadQueue = withOrder;
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
return downloadQueue;
|
return downloadQueue;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3305,7 +2942,7 @@ ipcMain.handle('retry-failed-downloads', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
void processQueue();
|
void processQueue();
|
||||||
@ -3319,12 +2956,12 @@ ipcMain.handle('start-download', async () => {
|
|||||||
|
|
||||||
const hasPendingItems = downloadQueue.some(item => item.status === 'pending');
|
const hasPendingItems = downloadQueue.some(item => item.status === 'pending');
|
||||||
if (!hasPendingItems) {
|
if (!hasPendingItems) {
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveQueue(downloadQueue);
|
saveQueue(downloadQueue);
|
||||||
emitQueueUpdated();
|
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||||
|
|
||||||
processQueue();
|
processQueue();
|
||||||
return true;
|
return true;
|
||||||
@ -3378,15 +3015,8 @@ ipcMain.handle('get-version', () => APP_VERSION);
|
|||||||
|
|
||||||
ipcMain.handle('check-update', async () => {
|
ipcMain.handle('check-update', async () => {
|
||||||
try {
|
try {
|
||||||
setupAutoUpdater();
|
const result = await autoUpdater.checkForUpdates();
|
||||||
const result = await requestUpdateCheck('manual', true);
|
return { checking: true };
|
||||||
if (result.reason === 'error') {
|
|
||||||
return { error: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.started
|
|
||||||
? { checking: true }
|
|
||||||
: { checking: true, skipped: result.reason };
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Update check failed:', err);
|
console.error('Update check failed:', err);
|
||||||
return { error: true };
|
return { error: true };
|
||||||
@ -3395,15 +3025,8 @@ ipcMain.handle('check-update', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('download-update', async () => {
|
ipcMain.handle('download-update', async () => {
|
||||||
try {
|
try {
|
||||||
setupAutoUpdater();
|
await autoUpdater.downloadUpdate();
|
||||||
const result = await requestUpdateDownload('manual');
|
return { downloading: true };
|
||||||
if (result.reason === 'error') {
|
|
||||||
return { error: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.started
|
|
||||||
? { downloading: true }
|
|
||||||
: { downloading: true, skipped: result.reason };
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed:', err);
|
console.error('Download failed:', err);
|
||||||
return { error: true };
|
return { error: true };
|
||||||
@ -3575,7 +3198,6 @@ app.on('window-all-closed', () => {
|
|||||||
stopMetadataCacheCleanup();
|
stopMetadataCacheCleanup();
|
||||||
cleanupMetadataCaches('shutdown');
|
cleanupMetadataCaches('shutdown');
|
||||||
stopDebugLogFlushTimer(true);
|
stopDebugLogFlushTimer(true);
|
||||||
stopAutoUpdatePolling();
|
|
||||||
|
|
||||||
if (currentProcess) {
|
if (currentProcess) {
|
||||||
currentProcess.kill();
|
currentProcess.kill();
|
||||||
@ -3591,6 +3213,5 @@ app.on('before-quit', () => {
|
|||||||
stopMetadataCacheCleanup();
|
stopMetadataCacheCleanup();
|
||||||
cleanupMetadataCaches('shutdown');
|
cleanupMetadataCaches('shutdown');
|
||||||
stopDebugLogFlushTimer(true);
|
stopDebugLogFlushTimer(true);
|
||||||
stopAutoUpdatePolling();
|
|
||||||
flushQueueSave();
|
flushQueueSave();
|
||||||
});
|
});
|
||||||
@ -165,22 +165,13 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Auto-Update Events
|
// Auto-Update Events
|
||||||
onUpdateChecking: (callback: () => void) => {
|
|
||||||
ipcRenderer.on('update-checking', () => callback());
|
|
||||||
},
|
|
||||||
onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string }) => void) => {
|
onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string }) => void) => {
|
||||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
onUpdateNotAvailable: (callback: () => void) => {
|
|
||||||
ipcRenderer.on('update-not-available', () => callback());
|
|
||||||
},
|
|
||||||
onUpdateDownloadProgress: (callback: (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => void) => {
|
onUpdateDownloadProgress: (callback: (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => void) => {
|
||||||
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
|
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
|
||||||
},
|
},
|
||||||
onUpdateDownloaded: (callback: (info: { version: string }) => void) => {
|
onUpdateDownloaded: (callback: (info: { version: string }) => void) => {
|
||||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||||
},
|
|
||||||
onUpdateError: (callback: (payload: { message: string }) => void) => {
|
|
||||||
ipcRenderer.on('update-error', (_, payload) => callback(payload));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -178,8 +178,8 @@ interface ApiBridge {
|
|||||||
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;
|
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;
|
||||||
mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>;
|
mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>;
|
||||||
getVersion(): Promise<string>;
|
getVersion(): Promise<string>;
|
||||||
checkUpdate(): Promise<{ checking?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'throttled' | 'error' | string }>;
|
checkUpdate(): Promise<{ checking?: boolean; error?: boolean }>;
|
||||||
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'error' | string }>;
|
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>;
|
||||||
installUpdate(): Promise<void>;
|
installUpdate(): Promise<void>;
|
||||||
openExternal(url: string): Promise<void>;
|
openExternal(url: string): Promise<void>;
|
||||||
runPreflight(autoFix: boolean): Promise<PreflightResult>;
|
runPreflight(autoFix: boolean): Promise<PreflightResult>;
|
||||||
@ -193,12 +193,9 @@ interface ApiBridge {
|
|||||||
onDownloadFinished(callback: () => void): void;
|
onDownloadFinished(callback: () => void): void;
|
||||||
onCutProgress(callback: (percent: number) => void): void;
|
onCutProgress(callback: (percent: number) => void): void;
|
||||||
onMergeProgress(callback: (percent: number) => void): void;
|
onMergeProgress(callback: (percent: number) => void): void;
|
||||||
onUpdateChecking(callback: () => void): void;
|
|
||||||
onUpdateAvailable(callback: (info: UpdateInfo) => void): void;
|
onUpdateAvailable(callback: (info: UpdateInfo) => void): void;
|
||||||
onUpdateNotAvailable(callback: () => void): void;
|
|
||||||
onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void;
|
onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void;
|
||||||
onUpdateDownloaded(callback: (info: UpdateInfo) => void): void;
|
onUpdateDownloaded(callback: (info: UpdateInfo) => void): void;
|
||||||
onUpdateError(callback: (payload: { message: string }) => void): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -197,13 +197,7 @@ const UI_TEXT_DE = {
|
|||||||
updates: {
|
updates: {
|
||||||
bannerDefault: 'Neue Version verfugbar!',
|
bannerDefault: 'Neue Version verfugbar!',
|
||||||
latest: 'Du hast die neueste Version!',
|
latest: 'Du hast die neueste Version!',
|
||||||
checking: 'Suche nach Updates...',
|
|
||||||
checkInProgress: 'Update-Prufung lauft bereits.',
|
|
||||||
readyToInstall: 'Update ist bereit zur Installation.',
|
|
||||||
checkFailed: 'Update-Prufung fehlgeschlagen.',
|
|
||||||
downloading: 'Wird heruntergeladen...',
|
downloading: 'Wird heruntergeladen...',
|
||||||
downloadInProgress: 'Update-Download lauft bereits.',
|
|
||||||
downloadFailed: 'Update-Download fehlgeschlagen.',
|
|
||||||
available: 'verfugbar!',
|
available: 'verfugbar!',
|
||||||
downloadNow: 'Jetzt herunterladen',
|
downloadNow: 'Jetzt herunterladen',
|
||||||
downloadLabel: 'Download',
|
downloadLabel: 'Download',
|
||||||
@ -197,13 +197,7 @@ const UI_TEXT_EN = {
|
|||||||
updates: {
|
updates: {
|
||||||
bannerDefault: 'New version available!',
|
bannerDefault: 'New version available!',
|
||||||
latest: 'You are on the latest version!',
|
latest: 'You are on the latest version!',
|
||||||
checking: 'Checking for updates...',
|
|
||||||
checkInProgress: 'Update check is already running.',
|
|
||||||
readyToInstall: 'Update is ready to install.',
|
|
||||||
checkFailed: 'Update check failed.',
|
|
||||||
downloading: 'Downloading...',
|
downloading: 'Downloading...',
|
||||||
downloadInProgress: 'Update download is already running.',
|
|
||||||
downloadFailed: 'Update download failed.',
|
|
||||||
available: 'available!',
|
available: 'available!',
|
||||||
downloadNow: 'Download now',
|
downloadNow: 'Download now',
|
||||||
downloadLabel: 'Download',
|
downloadLabel: 'Download',
|
||||||
@ -1,13 +1,4 @@
|
|||||||
let lastRuntimeMetricsOutput = '';
|
let lastRuntimeMetricsOutput = '';
|
||||||
let lastDebugLogOutput = '';
|
|
||||||
|
|
||||||
function canRunSettingsAutoRefresh(): boolean {
|
|
||||||
if (document.hidden) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return document.querySelector('.tab-content.active')?.id === 'settingsTab';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connect(): Promise<void> {
|
async function connect(): Promise<void> {
|
||||||
const hasCredentials = Boolean((config.client_id ?? '').toString().trim() && (config.client_secret ?? '').toString().trim());
|
const hasCredentials = Boolean((config.client_id ?? '').toString().trim() && (config.client_secret ?? '').toString().trim());
|
||||||
@ -151,10 +142,6 @@ function toggleRuntimeMetricsAutoRefresh(enabled: boolean): void {
|
|||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
runtimeMetricsAutoRefreshTimer = window.setInterval(() => {
|
runtimeMetricsAutoRefreshTimer = window.setInterval(() => {
|
||||||
if (!canRunSettingsAutoRefresh()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void refreshRuntimeMetrics(false);
|
void refreshRuntimeMetrics(false);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@ -258,16 +245,8 @@ async function runPreflight(autoFix = false): Promise<void> {
|
|||||||
async function refreshDebugLog(): Promise<void> {
|
async function refreshDebugLog(): Promise<void> {
|
||||||
const text = await window.api.getDebugLog(250);
|
const text = await window.api.getDebugLog(250);
|
||||||
const panel = byId('debugLogOutput');
|
const panel = byId('debugLogOutput');
|
||||||
const keepAtBottom = (panel.scrollHeight - panel.scrollTop - panel.clientHeight) < 20;
|
panel.textContent = text;
|
||||||
|
panel.scrollTop = panel.scrollHeight;
|
||||||
if (text !== lastDebugLogOutput) {
|
|
||||||
panel.textContent = text;
|
|
||||||
lastDebugLogOutput = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keepAtBottom) {
|
|
||||||
panel.scrollTop = panel.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDebugAutoRefresh(enabled: boolean): void {
|
function toggleDebugAutoRefresh(enabled: boolean): void {
|
||||||
@ -278,12 +257,8 @@ function toggleDebugAutoRefresh(enabled: boolean): void {
|
|||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
debugLogAutoRefreshTimer = window.setInterval(() => {
|
debugLogAutoRefreshTimer = window.setInterval(() => {
|
||||||
if (!canRunSettingsAutoRefresh()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void refreshDebugLog();
|
void refreshDebugLog();
|
||||||
}, 2000);
|
}, 1500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
54
typescript-version/src/renderer-updates.ts
Normal file
54
typescript-version/src/renderer-updates.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
async function checkUpdateSilent(): Promise<void> {
|
||||||
|
await window.api.checkUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkUpdate(): Promise<void> {
|
||||||
|
await window.api.checkUpdate();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (byId('updateBanner').style.display !== 'flex') {
|
||||||
|
alert(UI_TEXT.updates.latest);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadUpdate(): void {
|
||||||
|
if (updateReady) {
|
||||||
|
void window.api.installUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byId('updateButton').textContent = UI_TEXT.updates.downloading;
|
||||||
|
byId('updateButton').disabled = true;
|
||||||
|
byId('updateProgress').style.display = 'block';
|
||||||
|
byId('updateProgressBar').classList.add('downloading');
|
||||||
|
void window.api.downloadUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.api.onUpdateAvailable((info: UpdateInfo) => {
|
||||||
|
byId('updateBanner').style.display = 'flex';
|
||||||
|
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`;
|
||||||
|
byId('updateButton').textContent = UI_TEXT.updates.downloadNow;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
||||||
|
const bar = byId('updateProgressBar');
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = progress.percent + '%';
|
||||||
|
|
||||||
|
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
||||||
|
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
||||||
|
byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||||
|
updateReady = true;
|
||||||
|
|
||||||
|
const bar = byId('updateProgressBar');
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = '100%';
|
||||||
|
|
||||||
|
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.ready}`;
|
||||||
|
byId('updateButton').textContent = UI_TEXT.updates.installNow;
|
||||||
|
byId('updateButton').disabled = false;
|
||||||
|
});
|
||||||
@ -10,8 +10,6 @@ async function init(): Promise<void> {
|
|||||||
config.language = language;
|
config.language = language;
|
||||||
const initialQueue = await window.api.getQueue();
|
const initialQueue = await window.api.getQueue();
|
||||||
queue = Array.isArray(initialQueue) ? initialQueue : [];
|
queue = Array.isArray(initialQueue) ? initialQueue : [];
|
||||||
downloading = await window.api.isDownloading();
|
|
||||||
markQueueActivity();
|
|
||||||
const version = await window.api.getVersion();
|
const version = await window.api.getVersion();
|
||||||
|
|
||||||
byId('versionText').textContent = `v${version}`;
|
byId('versionText').textContent = `v${version}`;
|
||||||
Loading…
Reference in New Issue
Block a user