- `installer/installer.nsh`: customHeader macro sets Caption to
"Setup - ${PRODUCT_NAME} ${VERSION}" so users can see which release
they're installing from the window title (the default $(^SetupCaption)
omits the version, and re-setting Name trips NSIS warning 6029 which
electron-builder's -WX flag treats as a hard error).
- `scripts/build.ts`: afterAllArtifactBuild hook runs the app-builder
rcedit helper on the generated NSIS installer to rewrite its
FileDescription to "${APP_NAME} installer". electron-builder's
NsisTarget.computeVersionKey() hardcodes VIAddVersionKey /LANG=1033
"FileDescription" "${appInfo.description}", binding the installer's
FileDescription to the app binary's (both drawn from package.json
description); any in-NSIS override collides on the same LANG+key
with a hard "already defined!" error that -WX does not gate.
Post-processing with rcedit sidesteps this and lets installer and
app binary carry distinct descriptions — same approach VS Code's
Inno Setup pipeline uses (where "{AppName} Setup" is the default).
Closes#637
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Go Core already broadcast a `download-create` SSE event whenever
`/api/downloads` POST lands (UI, browser extension, Docker clients —
any route). The UI listened, but only to bump the sidebar badge via
`useDownloadStore.increase()`. The SWR list fetched by `useTasks`
stayed stale until the user refreshed manually, so tasks imported
through the extension's desktop-http / docker-http modes never
appeared without a page reload.
Fan the same SSE event through the existing `dispatchDownload`
pipeline so `useTasks` can mutate its SWR cache alongside the other
lifecycle events (success / failed / stopped).
- `packages/shared/common/src/types/index.ts`: new
`DownloadCreatedEvent` (`type: "created"`, carries `{ ids, count }`).
- `apps/ui/src/api/events.ts`: existing `download-create` handler now
additionally calls `dispatchDownload({ type: "created", ... })`
after its badge-increment side effect. Parses `ids` from the
payload for downstream filtering if anyone needs it later.
- `apps/ui/src/hooks/use-tasks.ts`: adds `isCreatedEvent` type guard
and calls `mutate()` on hit, same pattern as the other events.
Internal task creation (UI form submit) is untouched — it still
refreshes via its own local mutate. The SSE path only matters for
tasks that show up out-of-band.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two separate bugs had to be fixed together before copy worked:
1. `terminal-dialog.tsx` used to swallow every contextmenu event
inside the dialog (`e.preventDefault(); e.stopPropagation()`) —
killing Electron's built-in "Copy" menu for selected text in the
xterm log. Drop the preventDefault so the native menu fires, but
keep stopPropagation (now scoped to the contextmenu handler
alone) so the event doesn't bubble up through the React tree to
`DownloadTaskItem`'s `onContextMenu` and pop its "select /
download / refresh / delete" menu instead. Radix Dialog portals
the DOM to document.body, but React synthetic events still
travel the virtual tree — this is the classic portal-bubbling
gotcha.
2. xterm.js does not bind Ctrl+C / Cmd+C to "copy selection" by
default; it forwards them as control chars. Since the log view
uses `disableStdin: true`, hijacking those keys is safe.
`attachCustomKeyEventHandler` now intercepts copy shortcuts,
writes the selection to the clipboard via `navigator.clipboard`,
and returns false so xterm stops handling the event. No selection
→ passthrough.
Result: both right-click → Copy and Ctrl/Cmd+C paths work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Twitter-style page titles like "(2) 主页 / X" land a literal "/" in
the download filename. Gopeed used to strip such characters silently;
aria2 is strict and passes them through to the OS, which then reads
"/" as a path separator — the save ends up at
`.../(2) 主页 /X-....mp4` pointing at a non-existent sub-directory
and fails with `ERROR_PATH_NOT_FOUND (errNum=3)`.
Sanitize once, at the task-creation boundary, so the DB row, the
downloader command-line `-o` arg, and the post-download
`CheckFileExists(rec.Name, ...)` probe all agree on the same
filesystem-safe value.
- New exported `core.SanitizeFilename` replaces reserved path /
wildcard characters (`\ / : * ? " < > |`) and ASCII control chars
with `_`, and right-trims dots / spaces (Windows strips those
silently, producing a filename that doesn't match the DB row).
Falls back to "download" if every character was illegal.
- `service/download_task.go` `AddDownloadTask` and `AddDownloadTasks`
run titles through `SanitizeFilename` before the
`FindByName` de-duplication check, so both the dedup lookup and
the persisted row see the cleaned value.
- `core/downloader.go` `buildArgs` still calls `SanitizeFilename`
defensively on `p.Name` — cheap, and guards any future path that
bypasses the service layer.
Applies to every downloader (aria2, yt-dlp, BBDown, N_m3u8DL-RE,
mediago) since the fix is in the shared `name` arg-building branch
used by all Schema entries. Pre-existing broken tasks in the DB will
still fail "file not found" on the UI and need to be deleted +
re-created.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gopeed's last release is aging and its SChannel-linked Windows build
chokes on modern CDN TLS handshakes (we hit SEC_I_MESSAGE_FRAGMENT on
twimg.com). Replace it with aria2 — same short flags (-x/-s/-k carry
over verbatim), stricter arg handling, and well-trodden cross-platform
builds.
Binaries are vendored in-tree at extra/aria2/<os>/<arch>/ rather than
pulled from a single GitHub release, because aria2 static builds for
Linux / macOS / Windows come from different upstream repos. To keep
the deps pipeline unified:
- `scripts/download-deps.ts` grows a "source": "local" branch that
copies from `extra/<tool>/<os>/<arch>/` into `.deps/<os>-<arch>/`
instead of fetching from GitHub. Resulting layout matches the rest
of the tools, so binaryResolver.ts needs no changes.
- `scripts/deps-versions.json`: `gopeed` entry removed, `aria2`
entry added with source:"local" + path:"extra/aria2".
- `apps/core/internal/core/types.go`: BinaryNames[TypeDirect]
`"gopeed"` → `"aria2c"`.
- `apps/core/internal/core/schema/loader.go`: direct schema's Args
map to aria2's flags (-d / -o, plus --console-log-level=notice /
--summary-interval=1 / --allow-overwrite=true /
--auto-file-renaming=false for parseable output and predictable
rerun behaviour). ConsoleReg regexes updated to aria2's summary
line format (e.g. "DL:512KiB" for speed, "(83%)" for percent).
`--check-certificate=false` tacked on as a workaround for the
SChannel handshake issue on the bundled 1.19.0 Windows build —
proper fix is upgrading the vendored binary to a build that links
against OpenSSL (e.g. 1.37.0), comment calls that out.
- `apps/core/.env`, `Dockerfile`, `apps/electron/scripts/build.ts`,
`apps/core/README.md`: gopeed → aria2c / aria2 references.
No DB migration: existing `type: "direct"` rows keep working, the
underlying binary just swaps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ship the MediaGo browser extension alongside the Desktop installer so
users can "Load unpacked" it from Chrome / Edge without building the
repo or downloading a separate zip. Adds a discovery path from inside
the app, a three-language doc page, and fixes the Desktop HTTP port
the extension was pointing at.
Packaging:
- `pnpm build:electron` now also builds `@mediago/extension` (turbo
filter), and `apps/electron/scripts/build.ts` copies the dist to
`app/build/extension/` and declares it in electron-builder
`extraResources` → installers land `resources/extension/`.
Runtime:
- `resolveExtensionDir()` in binaryResolver — dev points at the
monorepo dist, prod at `process.resourcesPath/extension`, env
override via `MEDIAGO_EXTENSION_DIR`.
- New IPC `app.getExtensionDir()` returns the resolved path. UI pairs
it with the existing `shell.open()` (no dedicated open-folder IPC)
so the pattern matches configDir / binDir / localDir.
Settings UI:
- New "Browser extension directory" button in Settings → More Settings,
right next to the existing folder shortcuts. Web/server mode hides
the row behind `isWeb` and the stub returns "".
- New i18n key `extensionDir` in shared zh/en resources; duplicate
`currentVersion` key removed from zh.ts along the way.
Port fix:
- Extension `DESKTOP_HTTP_BASE` corrected from `:9900` → `:39719` to
match the Electron-side `preferredPort`. 9900 is the standalone
Go Core / web-server port — two separate deployments.
Docs:
- New `docs/extension.md` + `docs/en/extension.md` + `docs/jp/extension.md`
covering what the extension does, how to install the unpacked
build, the three dispatch modes (schema / desktop-http / docker-http),
import-behaviour toggles, language switching, and common pitfalls.
- Sidebar entries registered in `.vitepress/config.ts` for all three
locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the English-only convention already used in the Electron/UI
scripts. Covers dev.ts (start dev server / build Player UI / compile
dev binary) and release.ts (build all-platform binaries / package /
clean). Behaviour unchanged — message text only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Manifest V3 extension that sniffs downloadable video / audio URLs
across every site the user visits and hands captured sources to a
MediaGo server in one click. Ships unlisted (load-unpacked .zip), no
Chrome Web Store.
Dispatch modes (user-picked in options, no silent fallback):
- desktop-schema: navigates the current tab to
`mediago-community://index.html/?n=1&silent=1&url=…` using the
cat-catch `chrome.tabs.update` pattern. Reuses the existing
`useUrlInvoke` hook in apps/ui — no new deeplink plumbing.
- desktop-http: POST http://127.0.0.1:9900/api/downloads against the
Go Core bundled in a running Desktop.
- docker-http: same POST against a user-configured host, with an
optional X-API-Key header for `--enable-auth` deployments.
UI is React 19 + Tailwind v4 + shadcn/ui (new-york, neutral), matched
to apps/ui's stack. Popup shows per-tab sources with a red badge count;
options page has a 3-mode radio + per-mode field panel.
Supporting changes:
- Abstract sniff filter rules into @mediago/shared-common/sniff and
point the Electron sniffing helper at the same exports so desktop
and extension stay in lock-step.
- Tighten the YouTube host rule to actual video / short / live / embed
URLs (drops the homepage and feed false-positives).
- Fix the macOS electron-builder config: CFBundleURLSchemes was
hard-coded to a test string "mediagoaaa" — now sourced from
process.env.APP_NAME (mediago-community in .env) like everywhere
else. Also add a top-level `protocols` entry for clarity.
- Vite build pulls APP_NAME from the repo root .env via loadEnv() +
`define`, so the scheme name stays single-sourced.
- Root scripts: `pnpm dev:extension`, `pnpm build:extension`,
`pnpm pack:extension` (+ scripts/pack-extension.ts that cross-
platform-zips dist/ into release/).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DevTools profiling showed a ~319ms long task when switching to the
settings page, dominated by scripting inside node_modules — Ant Design
Form.Item registration and cssinjs style generation firing for ~25
fields in a single synchronous pass. Chunk loading was only ~9ms, so
earlier fixes around lazy-import and Suspense fallbacks missed the real
cause.
Changes:
- Stream the 6 cards in one per animation frame (visibleCount driven
by requestAnimationFrame), so the longest task is a single card's
registration instead of the full page. The first card paints
immediately and the rest follow over the next few frames.
- Skip the initial setFieldsValue via an isFirstSync ref;
initialValues already seeds the form, so that effect was only
forcing Ant Form to diff every Form.Item again right after mount.
- Wrap cardSections in useMemo with a narrow dep list
(settings.apiKey, settings.local, envPath, updateAvailable, stable
callbacks) so SSE config-changed updates don't rebuild the 340-line
JSX tree unless those specific fields changed.
- Move the key from <Card> to the outer <div> so React stops treating
each re-render as a remount of the card subtree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Make favorite title optional in both the DTO (drop binding:"required")
and the add-favorite modal (no validator). Server falls back to the
URL when the title is empty so the list entry always has a label.
- Auto-fill the title via the existing GET /api/url/title scraper when
the user leaves the URL field, unless they already typed their own
title. Failures are swallowed (server fallback still applies).
- Reorder the modal so URL is on top and Title below, matching the
actual data-flow: users paste a URL first and the title follows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Go Core now broadcasts a "download-create" SSE event after
POST /api/downloads succeeds, carrying the new task ids and count.
The renderer's events.ts listens for it and calls useDownloadStore's
increase() imperatively, so the sidebar badge updates regardless of
which WebContents issued the request — the source-extract overlay
dialog has its own Zustand instance and can no longer silently swallow
the increment.
- Remove the now-redundant local increase() calls in download-form and
browser-view-panel so the counter is incremented exactly once per task
from a single source of truth.
- Favorite.Create returns 409 Conflict with a translated
MsgURLAlreadyExists message when the URL already exists, instead of
500 with the raw "url_already_exists" string.
- http.ts response interceptor now surfaces the server's translated
message on 4xx/5xx instead of Axios's generic
"Request failed with status code XXX".
- Simplify getFavIcon: drop the 1s <img> probe that dropped most URLs
and return the canonical /favicon.ico; <Avatar> already falls back to
a link icon when the image fails to load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tray context menu was built once at startup, before the language
was loaded from Go Core, so its labels were stuck in the fallback
locale and never updated when the user switched languages. Keep the
Tray instance on the class, extract the menu build into refreshTrayMenu,
and subscribe to i18n "languageChanged" so the menu follows the
current language for both startup hydration and runtime changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resolve AppStore.language at apply-time via a shared resolveAppLanguage
helper so both renderer (navigator.language) and Electron main
(app.getLocale) follow the actual OS locale instead of silently
falling back to zh when the stored value is "system".
- Settings page: horizontal form with a fixed label column, borderless
cards flowing naturally in a 2-column CSS columns layout; drop
width="xl" on inputs and wrap button groups to fix English-label
overflow.
- scripts/download-deps: add a PowerShell fallback for zip extraction
on Windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Upgrade zustand from 5.0.0-rc.2 to ^5.0.0 (resolved 5.0.12)
- Fix player:dev and player:build scripts that referenced non-existent
@mediago/player-build, now correctly point to @mediago/core-build
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite README.md in idiomatic English as the default language
- Move original Chinese README to README.zh.md, delete README.en.md
- Polish ~80 UI translation strings in en.ts for natural English
- Fix hardcoded Chinese in Skills setup commands (setting-page)
- Fix mismatched translation key: reppeatPassword → repeatPassword
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace deprecated webContents.canGoBack/goBack with navigationHistory API
- Add try/catch to autoUpdater.checkForUpdates to handle ERR_CONNECTION_REFUSED
- Replace cross-fetch with Node native fetch to fix url.parse() deprecation
- Remove unused cross-fetch dependency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add youtube download type with yt-dlp schema (CLI args, progress regex)
- Replace 6 individual --xxx-bin flags with single --deps-dir flag
- Add BinaryNames map in Go core to auto-resolve binary paths from deps dir
- Simplify Electron/Server binary resolvers to return depsDir only
- Add yt-dlp to deps-versions.json (v2026.03.17)
- Add YouTube option to UI download form with URL auto-detection
- Fix download-terminal.tsx writing [object Object] instead of log content
- Improve error messages when binary not found (include path and type)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared navigation logic into useBrowserActions hook, fix race conditions
in sniffing helper, add event listener cleanup in webview service, split store
selectors for granular re-renders, memoize list items, throttle ResizeObserver,
and add loading/error states to favorites list. Also remove unused getMachineId
IPC handler and fix IPC callback type signatures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of capturing a screenshot, transferring a large DataURL via IPC,
and hiding the browser WebContentsView, use a separate overlay
WebContentsView layered above the browser view to render the download
form. This eliminates flicker, avoids potential page state loss, and
removes the inefficient screenshot transfer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump mediago-core from v0.1.0 to v0.2.1 in deps-versions.json
- Update ConsoleReg to match new structured log format:
speed regex for B/s units, start/isLive/error patterns
- Remove --no-log flag so structured logs are visible for parsing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use correct GoEnvPath fields (configDir/binDir) instead of non-existent workspace/binPath
- Defer form.setFieldsValue after modal opens so Form is mounted
- Add Input child to hidden Form.Item with name prop to satisfy antd 6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add mediago-core (caorushizi/mediago-core) as a fourth download type
alongside m3u8, bilibili, and direct. This enables HLS/DASH streaming
downloads using the custom Go-based downloader.
Changes span the full integration path: dependency download config,
Go Core backend (type, schema, CLI flag, binary map), binary resolution
for Electron/Server, shared TypeScript types, i18n labels, UI dropdown,
dev scripts, and Dockerfile.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bilibili returns an error page ("出错了") when requests lack a proper
User-Agent. Add browser User-Agent and Referer headers to both
GetPageTitle (service/helpers.go) and fetchTitle (handler/util.go).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When --local-dir is explicitly passed (e.g. Docker's /app/mediago/downloads),
use it directly and write back to appStore. Previously appStore's stale value
(e.g. home directory) would override the CLI flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker mode with --enable-auth was returning 401 for the homepage
and all static assets. Add /assets/, /favicon.ico, / and SPA
frontend routes (non-API paths without dots) to the auth whitelist.
Verified locally: homepage 200, /signin 200, /api/config 401 without key.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skills:
- Rewrite SKILL.md as OpenClaw Skill (remove scripts dependency)
- Guide users to install MediaGo + initialize config in one message
- Support Chinese and English natural language commands
- Remove mediago-api.sh (use curl directly, cross-platform)
Docs:
- Add OpenClaw Skill page (zh/en/jp) with install, config, usage guide
- Add to VitePress sidebar for all 3 languages
Settings UI:
- Add "Skills 设置" tab with install command + init command
- Electron mode: init shows URL only
- Docker mode: init shows URL + API key
- One-click copy buttons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On first run, the CLI --local-dir value was used by the Go core
but never written to appStore. The UI reads the download directory
from /api/config (appStore), so it showed empty. Now writes the
resolved local-dir to appStore when the stored value is empty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The auth status API returns { setuped: bool } but the frontend was
checking for non-existent fields (initialized, enableAuth). Fix to
use the correct field name so unauthenticated users are properly
redirected to the signin page in server mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change video stream route from /videos/*filepath to /videos/:id
(lookup file path from database by task ID instead of filename)
- Add mimeType field to Video API response so video.js knows the format
- Fix getVideoURL double-slash bug (/videos/... → //videos/...)
- Handle directory downloads: scan inside for first video file when
CheckFileExists returns a directory (e.g. multi-part bilibili downloads)
- Remove unused videoRoot field and ServeVideo function
- Update player-ui PlaylistItem to pass full VideoItem object
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change internal flag from true to false so the Go core listens on
LAN IP instead of 127.0.0.1, enabling mobile player access via QR code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Load dotenv-flow to read APP_NAME, use it to derive the server data
root directory (~/.${APP_NAME}-server/) instead of hardcoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consolidate all persistent paths under one mountable root:
- Docker: /app/mediago/{data,logs,downloads} with single VOLUME
- Server dev: ~/.mediago-server/{data,logs,downloads}
- DB renamed from app.db to data/mediago.db for consistency
- Update docker run command in README and docs (zh/en/jp)
- Add disclaimer to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove dynamic port finding via portfinder. ServiceRunner now uses
preferredPort directly as the fixed port. Electron mode uses port 39719.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite Dockerfile as 3-stage build (Node builder → Go builder → runtime)
- Use --platform=$BUILDPLATFORM for native Node/Go compilation (no QEMU)
- Cross-compile Go binary via GOOS/GOARCH for target architecture
- Add --platform flag to download-deps.ts for target-specific deps
- Map Docker TARGETARCH to Node arch naming (amd64 → linux-x64)
- Flatten deps directory structure in runtime image
- Rewrite build-server.yml to push to ghcr.io with docker/metadata-action
- Single job multi-arch build (linux/amd64 + linux/arm64) via QEMU
- Update .dockerignore to exclude electron, docs, build artifacts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactor video player API from filesystem scanning to database-backed:
- Video list queries downloads with status="success" + file existence check
- Add GET /api/v1/videos/:id endpoint for playing specific video by task ID
- Add playerUrl to /api/env response (computed from request host)
- Whitelist player/video paths in auth middleware
- Fix video URL generation to preserve folder structure (relative path)
- Fix player-ui Vite base path and video URL resolution under /player/
- Add openUrl implementation to web platform stubs
- Player-UI supports ?id= query param to auto-play specific video
- Play button now passes task.id instead of filename
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge the standalone mediago-player binary into mediago-core. The core
now serves the player UI at /player/ via go:embed, and provides video
listing/streaming endpoints. Video root reuses the existing download
directory (local-dir) so no separate flag is needed.
Changes:
- Add internal/video package to core (handler, service, types)
- Embed player-ui assets via //go:embed in assets/embed.go
- Add SPA handler for /player/ path
- Register /api/v1/videos and /videos/* routes in core router
- core:build now builds player-ui before Go compilation
- Remove apps/player/ entirely (Go app, scripts, configs)
- Remove VideoServer from Electron, derive playerUrl from coreUrl
- Remove player binary management from server app
- Update CI workflow to remove player go.sum cache path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node.js spawn with shell:true joins args with spaces without quoting,
so `-s -w` becomes two separate args on Windows cmd.exe. Manually
wrap args containing spaces in double quotes before passing to spawn.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactor core build scripts to use the same array-based spawn pattern
that fixed the player build (5349295d). The single-string command with
shell quoting caused ldflags parsing failures on Windows cmd.exe.
- Change runCommand to spawn(command, args) with shell only on Windows
- Remove Unix-only 2>/dev/null redirect in getVersion()
- Replace shell chmod glob with native Node.js chmodSync loop
- Add child process cleanup on SIGINT/SIGTERM
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split "-ldflags=-s -w" into separate "-ldflags" and "-s -w" args
so that "-w" is not misinterpreted as a go build flag when shell
mode is enabled on Windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add shell option for Windows in player's runCommand to fix
"spawn pnpm ENOENT" error (Windows needs shell:true to find .cmd)
- Add deps:download step to CI workflow so third-party tools
(ffmpeg, BBDown, etc.) are available during packaging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>