Addresses #638, where a prospective Italian translator asked for clearer
translator onboarding and a way to preview translations in place. The new
guide documents where strings live, the four-file registration path for a
new language, and the Vite HMR dev workflow that already solves the live
preview need. Linked from all three READMEs alongside CONTRIBUTING.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses the line-continuation form (`docker run ... \\n -v ... \\n caorushizi/mediago:3.5.0`)
into one line across README.md / README.jp.md so all three language
READMEs match (zh was already single-line). Makes the command easier
to copy-paste — readers don't have to deal with trailing backslashes
that some terminals / browsers mangle when highlighting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI workflow mirrors every built image to Docker Hub
(`caorushizi/mediago`), but the install lists in README (zh/en/jp)
and docs (guides / changelog for all three languages) only mentioned
GHCR. Most users default to Docker Hub, so they would miss the
simpler pull path.
Switch every Docker row into a two-line form showing both registries
explicitly:
- [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago): docker run ... caorushizi/mediago:3.5.0
- **GHCR**: docker run ... ghcr.io/caorushizi/mediago:3.5.0
Docker Hub appears first (shorter image name, more familiar to most
users) and its label is a link to the Hub page. GHCR is kept as a
second bullet for users who prefer it or are already authenticated.
The README feature-section (`🐳 Docker deployment`) code block was
also switched to the unqualified `caorushizi/mediago:3.5.0` form so
the showcase command defaults to Docker Hub, with a prose note that
the same image lives on GHCR too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Docker Hub mirror workflow used `secrets.DOCKERHUB_USERNAME != ''`
directly inside step `if:` conditions and step-level `env:`
expressions. GitHub's expression parser now rejects that with
`Unrecognized named-value: 'secrets'` — the `secrets` context is only
allowed in a narrow set of positions (mostly `with:` inputs and step
`env:` values, not inside `if:` expressions).
Route the check through a dedicated detection step whose `env:` reads
the secrets (the one permitted way to surface them) and writes a
plain boolean to step outputs. Every downstream conditional then
guards on `steps.dockerhub.outputs.enabled == 'true'` instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every image published to `ghcr.io/caorushizi/mediago` now also gets
pushed to `docker.io/caorushizi/mediago` under the same tags, so users
can `docker pull caorushizi/mediago:3.5.0` without setting up GHCR
auth.
- New `DOCKERHUB_IMAGE` env var pinning the Docker Hub slug.
- New `Login to Docker Hub` step guarded by
`secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != ''`
so forks without Docker Hub credentials can still build + push to
GHCR without failing the login.
- New `Resolve image targets` step that composes the metadata-action
`images:` list at runtime — only adds the Docker Hub target when
both secrets are present. Without this, metadata-action would keep
emitting Docker Hub tags that build-push-action would then 401 on.
- Build Summary shows both `docker pull` hints when Docker Hub is
enabled.
Setup (one-time, in repo Settings → Secrets and variables → Actions):
- `DOCKERHUB_USERNAME`: Docker Hub account name
- `DOCKERHUB_TOKEN`: PAT from https://hub.docker.com/settings/security
with Read+Write scope on caorushizi/mediago
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`docs/guides.md` still linked to v3.5.0-beta.0 assets under a stale
v3.0.1 heading; the English and Japanese versions lagged further
behind at v3.0.0. Update the "Download and install" block on each
guide to point at v3.5.0 (installer / portable / macOS arm64 + x64 /
Linux / Docker) with a link back to the GitHub Releases page for
older builds.
Prepend a v3.5.0 entry to each `changelog.md` using the same
user-facing highlights the README now uses (browser extension /
YouTube + 1000+ sites / OpenClaw Skill / HTTP API / in-app format
conversion / Docker simplification / Go backend). Older entries
(v3.0.0 and earlier) are left untouched — changelog is append-only.
`docs/history.md` intentionally left alone: it's the "old permalinks"
archive and doesn't track the current release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps `docker build -t mediago:local .` so contributors don't have to
remember the tag convention, and keeps local Docker builds discoverable
alongside pack:electron / release:electron in `pnpm run`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`scripts/download-deps.ts` treats the aria2 entry as `source: "local"`
and copies from `extra/aria2/<os>/<arch>/aria2c` into `.deps/`. Every
other downloader (ffmpeg / N_m3u8DL-RE / BBDown / yt-dlp / mediago) is
`source: "github"` and fetched over the network, so they reach the
image fine — but the Docker build context never included `extra/`,
which made `copyLocalTool()` hit its "source missing" branch and
silently skip aria2. Resulting image had no `aria2c`, any direct
download inside the container failed with a binary-not-found error
the moment a task was dispatched.
Add a `COPY extra/ extra/` in the node-builder stage, right before
`pnpm deps:download` runs, so the vendored binary is actually visible
to the script.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a released image has to be re-cut (e.g. the broken 3.5.0 that
shipped with the stale per-downloader flags), there was no clean way
to re-publish under the same tag from the workflow — only a fresh
git tag push would trigger a build.
Extend `workflow_dispatch` with two inputs:
- `tag` — image tag to publish under (e.g. `3.5.0` to overwrite
the existing one, or `3.5.0-fix.1` for a side-by-side).
Empty value falls back to the pre-existing
`dev-<run_id>` behaviour.
- `push_latest` — when rebuilding a stable release manually, optionally
re-stamp the `latest` tag at the same time.
`docker/metadata-action` already emits the standard OCI labels,
including `org.opencontainers.image.revision` sourced from `github.sha`.
That field alone is enough to tell two same-tag builds apart via
`docker inspect` — no Dockerfile changes needed, no manually-computed
build args. Summary step now reads version / commit from metadata-action's
own outputs (`steps.meta.outputs.version`) and `github.sha`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The image was failing at startup with:
flag provided but not defined: -m3u8-bin
mediago-core consolidated the per-downloader `--*-bin` flags into a
single `--deps-dir` in 3.5.0-beta.1, but the Dockerfile was still
passing the old five flags (`--m3u8-bin`, `--bilibili-bin`,
`--direct-bin`, `--mediago-bin`, `--ffmpeg-bin`). Go flag parsing
rejects unknown flags, so the container exited immediately.
While fixing, extracted the invocation into
`scripts/docker-entrypoint.sh` rather than leaving a nine-line
quoted-array `CMD` at the bottom of the Dockerfile:
- `exec mediago-core …` replaces the shell so SIGTERM from
`docker stop` reaches the Go process directly — no shell parent to
swallow signals.
- `"$@"` forwards any extra flags from `docker run image … --foo=bar`
so callers can override individual options without replacing the
whole command.
- Switching from `CMD` to `ENTRYPOINT` makes the script the fixed
launch path; runtime overrides still work via the forwarded `"$@"`.
- Editing default args no longer touches the Dockerfile (and thus
doesn't invalidate the full image layer).
New `.gitattributes` pins `*.sh` to LF line endings so a Windows
checkout doesn't turn the entrypoint into a `/bin/sh^M: bad interpreter`
error inside the Linux container.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three READMEs had drifted into a dev-first shape — the first screen
of each started with pnpm / install commands before telling end users
what the product actually does — and the Japanese version had fallen
behind (outdated commands, no v3.5.x downloads, screenshots pointing
at an external CDN rather than the in-repo images).
Rework all three around the v3.5.0 user-friendly feature story and
move the developer setup into its own file:
- Unified skeleton across zh/en/jp: header badges → one-sentence pitch →
six feature sections (browser extension · YouTube / 1000+ sites ·
OpenClaw Skill · HTTP API · in-app format conversion · Docker +
LAN) → screenshots → v3.5.0 downloads (+ BT Panel one-click) →
v3.5.0 highlights → tech-stack badge row → acknowledgements →
disclaimer → link to CONTRIBUTING.
- Header elements preserved verbatim: nav links, language switcher,
MediaGo Pro / Try Now banners, downloads / stars / forks / GitCode /
Trendshift badges, 4 screenshots, BT Panel one-click deployment,
the full 5-point disclaimer.
- Tech-stack prose list compressed to a single row of shields.io
badges (React / Electron / Vite / TypeScript / Tailwind / shadcn/ui /
Go / Ant Design).
- Acknowledgements gain aria2 (now powering direct downloads after the
gopeed swap).
- README.jp.md specifically: fixed four `static.ziying.site/images/*`
references to use the in-repo `./images/*` (matching zh/en); replaced
the outdated v3.0.0-only download block with v3.5.0; added the
English-language switcher link that was missing from the header.
- README.md/zh.md: dropped the v3.0.0 changelog block (history lives
on GitHub Releases) and the "Quick Start" / "Running locally" pnpm
command blocks.
New file: `CONTRIBUTING.md`. English only — prerequisites, repo layout,
a short list of the commands contributors actually type (deps:download
/ dev:electron / dev:server / pack:electron / release:electron / check),
Conventional Commits note, PR guidance. Each README ends with a one-
line pointer to it in the local language.
No product behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users naturally expect the "Current version" label in chrome://extensions
to match the version shown inside the MediaGo window. Tie both to a single
source of truth: `apps/electron/app/package.json`, which Electron's build
pipeline (`apps/electron/tsdown.config.ts`) already reads and exposes as
`process.env.APP_VERSION`.
`manifest.config.ts` now imports the same file and:
- `version` — stripped down to the numeric prefix (`split(/[-+]/)[0]`)
so Chrome accepts it. Chrome's manifest parser only
allows 1–4 dot-separated integers; SemVer pre-release
or build suffixes (`-beta.2`, `+build.7`) get rejected
at install time.
- `version_name` — keeps the full SemVer string. It's free-form, not
used for update-ordering, and is what chrome://extensions
displays when present.
Effect: bumping `apps/electron/app/package.json` once is enough to
re-stamp both the Desktop binary and the bundled extension manifest.
The extension's own `packages/mediago-extension/package.json` is a
private workspace package — its `version` field is unused by the
manifest and intentionally left alone to avoid a pointless sync step
on every release.
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>
If a stray MediaGo instance (typically the installed Desktop build
running in the background from the Start menu) already holds the
preferred port, a freshly-spawned child fails to bind and exits
immediately — but `waitForHealthy()` happily answers green because
the *other* instance's `/healthy` responds on the same port.
ServiceRunner then considers the service "started" and every
subsequent request silently lands on the wrong process. In practice
this surfaced as dev mode talking to the installed Go Core and
reporting a stale gopeed binary path, even though the dev build had
already been rebuilt with aria2c.
Pre-check: before spawning, open and close a throwaway TCP listener
on `<bindHost>:<preferredPort>`. If the bind fails, throw a
human-readable error instead of proceeding. The tiny check-vs-bind
race window is acceptable for catching the real-world "long-running
stray instance" case.
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>
ServiceRunner used to pass the detected LAN IPv4 as the `HOST` env var
when `internal: false`, so the Go Core bound *only* to that LAN IP —
requests to 127.0.0.1 / localhost were refused. That blocked the
browser-extension's `desktop-http` mode (fixed at `http://127.0.0.1`).
Split bind host from display host:
- Bind (`HOST` env to child): `0.0.0.0` when `internal: false`, else
`127.0.0.1`. Listener now accepts traffic on every interface.
- Display (`state.host` / `getURL()`): keeps the detected LAN IPv4
when `internal: false` so the settings UI can surface a shareable
LAN URL, falls back to 127.0.0.1 otherwise.
Net effect: Desktop is reachable via 127.0.0.1, localhost, and its LAN
IP simultaneously, while the UI still shows the LAN address.
Also translates all remaining Chinese JSDoc / inline comments in
`index.ts` and `utils.ts` to English, matching the rest of the package.
Behaviour otherwise unchanged.
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>
The MediaGo extension shipped Chinese-only (50+ hardcoded strings across
popup, options, background). Wire up i18next + react-i18next so the UI
follows the browser language by default and lets the user override to
zh / en in a new LanguageCard on the options page.
- `ExtensionSettings.language: "system" | "zh" | "en"` (default system),
persisted via existing `chrome.storage.local`
- `resolveLanguage()` maps system -> `chrome.i18n.getUILanguage()` or
navigator.language with a zh/en fallback
- Bootstrap one i18next instance per page (popup / options), subscribe
to `chrome.storage.onChanged` so switching on one surface updates the
other without a reload
- Background service worker emits `LocalizedMessage` descriptors
(`{ key, values? }`) for toast copy; popup calls `t(key, values)` so
the SW never has to carry an i18n instance
- Full zh/en coverage for popup, options, error paths
- Chrome-native `_locales/{en,zh_CN}/messages.json` + `__MSG_*__` for
`manifest.name` / `description` / `action.default_title` so the
chrome://extensions row and toolbar tooltip follow the browser UI
language (independent dimension from the in-UI toggle, by design)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The useUrlInvoke hook at apps/ui/src/hooks/use-url-invoke.ts reads
both `silent` and `downloadNow` query params; they were previously
hard-coded on the extension side. Surface them as user toggles so
Schema-mode users can opt into "open the download form for review"
and both modes can opt into "start downloading immediately instead
of just queuing".
- ExtensionSettings gains `downloadNow` (default false) and
`schemaSilent` (default true).
- importViaHttp threads `startDownload` into the POST body.
- buildTaskDeeplink writes `silent=1` / `downloadNow=1` into the
deeplink URL based on the flags (falsy => omit the key, matching
the truthy-check in useUrlInvoke).
- New headless Switch component (no @radix-ui/react-switch dep).
- New ImportBehaviourCard in options, with a patch-style save hook
so each toggle persists immediately.
- use-options.ts now merges on top of the current persisted settings
so the server-config card doesn't wipe the new fields.
Co-Authored-By: Claude Opus 4.6 (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>
Correct 8 inaccuracies where CLAUDE.md diverged from the actual codebase:
server is a Node.js launcher not Koa 3, apps/player/ doesn't exist,
remove non-existent commands, fix adapter layer and event bridge file
names, correct DI and module format claims.
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>
FloatButton was effectively dead — it listened for "webview-link-message" which
no one sends after the IPC namespace refactoring, and its functionality is fully
covered by BrowserViewPanel in the main UI. Remove it along with jQuery, Lit and
nanoid dependencies. Consolidate remaining BilibiliButton into a single-file
vanilla custom element, replace fragile global-index DOM lookup with
closest()-based card-relative queries, and replace setInterval polling with
MutationObserver. Build output reduced from 607KB to 1.9KB.
Co-Authored-By: Claude Opus 4.6 <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>
Create a Claude Code skill that wraps MediaGo's download API:
- SKILL.md with setup instructions, download workflow, and config
management via natural language
- mediago-api.sh helper script for health check, download, status,
list, and wait commands
- Config stored in ~/.mediago-skill.json (URL + API key)
- Auto-detects download type from URL (m3u8/bilibili/direct)
- Supports first-time setup via natural language configuration
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>
Node.js 20 reached EOL in April 2026. Upgrade build Node.js
version to 24.14.0 LTS across all CI workflows.
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>
N_m3u8DL-RE is a .NET application that requires libicu for
globalization support. Without it the process aborts immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Install unzip in Node builder for zip-format deps (BBDown, gopeed)
- Copy apps/electron/app/package.json needed by UI vite config
- Add second go mod download after full source copy to fix go.sum
Verified: docker build + run succeeds locally (healthy + player UI ok)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pnpm install --frozen-lockfile requires all workspace packages to be
present. The .dockerignore excluded apps/electron/ entirely, and the
Dockerfile was missing apps/core/ and apps/electron/ package.json.
- Keep apps/electron/package.json in build context (exclude only src/)
- Add COPY for apps/core/package.json and apps/electron/package.json
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>
The build was failing because:
- macOS runners don't have Go pre-installed (exit code 127)
- Ubuntu/Windows runners lack the swag tool (spawn swag ENOENT)
Add setup-go action and swag installation step before building.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevent accidentally committing binary dependencies that were
removed from git history via git-filter-repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update the custom protocol from mediago:// to mediago-community://
and encode the URL parameter to handle special characters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. events.ts: progress polling was using /api/downloads/active which
returns DB records without percent/speed fields. Changed to /api/tasks
which returns TaskInfo with real-time progress data.
2. download-item.tsx: lint fix had renamed callback params from `task`
to `taskItem` but missed updating all references in the function body,
causing ReferenceError. Reverted param names back to `task`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded "mediago" protocol scheme with dynamic `appName`
from environment, so community/dev builds use their own scheme
(e.g. "mediago-community://"). Applies to:
- constants: defaultScheme = appName
- main.window.ts: production URL uses defaultScheme
- browser.window.ts: same
- build.ts: CFBundleURLSchemes uses dynamic scheme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the frontend hashed passwords with MD5 + a hardcoded salt
before sending to the backend. This exposed the salt in client-side JS
and used an insecure hash algorithm.
Backend changes:
- Setup: accepts plain password, bcrypt hashes it, generates UUID apiKey
- Signin: accepts plain password, bcrypt verifies, returns stored apiKey
- Status: checks passwordHash instead of apiKey
- AppStore: new passwordHash field
- Middleware: add X-API-Key header support (was only reading Authorization)
- CORS: allow X-API-Key header
Frontend changes:
- Signin page: send plain password, receive apiKey from response
- Remove md5/crypto-js dependency and APIKEY_SALT_KEY constant
- api/download-task: auto-inject localPath/deleteSegments from store
(was lost during go-adapter migration, caused EOF errors)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of manually calling setHttpApiKey() after signin, use an axios
request interceptor that reads apiKey from useAppStore.getState() on
every request. This ensures the header is always in sync with the
stored apiKey, regardless of when or how it was set.
Removed: setHttpApiKey() function (no longer needed).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After login, the apiKey was saved to Zustand/localStorage but never
set on the http axios instance header, causing subsequent API calls
to return 401.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add proper TypeScript return types to every api/ function, using
existing shared types (Favorite, Video, Conversion, AppStore, etc.)
from @mediago/shared-common.
New types added:
- GoEnvPath (api/config.ts) for /api/env response
- AuthStatus (api/auth.ts) for /api/auth/status response
Updated SWR hooks to use typed data instead of Record<string, unknown>
casts: useFavorites, useConfig, useEnvPath, useAuthApi, useConversions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues:
1. http.ts 401 handler used window.location.hash (HashRouter syntax)
but the app uses BrowserRouter — changed to window.location.pathname
2. useAuth() blindly checked localStorage apiKey, which is always empty
on first visit. Now queries Go Core auth status first: only redirects
to /signin when auth is enabled and configured (or needs setup)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The UI sends X-API-Key for authentication, but the CORS AllowHeaders
config only listed Authorization. This caused preflight failures in
server mode where UI (localhost:8555) and Go Core (127.0.0.1:9900)
are on different origins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sidebar: toggle between embedded and external browser window mode,
show extract page in sidebar even when opened externally
- Home page: remove redundant material extraction button
- Converter: temporarily disable video format options, keep audio only
- Electron: remove node-machine-id dependency from package.json
- Tool bar: fix combineToHomePage argument structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PageContainer was missing flex-col, causing pagination to render inline
with the list. Add flex-col + gap-3 to PageContainer and remove
duplicate padding/bg from the inner list container.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded { current: 1, pageSize: 500 } with dynamic pagination
state. Default pageSize is 50 with an Ant Design Pagination component
at the bottom of the list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
machineId is now generated by Go Core on first startup (uuid),
so the Electron IPC handler using node-machine-id is no longer needed.
Removes: getMachineId handler, node-machine-id import, nanoid import.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move machineId generation from Electron (node-machine-id) to Go Core.
On first startup, if machineId is empty in appStore config, Go Core
generates a UUID and persists it. This makes machineId available in
both Electron and Web modes via the config API.
UI now reads machineId from getConfig() instead of getMachineId() IPC.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PlatformApi methods via Electron IPC return { code, data, msg } objects.
After migrating from useAPI() to usePlatform(), the auto-unwrap logic was
lost, causing consumers to receive raw objects instead of extracted data
(e.g. onSelectDownloadDir() returned [object Object] instead of a path).
Fix: wrap all PlatformApi function calls in the Proxy get trap to
auto-unwrap { code, data } responses, matching the old useAPI() behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- core: move defer logger.Sync() after logger re-init to avoid nil panic
on early startup
- electron: replace console.log with ElectronLogger in DownloaderServer
and ElectronApp for consistent log output
- ui: add 1s delay before fetching coreUrl in Electron mode to wait for
Go core to finish starting
- ui: guard data?.list access in converter-page to prevent crash when
data is undefined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, init() was not awaited in index.ts, so any failure in the async
chain (e.g. getConfig() timing out) was silently swallowed and the window
was never created.
Fix:
1. Await mediago.init() in index.ts so errors surface properly
2. Move serviceInit() (window creation) before Go core startup so the UI
always appears, even if the backend fails to start
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Use Go build tag `dev` to conditionally compile swagger docs and routes.
Production binaries no longer include swaggo/files (swagger UI assets):
- mediago-core: 34 MB → 24 MB (-10 MB, -29%)
- mediago-player: 29 MB → 13 MB (-16 MB, -55%)
Dev builds (-tags dev) retain full swagger at /swagger and /docs.
2. Set executable permission (0o755) on built binaries (non-Windows).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The packaged app failed to launch because mediago-core was missing from
Resources/bin/. The asarUnpack approach relied on platform-specific npm
optional deps that pnpm workspace hoisting never installed in app/node_modules/.
Fix: copy locally-built binaries (core, player, deps) into app/build/bin
and app/build/deps during the pack step, then expose them via extraResources
so electron-builder places them at the expected Resources/bin/ and
Resources/deps/ paths at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Redesign the "Add File" flow as a modal dialog containing:
- File path field with "Browse" button (opens native file picker)
- Output format selector (Video: MP4/MKV/WebM, Audio: MP3/AAC/FLAC/WAV)
- Quality selector (High/Medium/Low)
- Two action buttons: "Add to List" (saves for later) and
"Convert Now" (saves and starts conversion immediately)
Also fix silent failure when adding files — getFileName() used
new URL() which throws on local file paths. Replaced with simple
path.split() extraction and added try-catch error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move ffmpeg execution from Electron to Go Core so conversion works in
both Electron and web server modes.
Go Core backend:
- Add -ffmpeg-bin CLI flag to pass ffmpeg binary path
- Enhance Conversion DB model with status, outputPath, outputFormat,
quality, progress, and error fields
- Create converter executor (service/converter.go) that spawns ffmpeg,
builds args per format/quality, and parses stderr for progress
- Add StartConversion/StopConversion to service with SSE events for
real-time progress (conversion-start/progress/success/failed/stop)
- Add POST /api/conversions/:id/start and /:id/stop endpoints
Supported formats:
- Video: MP4 (H.264), MKV (H.264), WebM (VP9) with CRF quality
- Audio: MP3, AAC, FLAC, WAV with bitrate quality presets
- Quality presets: high/medium/low mapping to CRF and bitrate values
Frontend (converter-page):
- Add format selector (Video/Audio groups) and quality dropdown
- Show per-item status badge, progress bar during conversion
- Action buttons: Start/Stop/Open Folder/Delete based on status
- "Convert All" batch button for pending items
- SWR auto-refresh during active conversions
Integration:
- Pass -ffmpeg-bin from Electron and core:dev to Go Core
- Remove old broken Electron-only convertToAudio code
- Add startConversion/stopConversion to core-sdk and GoApi
- Add i18n keys for new UI elements (en/zh)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vite 8's rollup types no longer accept the object shorthand for
manualChunks. Switch to the function form which is compatible with
all Vite versions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UI app:
- Add Vite manual chunks (vendor, antd, zustand) for better caching
- Wrap DownloadTaskItem with React.memo() to prevent list re-renders
- Wrap IconButton with React.memo() to prevent re-renders in lists
- Add missing Suspense fallback to SigninPage route
Player app:
- Add Vite manual chunks (videojs, vendor) to isolate video.js bundle
- Remove duplicate resize calculation in usePlayerSize hook
(ResizeObserver already handles container size changes)
- Disable SWR revalidateOnFocus/revalidateOnReconnect for video list
(list only changes when user adds/removes videos)
- Extract PlaylistItem into memoized component to prevent re-renders
when sibling items change, and eliminate duplicated rendering logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the app polled GET /api/tasks every second unconditionally
from startup, even with no downloads running. Now polling is driven
by SSE events: started on download-start, stopped when no tasks have
Downloading status (checked after success/failed/stop events).
Applied to both Electron main process (downloader.server.ts) and
web/UI mode (go-event-bridge.ts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converted all Chinese-language comments across 47 source files to
English, covering Go backend (apps/core, apps/player), TypeScript
scripts, Electron main process, UI components, and shared packages.
Only comment text was modified — code, string literals, log messages,
and i18n translation values were left untouched.
Also fix pre-existing lint errors in scripts/release.ts and
scripts/utils.ts (node: protocol prefix, unused imports, bare catch).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes fixed:
1. ElectronStore migration deleted Go Core's config.json on every startup.
The old migration code unconditionally ran unlinkSync() on config.json
in the workspace directory — the same file Go Core uses for persistence.
Removed the migration entirely since it has already run for all users
(window-state.json now holds the bounds data).
2. core:dev mode saved config to the wrong directory (log dir instead of
data dir) because -config-dir was not passed. Added config_dir to
devConfig and the dev() command args, pointing to ~/.mediago/data to
avoid collision with the old v2.0 ~/.mediago/config.json format.
3. syncCLIToAppStore() overwrote user settings with CLI default values
on every Go Core startup in core:dev mode. Removed the function; CLI
args now only set initial cfg defaults, while appStore (user config)
takes precedence via the existing syncAppStoreToCfg().
Also aligns blockAds default (true) between Go Core and frontend Zustand store.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getGoApi() was called eagerly inside useMemo during component mount,
before initGoAdapter() had run. Now GoApi methods resolve getGoApi()
at invocation time, so useAPI() can safely be called in App.tsx before
the adapter is initialized.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major cleanup of the frontend adapter layer:
1. Type split: MediaGoApi (55 methods) → GoApi (22 data/CRUD methods via
Go Core HTTP) + PlatformApi (~30 Electron-native methods via IPC)
2. Remove Proxy-based routing: replace the fragile apiAdapter Proxy +
GO_METHODS set + ALL_API_METHODS array with direct object composition.
GoApi methods never fall through to Electron IPC.
3. Clean dead code: remove 18 preload methods that called IPC channels
with no handler (getFavorites, getAppStore, createDownloadTasks, etc.)
Remove unused event constants (SOCKET_TEST, SETUP_AUTH, SIGNIN, etc.)
4. New platform-stubs.ts: explicit no-op stubs for web/server mode
instead of a catch-all Proxy that returns empty responses.
5. Simplified useAPI: direct spread of wrapped GoApi + PlatformApi
instead of dynamic Object.keys().reduce() enumeration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add adapterReady state to App.tsx: show Loading until initGoAdapter
completes, preventing API calls before goHandle is set (fixes
"No handler registered" errors for get-favorites, get-app-store, etc.)
- Add error handling and debug logging for player server startup
- Prepend core:build && player:build to dev:electron, dev:server,
pack:electron, and release:electron scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Sync Go Core config to Zustand on app startup so settings persist
across restarts (Go Core is the single source of truth).
2. Go Core Create handler now respects startDownload param — downloads
start immediately when "Download Now" is clicked.
3. Player integration fixes:
- Electron: graceful skip when player binary is not built
- Server: start player service alongside Go Core, expose playerUrl
- Web adapter: derive playerUrl from current host instead of empty string
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add apps/core, apps/player, apps/player-ui, packages/core-sdk.
Remove deleted packages (shared/node, shared/browser, config).
Replace stale TypeORM references with @mediago/service-runner.
Document Go Core commands, adapter architecture, and port numbers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename APP_NAME to mediago-community in .env
- Update APP_ID to mediago.caorushizi.cn
- Set mac identity: null to skip code signing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bump knip to v6.1.1, turbo to v2.9.1
- Remove oxlint-tsgolint devDependency
- Remove hidden={isWeb} from showTerminal setting item
- Format .vscode/settings.json and setting-page/index.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@electron/rebuild and prebuild-install are no longer needed as the
Electron app has no native runtime dependencies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix PTYRunner.readPTYOutput to use raw chunk reads instead of a
broken bufio.Scanner split that called onStdLine with the entire
remaining buffer on every invocation, causing log lines to be
written multiple times
- Remove unused flushInterval field and NewPTYRunnerWithInterval
- Add convertEol: true to XTerm so bare \n in stored logs is treated
as \r\n, preventing misaligned output when replaying log files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update apps/server/src/binaryResolver.ts to use monorepo paths
instead of npm platform packages
- Remove @mediago/core and @mediago/deps from server package.json
- Add .gitkeep placeholder in apps/player/assets/ui/ for go:embed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4-6 of monorepo merge:
- Create scripts/download-deps.ts for downloading third-party tools
from GitHub Releases (ffmpeg, N_m3u8DL-RE, BBDown, gopeed)
- Add scripts/deps-versions.json for version-locked tool definitions
- Update pnpm-workspace.yaml for new monorepo structure
- Change @mediago/core-sdk references from file:// to workspace:*
- Remove @mediago/core, @mediago/deps, @mediago/player npm deps
from electron package.json
- Rewrite binaryResolver.ts to use monorepo paths (dev) and
extraResources (production) instead of npm platform packages
- Simplify apps/core/scripts/config.ts (remove npm config, use .deps/)
- Add Go project scripts to root package.json
- Update turbo.json build outputs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete DownloadController (4 methods now handled by Go Core go-adapter),
move showDownloadDialog to WebviewController. Remove exportFavorites,
importFavorites, exportDownloadList from GO_METHODS so Electron mode
uses IPC with native file dialogs instead of bypassing them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update go-adapter getEnvPath to call the new /api/env endpoint instead
of returning hardcoded empty strings for binPath and workspace.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add notification badge update after creating download tasks
- Add defensive null check in useAPI to prevent destructuring undefined adapter results
- Import download store for increase() calls in download-form
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove old Koa/Socket.IO server, replace with minimal ServiceRunner launcher
- UI now connects directly to Go Core via SDK (HTTP + SSE), no middleware
- Add GoEventBridge for SSE events and progress polling
- Add Dockerfile and docker-compose for single-container deployment
- Fix web mode adapter Proxy ownKeys for proper method enumeration
- Fix auth flow: setupAuth now sets apiKey, App.tsx passes stored apiKey
- Add error handling in download-form for getVideoFolders
- Remove conversion.controller import (file was deleted)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failed and stopped events were only logged but never sent to the UI,
causing tasks to stay stuck showing "downloading" status. Now all terminal
state changes (success/failed/stopped) are forwarded via IPC/Socket.io
and trigger SWR revalidation for immediate UI refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the entire TypeORM-based data layer (entities, repositories, services)
and refactor Electron/Server controllers to delegate to Go backend via HTTP API.
Add go-adapter for UI to communicate directly with Go core service.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Prettier with oxfmt for formatting and expand oxlint config with
full rule categories, React/TS overrides, and per-app config inheritance.
Add husky + lint-staged for pre-commit hooks to incrementally enforce
formatting on changed files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace @typescript/native-preview with standard typescript package
and remove tsgo-specific dts config from tsdown configurations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- bundle linux arm64 helper binaries under bin/linux/arm64\n- harden frontend download and browser components with safer defaults\n- assign stable ids via useId to avoid duplicate DOM targets\n- align shared Favorite type with title/icon fields and update consumers\n- point release pipeline at backend-electron and force clean deletions\n- switch preload build entry and loosen biome static interaction lint
- Move i18n instance from shared-common to shared-node for backend usage
- Create shared-browser package with React i18n integration for frontend
- Centralize i18n resources in shared-common with structured organization
- Update all imports across backend-electron, backend-web, and frontend-main
- Add language detection and browser-specific i18n configuration
- Maintain backward compatibility while improving maintainability
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove packages/shared package entirely
- Update all apps and packages to use @mediago/shared-common and @mediago/shared-node instead
- Simplify router implementation by removing dynamic electron imports
- Update build configurations and dependency references
- Clean up pnpm-lock.yaml to reflect new package structure
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Replace remote image URLs with local images in README.md
- Add screenshot images to images/ directory
- Update CLAUDE.md with current project structure and commands
- Remove obsolete @ghostery/adblocker patch
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Consolidate download-related methods to reduce duplication:
- Merge downloadNow, downloadItemsNow into addDownloadItems with startDownload flag
- Merge editDownloadNow into editDownloadItem with startDownload flag
- Remove redundant addDownloadItem method in favor of addDownloadItems
- Update both Electron and Web backends with unified parameter structure
- Maintain backward compatibility while simplifying the API surface
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract shared package into three separate packages for better separation of concerns:
- @mediago/shared-common: Common types, constants, utilities, i18n
- @mediago/shared-node: Node.js specific services, DAOs, entities
- @mediago/shared-browser: Browser-specific code
Benefits:
- Improved dependency management and package isolation
- Eliminated circular dependencies between packages
- Better maintainability with domain-separated codebases
- Enhanced type safety across the monorepo
Changes:
- Created new packages with proper TypeScript configurations
- Migrated all code from packages/shared/src/{common,node,browser}
- Updated 40+ import statements across the entire codebase
- Resolved package dependency conflicts and circular references
- All backend packages pass TypeScript compilation
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract platform-agnostic business logic from backend-electron and backend-web controllers
into reusable services in the shared package, enabling code reuse across platforms:
- ConversionService: handles conversion record management operations
- DownloadManagementService: manages download lifecycle, CRUD operations, and metadata
- FavoriteManagementService: handles bookmark/favorites management
Benefits:
- Eliminates code duplication between Electron and Web backends
- Centralizes business logic maintenance in shared package
- Preserves platform-specific UI interactions (dialogs, notifications)
- Maintains dependency injection architecture consistency
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Rename IpcHandlerService to ElectronRouter for clarity
- Introduce shared MEDIAGO_METHOD and MEDIAGO_EVENT constants
- Replace separate @get/@post decorators with unified @handle decorator
- Standardize routing metadata handling across platforms
- Add MediaGoRouter interface for consistent router contracts
- Remove unused @on decorator from Electron helpers
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Move adapters from src/adapters/ to src/hooks/adapters/
- Add ownKeys and getOwnPropertyDescriptor traps to electronAdapter Proxy
- Enable Object.keys() iteration over electronAdapter methods
- Fix useAPI hook to properly enumerate API methods in Electron environment
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Upgrade Inversify from v6 to v7 and implement modern binding decorators pattern:
- Replace manual container bindings with @provide decorators
- Update all controllers, services, and vendors to use decorator-based DI
- Simplify inversify.config.ts by removing explicit bindings
- Add @inversifyjs/binding-decorators dependency for cleaner configuration
- Maintain singleton pattern and existing functionality
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract preload.ts from apps/backend-electron to packages/electron-preload for better modularity and reusability. This includes:
- Create new @mediago/electron-preload package with proper build configuration
- Update backend-electron to use the new preload package dependency
- Maintain all existing functionality while improving architecture
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Replace `apis/` folder with `adapters/` for clearer architecture
- Integrate IPC logic into corresponding adapter files:
- `electronIpcAdapter` in `electron.ts`
- `webIpcAdapter` in `web.ts`
- Add new `useAPI` Hook with cleaner separation of concerns
- Maintain backward compatibility with `useElectron` alias
- Improve code organization and maintainability
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Create base configurations in packages/config for different app types
- Standardize all tsconfig.json files to extend from shared configs
- Fix module resolution inconsistencies (NodeNext -> bundler mode)
- Remove duplicate configuration options across packages
- Improve code quality by fixing linting issues in shared services
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Docker configuration to resolve node-pty native module conflicts
- Create docker/Dockerfile for production builds
- Create docker/Dockerfile.dev for development with hot reload
- Add docker-compose.yml for easy development setup
- Include system dependencies (python3, make, g++, libicu-dev) for native modules
- Update package.json with dev:docker and build:docker commands
- Move .dockerignore to root directory for proper build context
- Remove electron rebuild scripts as Docker handles native module compilation
- Enable mixed development: webapi in container, electron on host
Usage:
- pnpm dev:docker # Start webapi in Docker container
- pnpm -F electron dev # Start electron locally in parallel
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Rename apps with more descriptive names:
• main → electron (Electron main process)
• renderer → frontend (React UI for both Electron and web)
• backend → webapi (Koa.js API server)
- Remove tools/scripts custom build logic:
• Replace custom zx scripts with native Turbo commands
• Update dev command to use Turbo filters for plugin/mobile builds
• Migrate web-release build to use Turbo pipeline
• Remove tools/scripts directory entirely
- Update comprehensive README documentation for each app:
• apps/electron/README.md - Desktop application lifecycle and system integration
• apps/frontend/README.md - React UI shared between Electron and web
• apps/webapi/README.md - Standalone API server with Docker support
- Update all configuration references:
• package.json: Update all Turbo filter commands
• biome.json: Remove tools/scripts path references
• pnpm-workspace.yaml: Remove tools/* workspace
• CLAUDE.md: Update architecture documentation
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Install Turborepo for build orchestration and caching
- Reorganize directory structure following monorepo best practices:
- Move packages/main,renderer,backend,mobile,plugin → apps/
- Keep packages/shared for shared utilities and types
- Create packages/config for shared configurations
- Move scripts/ → tools/scripts/
- Update pnpm-workspace.yaml for new structure (apps/*, packages/*, tools/*)
- Configure turbo.json with build pipelines and task dependencies
- Update all package.json scripts to use Turbo commands
- Update biome.json paths for new directory structure
- Add packageManager field to root package.json
- Update CLAUDE.md documentation with new architecture
Benefits:
- Faster builds with intelligent caching and incremental builds
- Parallel task execution across packages
- Clear separation between apps and shared packages
- Improved developer experience with unified build system
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Install @biomejs/biome as unified linting and formatting tool
- Remove all ESLint/Prettier configurations and dependencies
- Create unified biome.json with TypeScript, React, CSS support
- Update all package scripts to use Biome commands
- Configure lint-staged across all packages
- Add root-level lint, format, and lint:fix commands
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
license: MIT
metadata:
author: vercel
version: "1.0.0"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 67 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
**Incorrect (runs twice in dev, re-runs on remount):**
```tsx
functionComp() {
useEffect(()=>{
loadFromStorage();
checkAuthToken();
},[]);
// ...
}
```
**Correct (once per app load):**
```tsx
letdidInit=false;
functionComp() {
useEffect(()=>{
if(didInit)return;
didInit=true;
loadFromStorage();
checkAuthToken();
},[]);
// ...
}
```
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.
This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.
**Incorrect:**
```typescript
constsomeFlag=awaitgetFlag();
if(someFlag&&someCondition){
// ...
}
```
**Correct:**
```typescript
if(someCondition){
constsomeFlag=awaitgetFlag();
if(someFlag){
// ...
}
}
```
This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.
Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
**Incorrect (imports entire library):**
```tsx
import{Check,X,Menu}from"lucide-react";
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import{Button,TextField}from"@mui/material";
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct - Next.js 13.5+ (recommended):**
```js
// next.config.js - automatically optimizes barrel imports at build time
// Keep the standard imports - Next.js transforms them to direct imports
import{Check,X,Menu}from"lucide-react";
// Full TypeScript support, no manual path wrangling
```
This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.
**Correct - Direct imports (non-Next.js projects):**
```tsx
importButtonfrom"@mui/material/Button";
importTextFieldfrom"@mui/material/TextField";
// Loads only what you use
```
> **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.
These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
## Use Passive Event Listeners for Scrolling Performance
Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
constnumbers=[5,2,8,1,9];
constmin=Math.min(...numbers);
constmax=Math.max(...numbers);
```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
## Defer Non-Critical Work with requestIdleCallback
**Impact: MEDIUM (keeps UI responsive during background tasks)**
Use `requestIdleCallback()` to schedule non-critical work during browser idle periods. This keeps the main thread free for user interactions and animations, reducing jank and improving perceived performance.
**Incorrect (blocks main thread during user interaction):**
```typescript
functionhandleSearch(query: string){
constresults=searchItems(query);
setResults(results);
// These block the main thread immediately
analytics.track("search",{query});
saveToRecentSearches(query);
prefetchTopResults(results.slice(0,3));
}
```
**Correct (defers non-critical work to idle time):**
```typescript
functionhandleSearch(query: string){
constresults=searchItems(query);
setResults(results);
// Defer non-critical work to idle periods
requestIdleCallback(()=>{
analytics.track("search",{query});
});
requestIdleCallback(()=>{
saveToRecentSearches(query);
});
requestIdleCallback(()=>{
prefetchTopResults(results.slice(0,3));
});
}
```
**With timeout for required work:**
```typescript
// Ensure analytics fires within 2 seconds even if browser stays busy
## Use toSorted() Instead of sort() for Immutability
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
var theme = localStorage.getItem('theme') || 'light';
var el = document.getElementById('theme-wrapper');
if (el) el.className = theme;
} catch (e) {}
})();
`,
}}
/>
</>
);
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
impactDescription: avoids noisy hydration warnings for known differences
tags: rendering, hydration, ssr, nextjs
---
## Suppress Expected Hydration Mismatches
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
**Impact: HIGH (reduces load time for critical resources)**
React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
- **`preloadModule(href)`**: Fetch an ES module you'll use soon
- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
- **`preinitModule(href)`**: Fetch and evaluate an ES module
**Example (preconnect to third-party APIs):**
```tsx
import{preconnect,prefetchDNS}from"react-dom";
exportdefaultfunctionApp() {
prefetchDNS("https://analytics.example.com");
preconnect("https://api.example.com");
return<main>{/* content */}</main>;
}
```
**Example (preload critical fonts and styles):**
```tsx
import{preload,preinit}from"react-dom";
exportdefaultfunctionRootLayout({children}){
// Preload font file
preload("/fonts/inter.woff2",{
as:"font",
type:"font/woff2",
crossOrigin:"anonymous",
});
// Fetch and apply critical stylesheet immediately
preinit("/styles/critical.css",{as:"style"});
return(
<html>
<body>{children}</body>
</html>
);
}
```
**Example (preload modules for code-split routes):**
Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect (excessive precision):**
```svg
<pathd="M 10.293847 20.847362 L 30.938472 40.192837"/>
impactDescription: avoids redundant renders and state drift
tags: rerender, derived-state, useEffect, state
---
## Calculate Derived State During Rendering
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
**Incorrect (redundant state and effect):**
```tsx
functionForm() {
const[firstName,setFirstName]=useState("First");
const[lastName,setLastName]=useState("Last");
const[fullName,setFullName]=useState("");
useEffect(()=>{
setFullName(firstName+" "+lastName);
},[firstName,lastName]);
return<p>{fullName}</p>;
}
```
**Correct (derive during render):**
```tsx
functionForm() {
const[firstName,setFirstName]=useState("First");
const[lastName,setLastName]=useState("Last");
constfullName=firstName+" "+lastName;
return<p>{fullName}</p>;
}
```
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect (requires state as dependency):**
```tsx
functionTodoList() {
const[items,setItems]=useState(initialItems);
// Callback must depend on items, recreated on every items change
constaddItems=useCallback(
(newItems: Item[])=>{
setItems([...items,...newItems]);
},
[items],
);// ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
constremoveItem=useCallback((id: string)=>{
setItems(items.filter((item)=>item.id!==id));
},[]);// ❌ Missing items dependency - will use stale items!
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct (stable callbacks, no stale closures):**
```tsx
functionTodoList() {
const[items,setItems]=useState(initialItems);
// Stable callback, never recreated
constaddItems=useCallback((newItems: Item[])=>{
setItems((curr)=>[...curr,...newItems]);
},[]);// ✅ No dependencies needed
// Always uses latest state, no stale closure risk
1.**Stable callback references** - Callbacks don't need to be recreated when state changes
2.**No stale closures** - Always operates on the latest state value
3.**Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4.**Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect (runs on every render):**
```tsx
functionFilteredList({items}:{items: Item[]}){
// buildSearchIndex() runs on EVERY render, even after initialization
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
impact: MEDIUM
impactDescription: restores memoization by using a constant for default value
tags: rerender, memo, optimization
---
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
To address this issue, extract the default value into a constant.
**Incorrect (`onClick` has different values on every rerender):**
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
impactDescription: prevents remount on every render
tags: rerender, components, remount, performance
---
## Don't Define Components Inside Components
**Impact: HIGH (prevents remount on every render)**
Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
A common reason developers do this is to access parent variables without passing props. Always pass props instead.
Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.
title: Do not wrap a simple expression with a primitive result type in useMemo
impact: LOW-MEDIUM
impactDescription: wasted computation on every render
tags: rerender, useMemo, optimization
---
## Do not wrap a simple expression with a primitive result type in useMemo
When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.
This pattern also applies to `useEffect` when combining unrelated side effects:
**Incorrect (both effects run when either dependency changes):**
```tsx
useEffect(()=>{
analytics.trackPageView(pathname);
document.title=`${pageTitle} | My App`;
},[pathname,pageTitle]);
```
**Correct (effects run independently):**
```tsx
useEffect(()=>{
analytics.trackPageView(pathname);
},[pathname]);
useEffect(()=>{
document.title=`${pageTitle} | My App`;
},[pageTitle]);
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.
## Use useDeferredValue for Expensive Derived Renders
When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.
**Incorrect (input feels laggy while filtering):**
impactDescription: avoids unnecessary re-renders on frequent updates
tags: rerender, useref, state, performance
---
## Use useRef for Transient Values
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import{LRUCache}from"lru-cache";
constcache=newLRUCache<string,any>({
max: 1000,
ttl: 5*60*1000,// 5 minutes
});
exportasyncfunctiongetUser(id: string){
constcached=cache.get(id);
if(cached)returncached;
constuser=awaitdb.user.findUnique({where:{id}});
cache.set(id,user);
returnuser;
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
If you must pass objects, pass the same reference:
```typescript
constparams={uid: 1};
getUser(params);// Query runs
getUser(params);// Cache hit (same reference)
```
**Next.js-Specific Note:**
In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
- Database queries (Prisma, Drizzle, etc.)
- Heavy computations
- Authentication checks
- File system operations
- Any non-fetch async work
Use `React.cache()` to deduplicate these operations across your component tree.
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
**Impact: HIGH (avoids repeated file/network I/O per request)**
When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
**Incorrect (reads font file on every request):**
```typescript
// app/api/og/route.tsx
import{ImageResponse}from'next/og'
exportasyncfunctionGET(request: Request){
// Runs on EVERY request - expensive!
constfontData=awaitfetch(
newURL('./fonts/Inter.ttf',import.meta.url)
).then(res=>res.arrayBuffer())
constlogoData=awaitfetch(
newURL('./images/logo.png',import.meta.url)
).then(res=>res.arrayBuffer())
returnnewImageResponse(
<divstyle={{fontFamily:'Inter'}}>
<imgsrc={logoData}/>
HelloWorld
</div>,
{fonts:[{name:'Inter',data: fontData}]}
)
}
```
**Correct (loads once at module initialization):**
```typescript
// app/api/og/route.tsx
import{ImageResponse}from'next/og'
// Module-level: runs ONCE when module is first imported
- Reading configuration files that don't change at runtime
- Loading email templates or other static templates
- Any static asset that's the same across all requests
When not to use this pattern:
- Assets that vary per request or user
- Files that may change during runtime (use caching with TTL instead)
- Large files that would consume too much memory if kept loaded
- Sensitive data that shouldn't persist in memory
With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
MediaGo is a pnpm/turborepo monorepo. Feature apps live in `apps/` (`frontend-main`, `frontend-mobile`, `backend-web`, `backend-electron`) for the user surfaces and API. Reusable logic stays in `packages/` (`shared` for cross-runtime helpers, `backend` for orchestration, `main` for Electron packaging). Long-form docs and assets sit in `docs/`, `images/`, and `docker/`. End-to-end checks live in `tests/`.
## Build, Test, and Development Commands
Run `pnpm install` once per clone. Use `pnpm dev` for the unified desktop + web experience, or scope to `pnpm dev:web` / `pnpm dev:electron`. `pnpm build` triggers the production Turborepo pipeline; `pnpm build:web-release` plus `pnpm build:docker` produce the deployable web bundle. Keep the codebase healthy with `pnpm lint`, `pnpm lint:fix`, `pnpm format`, and verify types through `pnpm types`.
## Coding Style & Naming Conventions
Target modern TypeScript with ES modules, two-space indentation, UTF-8, and LF endings per `.editorconfig`. Components, hooks, and services adopt PascalCase (e.g. `UserPreferencesPanel.tsx`). Utilities and helpers stay camelCase, and constants use SCREAMING_SNAKE_CASE. Always run `pnpm format` before committing; reserve comments for clarifying complex logic.
## Testing Guidelines
Integration suites live under `tests/*.test.ts` and execute via `pnpm test` using the Node `tsx` runner. Name files descriptively like `download.queue.integration.test.ts`. Mock external services, prefer shared fixtures in `tests/fixtures/`, and cover happy path, recovery, and edge behaviors when touching runtime code.
## Commit & Pull Request Guidelines
Follow Conventional Commits (e.g. `feat(frontend-main): add download queue UI`) and use `pnpm commit` (Commitizen) to stay compliant. Pull requests should summarize the change, link issues with `Closes #123`, note local test runs (`pnpm test`), and attach screenshots or recordings for UI updates. Call out new environment variables, migrations, or follow-up tasks so reviewers can reproduce the setup quickly.
pnpm deps:download:all # Download tools for all platforms
pnpm lint # Lint with oxlint
pnpm lint:fix # Auto-fix lint issues
pnpm format # Format with oxfmt
pnpm format:check # Check formatting without modifying
pnpm check # Full check: lint + format + type check
pnpm type:check # TypeScript type checking via Turborepo
pnpm pack:electron # Build + package Electron distributable
```
Commits use Conventional Commits format (e.g. `feat(electron): add queue UI`).
## Architecture
### Monorepo Layout
**Apps:**
- **`apps/core/`** — Go (Gin) REST API backend for download orchestration. Runs on port 9900. Uses SQLite (GORM), SSE for real-time events, PTY for capturing download tool output. Built with Gulp + Go cross-compilation.
- **`apps/electron/`** — Electron main process (tsdown build, inversify DI). Launches Go Core via `@mediago/service-runner`.
- **`apps/server/`** — Node.js launcher (tsdown build). Spawns Go Core via `@mediago/service-runner`.
- **`apps/ui/`** — Shared React 19 frontend (Vite 8, Ant Design 6, Zustand, TailwindCSS 4, i18next). Used by both Electron and server targets.
- **`apps/player-ui/`** — React 19 frontend for player (Vite 8, shadcn/ui, video.js, TailwindCSS 4). Built assets are embedded into Go Core via `//go:embed`.
**Packages:**
- **`packages/shared/common/`** — Platform-agnostic shared types, constants, and utilities
- **`packages/core-sdk/`** — TypeScript SDK for Go Core REST API (Axios, SSE via eventsource)
- **`packages/electron-preload/`** — Electron preload scripts for IPC bridge
- **`packages/browser-extension/`** — Browser extension (Lit web components)
The `APP_TARGET` env var (`electron` | `server`) controls which backend the UI builds against. Both targets share the same React UI but connect via different transports:
- **Electron**: IPC bridge (preload) + Go Core direct (via `@mediago/core-sdk`)
- **Server/Web**: HTTP/WebSocket + Go Core direct (via `@mediago/core-sdk`)
The UI adapter layer (`apps/ui/src/hooks/adapters/`) abstracts this: `electron.ts` provides IPC bridge in desktop mode, `platform-stubs.ts` provides no-op stubs in web mode, and `index.ts` exports `platformApi` which selects the appropriate adapter.
### Key Patterns
- **Go Core as subprocess**: Both Electron and server apps launch Go Core via `@mediago/service-runner`, which manages the process lifecycle and port allocation
- **Dependency Injection**: inversify with `@inversifyjs/binding-decorators` in Electron backend
- **State Management**: Zustand in the UI
- **Real-time events**: Go Core emits SSE events (`/api/events`); the UI's `api/events.ts` subscribes and dispatches to React via a listener pattern
- **TypeScript**: Strict mode with experimental decorators and decorator metadata enabled
- **Module format**: ES Modules everywhere
## Tooling
- **Package manager**: pnpm 10.15.0 (enforced via `packageManager` field)
- **Build orchestration**: Turborepo
- **App bundling**: tsdown for Node/Electron, Vite 8 for UI apps
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.