Compare commits

...

186 Commits

Author SHA1 Message Date
caorushizi d654368290 feat(core): add cobra cli entrypoint 2026-05-17 03:37:28 +08:00
y2hlbg ab67f6b409 docs: remove commercial entrypoints
Deploy MediaGo Docs / Build Docs (push) Failing after 1s
2026-04-28 19:14:58 +08:00
y2hlbg 3eb7f13eca docs(readme): update 52pojie credit note 2026-04-28 15:03:57 +08:00
y2hlbg 3b279e4a05 docs(readme): add 52pojie credit note 2026-04-28 15:03:57 +08:00
caorushizi ed03781692 fix: package.json name 2026-04-28 00:28:27 +08:00
y2hlbg fb9dd15186 fix(docs): restore canonical site url 2026-04-26 20:52:58 +08:00
y2hlbg 115572c513 feat(docs): add seo blog pillar pages 2026-04-26 20:24:22 +08:00
y2hlbg 1aa2bcd511 docs: add community links to readme 2026-04-23 10:08:23 +08:00
Charlie 248237d593 feat(i18n): add Italian localization (#646) 2026-04-23 09:04:18 +08:00
caorushizi 99281d89f9 docs: refresh VitePress homepages and add download API reference
A batch of VitePress site updates around the v3.5.0 release:

- Refresh the zh / en / jp homepages into a 3x3 feature grid aligned
  with README v3.5.0: keep the foundational "no packet capture /
  batch download / mobile playback" cards, add v3.5.0 pillars
  (browser extension / broad video source coverage / OpenClaw Skill /
  HTTP API / built-in format conversion / one-line Docker). Rewrite
  the hero with a tighter "cross-platform video downloader + built-in
  sniffing" tagline and point the English hero at the
  English-localized `home_en.png`.
- Fix en / jp hero action links that previously routed to the Chinese
  root pages (`/guides` → `/en/guides`, `/jp/guides` etc.).
- Add the macOS usage block to the en / jp quick-start guides
  (Intel vs Apple Silicon + `sudo xattr -dr com.apple.quarantine`
  unblock command), and drop the QQ feedback group tip from those
  locales — QQ is China-only, not useful for en / jp audiences.
- Add a new Download API reference page (`/api`) for all three locales,
  registered in the sidebar under "开始 / Quick start / 始める".
  Covers Base URL (desktop `39719`, Docker `9900`), auth (API key from
  Settings page), curl quick-start, download SSE events, and the full
  `/api/downloads/*` endpoint reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 01:12:17 +08:00
caorushizi aa24ef5966 docs(readme): use English-localized screenshots in English README
The English README was reusing the Chinese-localized screenshots
(`home.png`, `home-dark.png`, `settings.png`, `browser.png`), which
left English readers staring at a UI they could not read. Ship a
parallel set of English screenshots under the `_en` suffix and point
`README.md` at them; `README.zh.md` / `README.jp.md` keep their own
assets untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:29:58 +08:00
caorushizi e94b91f2f7 docs(i18n): add TRANSLATION.md contribution guide
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>
2026-04-23 00:13:22 +08:00
Charlie fc99be56df Merge pull request #640 from caorushizi/fix/installer-metadata
fix(installer): show version in title bar, set installer FileDescription (#637)
2026-04-22 23:40:23 +08:00
Charlie 34bbe42600 Merge pull request #639 from bovirus/master
Add Italian langauge
2026-04-22 23:36:50 +08:00
caorushizi dfcdf6a6a5 fix(installer): show version in title bar, set installer FileDescription (#637)
- `installer/installer.nsh`: customHeader macro sets Caption to
  "Setup - ${PRODUCT_NAME} ${VERSION}" so users can see which release
  they're installing from the window title (the default $(^SetupCaption)
  omits the version, and re-setting Name trips NSIS warning 6029 which
  electron-builder's -WX flag treats as a hard error).
- `scripts/build.ts`: afterAllArtifactBuild hook runs the app-builder
  rcedit helper on the generated NSIS installer to rewrite its
  FileDescription to "${APP_NAME} installer". electron-builder's
  NsisTarget.computeVersionKey() hardcodes VIAddVersionKey /LANG=1033
  "FileDescription" "${appInfo.description}", binding the installer's
  FileDescription to the app binary's (both drawn from package.json
  description); any in-NSIS override collides on the same LANG+key
  with a hard "already defined!" error that -WX does not gate.
  Post-processing with rcedit sidesteps this and lets installer and
  app binary carry distinct descriptions — same approach VS Code's
  Inno Setup pipeline uses (where "{AppName} Setup" is the default).

Closes #637

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:23:51 +08:00
bovirus f395d49a10 GUI - Add Italian language 2026-04-22 17:17:10 +02:00
bovirus f6a228f318 Browser extension - Add italian language 2026-04-22 16:41:46 +02:00
caorushizi ba958b9f44 docs: flatten Docker feature-section command to a single line
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>
2026-04-22 17:15:15 +08:00
caorushizi 19e49e89b9 docs: list Docker Hub alongside GHCR in README and docs
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>
2026-04-22 17:09:57 +08:00
caorushizi 3f889da389 fix(ci): move secrets check off if: to dodge Unrecognized named-value: 'secrets'
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>
2026-04-22 16:46:52 +08:00
caorushizi ab669f2e74 ci(docker): mirror image to Docker Hub alongside GHCR
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>
2026-04-22 16:10:33 +08:00
caorushizi 5386bfc8a2 docs: bump guides / changelog to v3.5.0 across zh / en / jp
`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>
2026-04-22 16:10:13 +08:00
caorushizi aa998d9a3a chore: add build:docker npm script for local image builds
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>
2026-04-22 15:11:37 +08:00
caorushizi dde75321a7 fix(docker): copy extra/ into build context so aria2 ships in the image
`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>
2026-04-22 15:11:23 +08:00
caorushizi d23f594b88 ci(docker): allow rebuilding an existing tag via workflow_dispatch
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>
2026-04-22 14:36:30 +08:00
caorushizi b1cbb74f32 fix(docker): use --deps-dir and move CMD flags into entrypoint script
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>
2026-04-22 14:24:50 +08:00
caorushizi f01cae6554 docs: rewrite README (zh/en/jp) around user-facing features, extract dev setup
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>
2026-04-22 14:14:28 +08:00
caorushizi 74b67101ae feat(extension): pin manifest version to the Desktop app version
Build & Push Docker Image / Build and push multi-arch image (push) Failing after 1s
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>
2026-04-21 18:16:07 +08:00
caorushizi 3279f40541 v3.5.0 2026-04-21 18:16:07 +08:00
caorushizi 62331e5c7b fix(ui,shared): refresh download list on externally-created tasks
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>
2026-04-21 17:11:37 +08:00
caorushizi d9c3d8ae1d fix(ui): let users copy text from the download-log terminal
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>
2026-04-21 17:11:16 +08:00
caorushizi c34812841e fix(core): sanitize filenames at task creation to prevent path-separator injection
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>
2026-04-21 17:10:54 +08:00
caorushizi 28e2c58366 fix(node-service): fail fast when the preferred port is already bound
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>
2026-04-21 17:10:30 +08:00
caorushizi a9e3ed6a22 feat(core): switch direct downloader from gopeed to aria2
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>
2026-04-21 17:09:35 +08:00
caorushizi 48aabc9a7d v3.5.0-beta.2 2026-04-21 14:26:34 +08:00
caorushizi b72b39ad7e fix(node-service): bind to 0.0.0.0 when internal=false so localhost reaches the server
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>
2026-04-21 13:56:22 +08:00
caorushizi f04989e973 feat(extension): bundle into desktop app with settings entry and docs
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>
2026-04-21 13:56:05 +08:00
caorushizi e5bcd7a224 chore(core): translate build-script console logs to English
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>
2026-04-21 13:55:34 +08:00
caorushizi fbf31d706c style(extension): apply Cursor-inspired warm palette and typography
Reskin popup + options to match Cursor's design language — warm off-
white over cream surfaces, gothic/serif/mono three-voice typography,
oklab borders, diffused atmospheric shadows. Preserves every feature;
only visuals change.

Palette & tokens (globals.css):
- Light: `#f2f1ed` page, `#26251e` text, `#e6e5e0` card, `#cf2d56`
  crimson for hover, `#f54e00` orange accent, surface-100..500 scale,
  AI-timeline colors (peach / sage / blue / lavender)
- Dark: warm-offset variant (`#1e1d16` page, `#ebeae5` text) — no
  cold neutral inversion
- Borders: `oklab(0.263084 -0.00230259 0.0124794 / 0.1)` perceptually
  uniform warm brown
- Shadows: ambient (16/8px), elevated (28/70px + oklab ring), focus
  (4/12px) — all token-driven
- Radius scale: 4px inline / 8px default / 10px featured / 14px breathing
- Motion: `--transition-color: 150ms ease` / `--transition-shadow: 200ms`

Fonts (locally bundled variable woff2, no remote fetch):
- Geist Variable (→ CursorGothic analogue)
- Source Serif 4 Variable (→ jjannon analogue)
- JetBrains Mono Variable (→ berkeleyMono analogue)
- OpenType feat utilities: `.font-feat-display` (ss09/ss01) and
  `.font-feat-editorial` (cv11/liga)
- Global h1/h2/h3 letter-spacing follows Cursor's size curve
  (-0.03em / -0.02em / -0.0125em)

Components:
- Button — warm-primary (hover → crimson text) + dark / outline /
  secondary-pill / tertiary-pill / ghost / light-surface / link
- Badge — full pill + timeline-color variants mapped to DownloadType
  (thinking/grep/read/edit/mediago) via `variantForDownloadType()`
- Card — ambient by default; `elevated` for permanent lift;
  `interactive` eases ambient → elevated on hover (used on ServerCard,
  the page's primary action target)
- Sonner / Switch / Input / RadioGroup — warm-palette rewrites with
  oklab focus rings and serif descriptions where appropriate

Pages:
- Popup — tonal bands (header cream → page-info surface-100 → list
  bg → footer surface-100), source type badges colored by DownloadType
- Options — responsive hero (40 → 52 → 60px with scaled letter-
  spacing), decorative blur glows behind headline, "cream → bg →
  cream" section rhythm with a closing surface-200 strip, per-card
  mono chapter dots in timeline colors, RuleListCard redone as a
  vertical timeline echoing Cursor's AI-step visualisation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:16:43 +08:00
caorushizi b782329ae2 feat(extension): add i18n with system/zh/en language toggle
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>
2026-04-21 11:16:03 +08:00
caorushizi 65e20638ff feat(extension): expose silent / downloadNow knobs in options
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>
2026-04-15 23:58:44 +08:00
caorushizi 19048f6ead feat(extension): add MediaGo browser extension for media URL sniffing
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>
2026-04-15 23:54:31 +08:00
caorushizi b29a5f1748 perf(settings): mount cards one per frame to break up navigation jank
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>
2026-04-15 06:03:33 +08:00
caorushizi 8783d949ba feat(favorite): optional title with auto-fetch and URL-first form
- 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>
2026-04-15 05:28:35 +08:00
caorushizi bee6bd1cbd fix(ui,core): drive download badge via SSE and polish favorite error UX
- 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>
2026-04-15 05:28:22 +08:00
caorushizi bdea15ace9 fix(electron): rebuild tray menu on language change
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>
2026-04-15 04:01:20 +08:00
caorushizi 72acd2ca94 refactor(ui,electron): fix "system" language fallback and rework settings layout
- 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>
2026-04-15 03:46:38 +08:00
caorushizi ba5f389dbe fix: upgrade zustand to stable and fix broken player build scripts
- 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>
2026-04-14 04:22:05 +08:00
caorushizi aaa5ee69d2 docs: fix documentation drift in CLAUDE.md
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>
2026-04-13 16:21:14 +08:00
dependabot[bot] fb671d5790 chore(deps): bump axios from 1.14.0 to 1.15.0
Build & Push Docker Image / Build and push multi-arch image (push) Failing after 0s
Bumps [axios](https://github.com/axios/axios) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 00:30:13 +08:00
caorushizi e32bfd1cb2 v3.5.0-beta.1 2026-04-13 00:27:24 +08:00
caorushizi 50a1a90e6a docs: rewrite README in English and polish UI translations
- 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>
2026-04-13 00:19:14 +08:00
caorushizi 2406b5b617 chore(electron,server): remove verbose startup logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 913d645d89 fix(electron): resolve deprecation warnings and unhandled promise rejection
- 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>
2026-04-13 00:19:14 +08:00
caorushizi c7dd925a34 feat(core): add yt-dlp plugin and consolidate binary paths into --deps-dir
- 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>
2026-04-13 00:19:14 +08:00
caorushizi c289d3690f refactor(browser-extension): remove dead FloatButton, jQuery and Lit dependencies
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>
2026-04-13 00:19:14 +08:00
caorushizi 8224c68f66 refactor(ui): optimize source-extract page and browser navigation flow
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>
2026-04-13 00:19:14 +08:00
caorushizi 6571fc022d refactor(ipc): namespace IPC events and consolidate similar methods
Replace flat IPC string constants with namespaced groups (browser.*,
app.*, dialog.*, shell.*, contextMenu.*, update.*) and consolidate
similar operations into generic methods:
- dialog.open/save replaces selectFile, selectDownloadDir, export/import
- shell.open replaces openDir, openUrl, openBrowser
- contextMenu.show replaces 3 separate context menu handlers
- on/off replaces rendererEvent/removeEventListener

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 00:19:14 +08:00
caorushizi 109c09a911 refactor(electron): replace screenshot hack with overlay WebContentsView for download dialog
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>
2026-04-13 00:19:14 +08:00
caorushizi 90d7fe578f feat(core): upgrade mediago-core to v0.2.1 and adapt log parsing
- 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>
2026-04-13 00:19:14 +08:00
caorushizi 82f948cdd9 fix(ui): fix openDir using wrong field names and antd 6 form warnings
- 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>
2026-04-13 00:19:14 +08:00
caorushizi 25d149e9ef feat(core): integrate mediago-core as new download type
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>
2026-04-13 00:19:14 +08:00
caorushizi 8d757ba18d docs: fix docker image tag (remove v prefix)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:20:01 +08:00
caorushizi 4f1c9fe65c docs: add OpenClaw Skill to README intro, docs homepage and guides
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:17:23 +08:00
caorushizi 78e3f96e3a docs: update README and guides for v3.5.0-beta.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:10:53 +08:00
caorushizi 0c377b7cb8 chore: bump version to 3.5.0-beta.0
Build & Push Docker Image / Build and push multi-arch image (push) Failing after 0s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:56:51 +08:00
caorushizi c2db365729 fix(core): add User-Agent and Referer for Bilibili title fetching
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>
2026-04-03 16:12:42 +08:00
caorushizi 7e21097175 fix(core): prioritize CLI --local-dir over appStore value
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>
2026-04-03 15:45:56 +08:00
caorushizi 0a36e39156 fix(core): whitelist static assets and SPA routes in auth middleware
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>
2026-04-03 12:34:16 +08:00
caorushizi d178fa4510 chore: update pnpm-lock.yaml
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:00:36 +08:00
caorushizi 8ca4d443c4 feat(skills,docs,ui): add OpenClaw Skill with docs and settings tab
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>
2026-04-03 11:54:59 +08:00
caorushizi a9075606ad feat(skills): add mediago video download skill for Claude Code
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>
2026-04-03 02:10:16 +08:00
caorushizi 05da1e1a75 fix(core): persist default download directory to config store
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>
2026-04-03 01:35:48 +08:00
caorushizi b5e0456786 fix(ui): fix auth redirect using wrong field name from API
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>
2026-04-03 01:29:26 +08:00
caorushizi 218603ff9f refactor(video): use ID-based video streaming and fix playback issues
- 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>
2026-04-03 01:22:15 +08:00
caorushizi 6f19d08a08 fix(electron): bind core service to LAN address for mobile access
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>
2026-04-03 00:52:11 +08:00
caorushizi 5079180a31 refactor(server): use APP_NAME from env for data root directory
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>
2026-04-03 00:46:44 +08:00
caorushizi b291e30b13 docs(docker,server): unify data paths under single root directory
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>
2026-04-03 00:40:11 +08:00
caorushizi 9428f52f7d chore(ci): upgrade Node.js from 20 to 24 LTS
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>
2026-04-03 00:12:47 +08:00
caorushizi 0be4e5b9e6 chore(ci): upgrade GitHub Actions to Node 24 compatible versions
Upgrade all actions to eliminate Node.js 20 deprecation warnings:
- actions/checkout v5 → v6
- actions/setup-node v4 → v6
- actions/setup-go v5 → v6
- actions/upload-artifact v4 → v6
- pnpm/action-setup v4 → v5
- docker/setup-buildx-action v3 → v4
- docker/login-action v3 → v4
- docker/metadata-action v5 → v6
- docker/build-push-action v6 → v7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:09:47 +08:00
caorushizi 5f8b1442c9 refactor(service-runner): remove portfinder, use fixed port
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>
2026-04-03 00:01:37 +08:00
caorushizi b6c0c64dfa fix(docker): add libicu for N_m3u8DL-RE .NET runtime dependency
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>
2026-04-02 23:50:37 +08:00
caorushizi 548ca9cd46 fix(docker): fix build failures for UI, Go deps, and unzip
- 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>
2026-04-02 23:33:35 +08:00
caorushizi 5d1dc7716f fix(docker): include all workspace package.json files for pnpm install
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>
2026-04-02 23:21:04 +08:00
caorushizi e9795e1667 docs: add macOS installation tips for Intel and Apple Silicon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:15:22 +08:00
caorushizi 1dd39a6973 chore(docker): rewrite Dockerfile and CI for GHCR multi-arch builds
- 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>
2026-04-02 23:14:42 +08:00
caorushizi de3b805ea0 feat(core,player-ui): refactor video API to use database and fix playback
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>
2026-04-02 22:45:27 +08:00
caorushizi 452bbc3b7d refactor: merge player Go backend into core
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>
2026-04-02 21:05:42 +08:00
caorushizi ae50e1d995 feat: add node service 2026-04-02 20:34:37 +08:00
caorushizi 7fd898a23a fix(core,player): quote args with spaces for Windows shell mode
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>
2026-04-02 20:00:45 +08:00
caorushizi b7527a012a fix(core): fix Windows build by using array-based spawn for Go commands
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>
2026-04-02 19:52:58 +08:00
caorushizi 5349295df8 fix(player): fix go build ldflags parsing on Windows
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>
2026-04-02 19:05:27 +08:00
caorushizi 38a4f63269 fix(ci,player): fix Windows build and add deps download step
- 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>
2026-04-02 18:52:09 +08:00
caorushizi a6eae611af fix(ci): add Go and swag setup to electron build workflow
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>
2026-04-02 18:42:50 +08:00
caorushizi 6dbe57c625 chore: add binary paths to .gitignore after history cleanup
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>
2026-04-02 17:12:24 +08:00
曹儒士子 f567f4f4a9 Merge pull request #616 from caorushizi/feat/skills
chore: migrate from @typescript/native-preview to typescript ^6.0.2
2026-04-02 17:00:01 +08:00
caorushizi b36585b927 docs: update catcatch integration URL scheme for community edition
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>
2026-04-02 16:58:34 +08:00
caorushizi 7c2a0474cd fix(ui): fix download progress polling and taskItem reference error
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>
2026-04-02 16:57:01 +08:00
caorushizi 8d4903bdff refactor(electron): use dynamic app name for custom URL scheme
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>
2026-04-02 16:46:52 +08:00
caorushizi 7eb654b6a5 feat(core,ui): move password hashing to backend with bcrypt
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>
2026-04-02 16:27:09 +08:00
caorushizi 403e01b28a fix(ui): auto-inject apiKey from Zustand store on every request
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>
2026-04-02 15:21:44 +08:00
caorushizi da5fa76c04 fix(ui): call setHttpApiKey after signin to fix 401 errors
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>
2026-04-02 15:18:20 +08:00
caorushizi b2fa4d2fbc feat(ui): add response types to all api/ functions and SWR hooks
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>
2026-04-02 15:10:05 +08:00
caorushizi c0bbfa5b09 fix(ui): fix auth redirect in server mode
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>
2026-04-02 14:50:51 +08:00
caorushizi f6e59e864a fix(core): allow X-API-Key header in CORS config
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>
2026-04-02 14:47:04 +08:00
caorushizi ee669f94ab refactor(electron,ui): improve sidebar extract toggle and cleanup
- 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>
2026-04-02 13:28:19 +08:00
caorushizi fa79482682 fix(ui): fix converter page pagination layout
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>
2026-04-02 13:04:07 +08:00
caorushizi 69cd2e6394 feat(ui): add pagination to converter page
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>
2026-04-02 13:00:24 +08:00
caorushizi b363b3dd7d refactor(electron): remove getMachineId controller method
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>
2026-04-02 12:53:33 +08:00
caorushizi 23fc28805b feat(core,ui): generate machineId in Go Core instead of Electron IPC
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>
2026-04-02 12:50:36 +08:00
caorushizi ba9e22381b fix(ui): add unwrapResponse to usePlatform() IPC method returns
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>
2026-04-02 12:43:15 +08:00
caorushizi ec2f82db37 refactor(ui): replace monolithic useAPI() with domain-specific api/ + SWR hooks
Split the monolithic useAPI() hook into a clean layered architecture:

1. api/ layer: domain-specific axios request functions
   - download-task.ts, favorite.ts, conversion.ts, config.ts, auth.ts, util.ts
   - events.ts: native EventSource for Go Core SSE (no sdk dependency)

2. SWR hooks: thin wrappers with auto-caching and mutation
   - useFavorites(), useConversions(), useConfig(), useEnvPath(), useAuthApi()

3. usePlatform(): pure Electron IPC methods (Proxy-safe delegation)

4. http.ts: axios instance with setupHttp() + response interceptor

Go SSE events and Electron IPC events are now fully separated:
- Go SSE (download/config) → api/events.ts → onDownloadEvent/onConfigChanged
- Electron IPC (webview/update) → usePlatform() → addIpcListener

Deleted: use-api.ts, go-adapter.ts, go-event-bridge.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:39:55 +08:00
caorushizi 8f47bf93af feat: size 2026-04-02 00:15:20 +08:00
caorushizi 0bb8f93e94 fix(electron,core,ui): improve startup robustness and logging
- 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>
2026-04-01 23:12:00 +08:00
caorushizi 23108c342b fix(electron): ensure window always shows regardless of Go core startup errors
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>
2026-04-01 16:05:16 +08:00
caorushizi 43c11928fa feat(core,player): exclude swagger from prod builds; add chmod to binaries
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>
2026-04-01 13:22:16 +08:00
caorushizi 4b01faad42 fix(electron): copy Go binaries via extraResources instead of npm packages
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>
2026-04-01 12:56:35 +08:00
caorushizi 10beac128f fix: resolve TypeScript type errors 2026-04-01 12:26:48 +08:00
caorushizi 3f4dc0290e fix(lint): resolve all oxlint errors across codebase
- Merge duplicate 'node:child_process' imports (player/scripts/utils.ts)
- Remove unused catch parameter (ui/utils/tdapp.ts)
- Merge duplicate 'react' imports (tool-bar.tsx, source-extract/index.tsx)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:15:15 +08:00
caorushizi 38cb884b77 style: apply oxfmt formatting across codebase
Also fix pre-existing lint errors exposed by staging:
- Merge duplicate 'eventsource' imports in eventEmitter.ts
- Remove unused catch parameter in eventEmitter.ts
- Merge duplicate 'react' imports in app-side-bar.tsx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:12:49 +08:00
caorushizi e659fccfad feat(converter): add file modal with browse, format, quality fields
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>
2026-04-01 12:04:12 +08:00
caorushizi 21e232f042 feat: implement format conversion with ffmpeg in Go Core
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>
2026-04-01 11:38:14 +08:00
caorushizi e4aa62e6e4 fix: use function form for manualChunks (Vite 8 compat)
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>
2026-04-01 05:16:57 +08:00
caorushizi 36daf13822 perf(ui): optimize frontend performance for UI and player
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>
2026-04-01 05:14:13 +08:00
caorushizi 55833142bd perf: only poll /api/tasks while downloads are active
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>
2026-04-01 05:06:06 +08:00
caorushizi 350e02c0f0 chore: translate all Chinese comments to English
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>
2026-04-01 03:56:57 +08:00
caorushizi f2d150bb02 fix: prevent settings from reverting to defaults after restart
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>
2026-04-01 03:08:18 +08:00
caorushizi fff1259704 fix: lazily resolve getGoApi() in useAPI to avoid init-time crash
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>
2026-04-01 01:23:42 +08:00
caorushizi 9470f357ec fix: restore IS_SETUP constant used as SWR cache key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:22:40 +08:00
caorushizi c1b8d67849 chore: add vercel-react-best-practices skill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:21:02 +08:00
caorushizi 52c3cdfc60 refactor: split MediaGoApi into GoApi + PlatformApi, remove Proxy routing
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>
2026-04-01 01:17:48 +08:00
caorushizi 9156cf9cd9 fix: gate UI behind Go adapter init and auto-build core/player
- 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>
2026-04-01 00:52:38 +08:00
caorushizi 38fa71b44b fix: config persistence, download-now auto-start, and player integration
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>
2026-04-01 00:27:30 +08:00
caorushizi ce406767be docs: update CLAUDE.md with current architecture
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>
2026-04-01 00:27:18 +08:00
caorushizi 8a5ac39cc5 chore: migrate player-ui to oxlint and upgrade to React 19, Vite 8, TS 6
- Remove biome.json from apps/player (use root oxlint/oxfmt config)
- Remove eslint from apps/player-ui, switch lint script to oxlint
- Upgrade React 18 → 19, Vite 7 → 8, TypeScript 5.9 → 6.0
- Upgrade @vitejs/plugin-react 5 → 6, @types/react to v19
- Fix vite.config.ts: __dirname → import.meta.dirname (ESM compat)
- Fix tsconfig.app.json: remove deprecated baseUrl option

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:05:02 +08:00
caorushizi 906db36c81 chore: set author to caorushizi and license to MIT across all packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:18:16 +08:00
caorushizi 208f2095bf chore: update app name to mediago-community and disable mac signing
- 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>
2026-03-31 23:18:04 +08:00
caorushizi e6a8a76d32 chore: upgrade electron to v41 and related build tools
- Upgrade electron 35.7.5 → 41.1.0
- Upgrade electron-builder 26.0.12 → 26.8.1
- Upgrade electron-updater ^6.6.2 → ^6.8.3
- Add electron-builder-squirrel-windows 26.8.1 to resolve peer conflict
- Remove rebuild script (electron-rebuild no longer a dependency)
- Remove rebuild:electron script from root package.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:03:16 +08:00
caorushizi 41eda86917 chore: update deps, remove showTerminal web restriction, format code
- 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>
2026-03-31 22:53:42 +08:00
caorushizi ffd3170261 chore: remove unused native electron devDependencies
@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>
2026-03-31 22:43:45 +08:00
caorushizi 2425598c6a fix: fix download log display corruption in terminal
- 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>
2026-03-31 22:43:36 +08:00
caorushizi e413bb5f38 fix: update binaryResolver paths, deps versions, and clean up docker-empty
- Fix monorepo root resolution in electron binaryResolver
- Downgrade N_m3u8DL-RE to v0.3.0-beta, use gopeed-web assets
- Remove win32-arm64 ffmpeg entry
- Convert download-deps to ESM with explicit file reads
- Add tsx devDependency
- Remove unused .docker-empty/.gitkeep

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:50:52 +08:00
caorushizi 93432c5968 refactor: move player-ui from apps/player/ to apps/ for cleaner monorepo structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:49:19 +08:00
caorushizi a0c10de673 fix: update server binaryResolver and remove npm binary deps
- 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>
2026-03-31 21:20:36 +08:00
caorushizi ffc4940508 feat: update cross-references and add deps download script
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>
2026-03-31 21:17:42 +08:00
caorushizi 72241bffd4 chore: merge mediago-player and clean up
- Subtree add mediago-player into apps/player
- Remove npm/ directory (7 platform packages)
- Remove pnpm-workspace.yaml, pnpm-lock.yaml, turbo.json
- Rename ui/ to player-ui/ to avoid conflict with apps/ui
- Rename package to @mediago/player-build
- Rename player-ui package to @mediago/player-ui
- Simplify gulpfile.ts and tasks.ts (remove npm build/publish)
- Remove npm templates
- Remove turbo, biome, vite override from devDependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:13:11 +08:00
caorushizi b6d3f80f03 Merge commit '4c93a1bdfaae8a96cfac8594921c4354a03ab531' as 'apps/player' 2026-03-31 21:10:53 +08:00
caorushizi 4c93a1bdfa Squashed 'apps/player/' content from commit 102bd03c
git-subtree-dir: apps/player
git-subtree-split: 102bd03cac286179ab06c3527edea045023e6fb1
2026-03-31 21:10:53 +08:00
caorushizi 85c0ff01e8 chore: clean up mediago-core after subtree merge
- Move SDK to packages/core-sdk
- Remove npm/ directory (14 platform packages no longer needed)
- Remove .bin/ directory (third-party binaries, will download from GitHub)
- Remove pnpm-workspace.yaml, pnpm-lock.yaml
- Remove npm.ts build script and templates
- Simplify gulpfile.ts (remove npm build/publish tasks)
- Rename package to @mediago/core-build

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:10:39 +08:00
caorushizi a9129f4a3c Merge commit '84ed412899931d87a526bb0e8d391a9b9f60ce18' as 'apps/core' 2026-03-31 21:08:13 +08:00
caorushizi 84ed412899 Squashed 'apps/core/' content from commit 55da68f3
git-subtree-dir: apps/core
git-subtree-split: 55da68f3dc6415a2cefb53e6112d0e6e4c77f9be
2026-03-31 21:08:13 +08:00
caorushizi e5138360d8 chore: add go.work and update .gitignore for monorepo merge
Prepare for merging mediago-core and mediago-player into the monorepo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:07:32 +08:00
caorushizi e6441aae6d refactor: remove DownloadController and fix export/import file dialogs
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>
2026-03-31 20:04:47 +08:00
caorushizi a611919bcb refactor: remove shared-browser, shared-node, config packages
- shared-browser: inline createBrowserI18n into apps/ui/src/i18n/
- shared-node: move code into apps/electron by function:
  - i18n → core/i18n.ts
  - handle decorator → core/decorators.ts
  - registerControllerHandlers → core/registerControllerHandlers.ts
  - TYPES → types/symbols.ts
  - binaryResolver → utils/binaryResolver.ts
  - DownloaderServer/VideoServer → services/
  - copy binaryResolver to apps/server for resolveCoreBinaries
- config: move tsconfig.{base,app,node}.json to monorepo root

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 19:47:51 +08:00
caorushizi c0cf028c55 feat: fetch real env paths from Go Core for config/bin directories
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>
2026-03-31 19:13:23 +08:00
caorushizi d3142586f7 fix: resolve multiple runtime issues in web mode
- 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>
2026-03-31 18:47:02 +08:00
caorushizi 8e8e71bd6b feat: replace Node.js server with Go Core direct connection
- 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>
2026-03-31 17:30:44 +08:00
caorushizi 1a58a05ce4 fix: forward download failed/stopped events to UI for immediate status updates
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>
2026-03-31 14:55:23 +08:00
caorushizi 8c70945a40 feat: migrate controllers to Go backend API, remove TypeORM/dao/services layer
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>
2026-03-31 14:33:11 +08:00
caorushizi 515bbe9b8e feat: migrate config to Go backend as single source of truth
Electron:
- Add GoConfigCache for synchronous config reads, seeded from Go API
- Replace all ElectronStore AppStore reads with GoConfigCache
- SSE config-changed events drive platform side effects + IPC to UI
- Slim ElectronStore to window bounds only, with old config.json migration
- Fix useProxy handler passing boolean instead of proxy address
- Add missing audioMuted/enableMobilePlayer SSE handlers

Server:
- Add ServerConfigCache to eliminate per-request getConfig() HTTP calls
- Migrate auth middleware and controllers to use config cache
- Remove StoreService and conf dependency entirely

UI:
- Replace "store-change" full-dump IPC with incremental "config-changed"

Shared:
- Remove appStoreDefaults (no longer needed)
- Fix DownloadStatus.Watting → Pending typo
- Fix VideoServer.enableMobilePlayer inverted logic
- Simplify DownloadServiceOptions (Go reads its own config)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 14:28:50 +08:00
caorushizi 0e6b86b3a0 chore: remove unused .biomeignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:03:47 +08:00
caorushizi b2bcfebeea chore(ui): add design assets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:02:55 +08:00
caorushizi cc81040641 chore: optimize Turborepo task configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:24:24 +08:00
caorushizi 84102b4b00 docs: add CLAUDE.md for Claude Code guidance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:16:54 +08:00
caorushizi 3a565676e3 chore: migrate from Prettier to oxlint + oxfmt toolchain
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>
2026-03-27 17:51:09 +08:00
caorushizi 8f1df0c008 chore: switch from rolldown-vite to official vite 8 and upgrade plugin-react
- vite: npm:rolldown-vite@latest → ^8.0.3
- @vitejs/plugin-react: ^5.0.4 → ^6.0.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 17:09:54 +08:00
caorushizi e6c58bc2b7 chore: upgrade react to v19 and antd to v6
- react/react-dom: ^18.3.1 → ^19.2.4
- @types/react: ^18.3.3 → ^19.2.14
- @types/react-dom: ^18.3.0 → ^19.2.3
- antd: ^5.27.4 → ^6.3.4
- @ant-design/icons: ^6.1.0 → ^6.1.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 17:06:28 +08:00
caorushizi 217bdde7d8 chore: migrate from @typescript/native-preview to typescript ^6.0.2
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>
2026-03-27 16:57:48 +08:00
曹儒士子 299bd25c69 Refactor Footer.vue template structure 2026-02-24 14:42:53 +08:00
caorushizi deb3f9ff67 feat: banner 2026-01-16 02:08:48 +08:00
tiyongliu f9418857bd feat: 实现linux dark主题系统顶部topbar的颜色没随设置改变。 2026-01-07 17:28:48 +08:00
zhecydn 6294502641 Update tampermonkey.md
docs: add front matter for VitePress layout
2026-01-07 17:23:51 +08:00
zhecydn 336147166c Create tampermonkey.md
Create tampermonkey.md
2026-01-07 17:23:51 +08:00
caorushizi 9406d8c2cb v3.1.0-beta.1
Make MediaGo Docker Image / Build Docker Image (arm64, linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Make MediaGo Docker Image / Create Multi-Arch Manifest (push) Has been cancelled
Make MediaGo Docker Image / Build Docker Image (amd64, linux/amd64, ubuntu-latest) (push) Failing after 1s
2025-11-19 00:29:40 +08:00
caorushizi 99ceba5363 feat: true portable
fixes: #476
2025-11-19 00:21:08 +08:00
caorushizi 7dca0a1c81 feat: 支持修改每页的数量 2025-11-18 22:56:12 +08:00
Charlie 8fc92801ce fix: 嗅探自动更新,如果页面title变化,更新页面信息,展示到右侧嗅探列表 (#587)
Co-authored-by: 曹儒士子 <84996057@qq.com>
2025-11-18 21:59:30 +08:00
y2hlbg 506cd12fba fix: 增加归一化逻辑,解决进度条倒退问题 2025-11-18 21:56:24 +08:00
caorushizi 343c41a862 feat: 优化 schema 调起的逻辑 2025-11-18 21:51:07 +08:00
caorushizi a3f0658cf0 fix: null window error 2025-11-18 18:45:56 +08:00
caorushizi e33d695ca9 feat: 添加 sentry 2025-11-18 18:31:46 +08:00
Charlie cfe0644962 Merge pull request #585 from caorushizi/fix/issues_464
fix: 代理支持socks5
2025-11-18 17:04:16 +08:00
y2hlbg bc64464e20 fix: 代理支持socks5 2025-11-18 16:30:34 +08:00
caorushizi 3d4ae46e30 fix: 构建 dev 版本的 docker 2025-11-18 13:06:17 +08:00
caorushizi 0ff360e7d6 fix: download progress
fixes: #577
2025-11-18 12:16:00 +08:00
caorushizi 0ad5f54ed3 add gitcode badge 2025-11-07 08:07:21 +08:00
638 changed files with 78951 additions and 15634 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,127 @@
# React Best Practices
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
## Structure
- `rules/` - Individual rule files (one per rule)
- `_sections.md` - Section metadata (titles, impacts, descriptions)
- `_template.md` - Template for creating new rules
- `area-description.md` - Individual rule files
- `src/` - Build scripts and utilities
- `metadata.json` - Document metadata (version, organization, abstract)
- **`AGENTS.md`** - Compiled output (generated)
- **`test-cases.json`** - Test cases for LLM evaluation (generated)
## Getting Started
1. Install dependencies:
```bash
pnpm install
```
2. Build AGENTS.md from rules:
```bash
pnpm build
```
3. Validate rule files:
```bash
pnpm validate
```
4. Extract test cases:
```bash
pnpm extract-tests
```
## Creating a New Rule
1. Copy `rules/_template.md` to `rules/area-description.md`
2. Choose the appropriate area prefix:
- `async-` for Eliminating Waterfalls (Section 1)
- `bundle-` for Bundle Size Optimization (Section 2)
- `server-` for Server-Side Performance (Section 3)
- `client-` for Client-Side Data Fetching (Section 4)
- `rerender-` for Re-render Optimization (Section 5)
- `rendering-` for Rendering Performance (Section 6)
- `js-` for JavaScript Performance (Section 7)
- `advanced-` for Advanced Patterns (Section 8)
3. Fill in the frontmatter and content
4. Ensure you have clear examples with explanations
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
## Rule File Structure
Each rule file should follow this structure:
````markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---
## Rule Title Here
Brief explanation of the rule and why it matters.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example
```
````
**Correct (description of what's right):**
```typescript
// Good code example
```
Optional explanatory text after examples.
Reference: [Link](https://example.com)
## File Naming Convention
- Files starting with `_` are special (excluded from build)
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
- Section is automatically inferred from filename prefix
- Rules are sorted alphabetically by title within each section
- IDs (e.g., 1.1, 1.2) are auto-generated during build
## Impact Levels
- `CRITICAL` - Highest priority, major performance gains
- `HIGH` - Significant performance improvements
- `MEDIUM-HIGH` - Moderate-high gains
- `MEDIUM` - Moderate performance improvements
- `LOW-MEDIUM` - Low-medium gains
- `LOW` - Incremental improvements
## Scripts
- `pnpm build` - Compile rules into AGENTS.md
- `pnpm validate` - Validate all rule files
- `pnpm extract-tests` - Extract test cases for LLM evaluation
- `pnpm dev` - Build and validate
## Contributing
When adding or modifying rules:
1. Use the correct filename prefix for your section
2. Follow the `_template.md` structure
3. Include clear bad/good examples with explanations
4. Add appropriate tags
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
6. Rules are automatically sorted by title - no need to manage numbers!
## Acknowledgments
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
@@ -0,0 +1,148 @@
---
name: vercel-react-best-practices
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)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
| -------- | ------------------------- | ----------- | ------------ |
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-cheap-condition-before-await` - Check cheap sync conditions before awaiting flags or remote values
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-auth-actions` - Authenticate server actions like API routes
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-dedup-props` - Avoid duplicate serialization in RSC props
- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-parallel-nested-fetching` - Chain nested fetches per item in Promise.all
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
- `client-passive-event-listeners` - Use passive listeners for scroll
- `client-localstorage-schema` - Version and minimize localStorage data
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-memo-with-default-value` - Hoist default non-primitive props
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-derived-state-no-effect` - Derive state during render, not effects
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
- `rerender-split-combined-hooks` - Split hooks with independent dependencies
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
- `rerender-transitions` - Use startTransition for non-urgent updates
- `rerender-use-deferred-value` - Defer expensive renders to keep input responsive
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
- `rerender-no-inline-components` - Don't define components inside components
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
- `rendering-usetransition-loading` - Prefer useTransition for loading state
- `rendering-resource-hints` - Use React DOM resource hints for preloading
- `rendering-script-defer-async` - Use defer or async on script tags
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
- `js-flatmap-filter` - Use flatMap to map and filter in one pass
- `js-request-idle-callback` - Defer non-critical work to browser idle time
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`
@@ -0,0 +1,46 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Eliminating Waterfalls (async)
**Impact:** CRITICAL
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
## 2. Bundle Size Optimization (bundle)
**Impact:** CRITICAL
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
## 3. Server-Side Performance (server)
**Impact:** HIGH
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
## 4. Client-Side Data Fetching (client)
**Impact:** MEDIUM-HIGH
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
## 5. Re-render Optimization (rerender)
**Impact:** MEDIUM
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
## 6. Rendering Performance (rendering)
**Impact:** MEDIUM
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
## 7. JavaScript Performance (js)
**Impact:** LOW-MEDIUM
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Advanced patterns for specific cases that require careful implementation.
@@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example();
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example();
```
Reference: [Link to documentation or resource](https://example.com)
@@ -0,0 +1,55 @@
---
title: Store Event Handlers in Refs
impact: LOW
impactDescription: stable subscriptions
tags: advanced, hooks, refs, event-handlers, optimization
---
## Store Event Handlers in Refs
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
**Incorrect (re-subscribes on every render):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler);
return () => window.removeEventListener(event, handler);
}, [event, handler]);
}
```
**Correct (stable subscription):**
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const listener = (e) => handlerRef.current(e);
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener);
}, [event]);
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from "react";
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler);
useEffect(() => {
window.addEventListener(event, onEvent);
return () => window.removeEventListener(event, onEvent);
}, [event]);
}
```
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
@@ -0,0 +1,42 @@
---
title: Initialize App Once, Not Per Mount
impact: LOW-MEDIUM
impactDescription: avoids duplicate init in development
tags: initialization, useEffect, app-startup, side-effects
---
## Initialize App Once, Not Per Mount
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
function Comp() {
useEffect(() => {
loadFromStorage();
checkAuthToken();
}, []);
// ...
}
```
**Correct (once per app load):**
```tsx
let didInit = false;
function Comp() {
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)
@@ -0,0 +1,39 @@
---
title: useEffectEvent for Stable Callback Refs
impact: LOW
impactDescription: prevents effect re-runs
tags: advanced, hooks, useEffectEvent, refs, optimization
---
## useEffectEvent for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Incorrect (effect re-runs on every callback change):**
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300);
return () => clearTimeout(timeout);
}, [query, onSearch]);
}
```
**Correct (using React's useEffectEvent):**
```tsx
import { useEffectEvent } from "react";
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
const onSearchEvent = useEffectEvent(onSearch);
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300);
return () => clearTimeout(timeout);
}, [query]);
}
```
@@ -0,0 +1,38 @@
---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
## Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect (config waits for auth, data waits for both):**
```typescript
export async function GET(request: Request) {
const session = await auth();
const config = await fetchConfig();
const data = await fetchData(session.user.id);
return Response.json({ data, config });
}
```
**Correct (auth and config start immediately):**
```typescript
export async function GET(request: Request) {
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id),
]);
return Response.json({ data, config });
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
@@ -0,0 +1,37 @@
---
title: Check Cheap Conditions Before Async Flags
impact: HIGH
impactDescription: avoids unnecessary async work when a synchronous guard already fails
tags: async, await, feature-flags, short-circuit, conditional
---
## Check Cheap Conditions Before Async Flags
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
const someFlag = await getFlag();
if (someFlag && someCondition) {
// ...
}
```
**Correct:**
```typescript
if (someCondition) {
const someFlag = await getFlag();
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.
@@ -0,0 +1,82 @@
---
title: Defer Await Until Needed
impact: HIGH
impactDescription: avoids blocking unused code paths
tags: async, await, conditional, optimization
---
## Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect (blocks both branches):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId);
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true };
}
// Only this branch uses userData
return processUserData(userData);
}
```
**Correct (only blocks when needed):**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true };
}
// Fetch only when needed
const userData = await fetchUserData(userId);
return processUserData(userData);
}
```
**Another example (early return optimization):**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId);
const resource = await getResource(resourceId);
if (!resource) {
return { error: "Not found" };
}
if (!permissions.canEdit) {
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions);
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId);
if (!resource) {
return { error: "Not found" };
}
const permissions = await fetchPermissions(userId);
if (!permissions.canEdit) {
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions);
}
```
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).
@@ -0,0 +1,52 @@
---
title: Dependency-Based Parallelization
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, dependencies, better-all
---
## Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from "better-all";
const { user, config, profile } = await all({
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id);
},
});
```
**Alternative without extra dependencies:**
We can also create all the promises first, and do `Promise.all()` at the end.
```typescript
const userPromise = fetchUser();
const profilePromise = userPromise.then((user) => fetchProfile(user.id));
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise,
]);
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
@@ -0,0 +1,28 @@
---
title: Promise.all() for Independent Operations
impact: CRITICAL
impactDescription: 2-10× improvement
tags: async, parallelization, promises, waterfalls
---
## Promise.all() for Independent Operations
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
```
@@ -0,0 +1,99 @@
---
title: Strategic Suspense Boundaries
impact: HIGH
impactDescription: faster initial paint
tags: async, suspense, streaming, layout-shift
---
## Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect (wrapper blocked by data fetching):**
```tsx
async function Page() {
const data = await fetchData(); // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
);
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct (wrapper shows immediately, data streams in):**
```tsx
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
);
}
async function DataDisplay() {
const data = await fetchData(); // Only blocks this component
return <div>{data.content}</div>;
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
**Alternative (share promise across components):**
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData();
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // Unwraps the promise
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // Reuses the same promise
return <div>{data.summary}</div>;
}
```
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
**When NOT to use this pattern:**
- Critical data needed for layout decisions (affects positioning)
- SEO-critical content above the fold
- Small, fast queries where suspense overhead isn't worth it
- When you want to avoid layout shift (loading → content jump)
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
@@ -0,0 +1,60 @@
---
title: Avoid Barrel File Imports
impact: CRITICAL
impactDescription: 200-800ms import cost, slow builds
tags: bundle, imports, tree-shaking, barrel-files, performance
---
## Avoid Barrel File Imports
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
module.exports = {
experimental: {
optimizePackageImports: ["lucide-react", "@mui/material"],
},
};
```
```tsx
// 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
import Button from "@mui/material/Button";
import TextField from "@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.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
@@ -0,0 +1,37 @@
---
title: Conditional Module Loading
impact: HIGH
impactDescription: loads large data only when needed
tags: bundle, conditional-loading, lazy-loading
---
## Conditional Module Loading
Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== "undefined") {
import("./animation-frames.js")
.then((mod) => setFrames(mod.frames))
.catch(() => setEnabled(false));
}
}, [enabled, frames, setEnabled]);
if (!frames) return <Skeleton />;
return <Canvas frames={frames} />;
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
@@ -0,0 +1,49 @@
---
title: Defer Non-Critical Third-Party Libraries
impact: MEDIUM
impactDescription: loads after hydration
tags: bundle, third-party, analytics, defer
---
## Defer Non-Critical Third-Party Libraries
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from "@vercel/analytics/react";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from "next/dynamic";
const Analytics = dynamic(
() => import("@vercel/analytics/react").then((m) => m.Analytics),
{ ssr: false },
);
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}
```
@@ -0,0 +1,35 @@
---
title: Dynamic Imports for Heavy Components
impact: CRITICAL
impactDescription: directly affects TTI and LCP
tags: bundle, dynamic-import, code-splitting, next-dynamic
---
## Dynamic Imports for Heavy Components
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from "./monaco-editor";
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />;
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from "next/dynamic";
const MonacoEditor = dynamic(
() => import("./monaco-editor").then((m) => m.MonacoEditor),
{ ssr: false },
);
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />;
}
```
@@ -0,0 +1,46 @@
---
title: Preload Based on User Intent
impact: MEDIUM
impactDescription: reduces perceived latency
tags: bundle, preload, user-intent, hover
---
## Preload Based on User Intent
Preload heavy bundles before they're needed to reduce perceived latency.
**Example (preload on hover/focus):**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== "undefined") {
void import("./monaco-editor");
}
};
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
);
}
```
**Example (preload when feature flag is enabled):**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== "undefined") {
void import("./monaco-editor").then((mod) => mod.init());
}
}, [flags.editorEnabled]);
return (
<FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>
);
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
@@ -0,0 +1,78 @@
---
title: Deduplicate Global Event Listeners
impact: LOW
impactDescription: single listener for N components
tags: client, swr, event-listeners, subscription
---
## Deduplicate Global Event Listeners
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect (N instances = N listeners):**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [key, callback]);
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from "swr/subscription";
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>();
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set());
}
keyCallbacks.get(key)!.add(callback);
return () => {
const set = keyCallbacks.get(key);
if (set) {
set.delete(callback);
if (set.size === 0) {
keyCallbacks.delete(key);
}
}
};
}, [key, callback]);
useSWRSubscription("global-keydown", () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach((cb) => cb());
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
});
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut("p", () => {
/* ... */
});
useKeyboardShortcut("k", () => {
/* ... */
});
// ...
}
```
@@ -0,0 +1,77 @@
---
title: Version and Minimize localStorage Data
impact: MEDIUM
impactDescription: prevents schema conflicts, reduces storage size
tags: client, localStorage, storage, versioning, data-minimization
---
## Version and Minimize localStorage Data
Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
**Incorrect:**
```typescript
// No version, stores everything, no error handling
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
const data = localStorage.getItem("userConfig");
```
**Correct:**
```typescript
const VERSION = "v2";
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
} catch {
// Throws in incognito/private browsing, quota exceeded, or disabled
}
}
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`);
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}
// Migration from v1 to v2
function migrate() {
try {
const v1 = localStorage.getItem("userConfig:v1");
if (v1) {
const old = JSON.parse(v1);
saveConfig({
theme: old.darkMode ? "dark" : "light",
language: old.lang,
});
localStorage.removeItem("userConfig:v1");
}
} catch {}
}
```
**Store minimal fields from server responses:**
```typescript
// User object has 20+ fields, only store what UI needs
function cachePrefs(user: FullUser) {
try {
localStorage.setItem(
"prefs:v1",
JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications,
}),
);
} catch {}
}
```
**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
@@ -0,0 +1,48 @@
---
title: Use Passive Event Listeners for Scrolling Performance
impact: MEDIUM
impactDescription: eliminates scroll delay caused by event listeners
tags: client, event-listeners, scrolling, performance, touch, wheel
---
## 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.
**Incorrect:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener("touchstart", handleTouch);
document.addEventListener("wheel", handleWheel);
return () => {
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Correct:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener("touchstart", handleTouch, { passive: true });
document.addEventListener("wheel", handleWheel, { passive: true });
return () => {
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
@@ -0,0 +1,56 @@
---
title: Use SWR for Automatic Deduplication
impact: MEDIUM-HIGH
impactDescription: automatic deduplication
tags: client, swr, deduplication, data-fetching
---
## Use SWR for Automatic Deduplication
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect (no deduplication, each instance fetches):**
```tsx
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((r) => r.json())
.then(setUsers);
}, []);
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from "swr";
function UserList() {
const { data: users } = useSWR("/api/users", fetcher);
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from "@/lib/swr";
function StaticContent() {
const { data } = useImmutableSWR("/api/config", fetcher);
}
```
**For mutations:**
```tsx
import { useSWRMutation } from "swr/mutation";
function UpdateButton() {
const { trigger } = useSWRMutation("/api/user", updateUser);
return <button onClick={() => trigger()}>Update</button>;
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)
@@ -0,0 +1,110 @@
---
title: Avoid Layout Thrashing
impact: MEDIUM
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
tags: javascript, dom, css, performance, reflow, layout-thrashing
---
## Avoid Layout Thrashing
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.
**This is OK (browser batches style changes):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line invalidates style, but browser batches the recalculation
element.style.width = "100px";
element.style.height = "200px";
element.style.backgroundColor = "blue";
element.style.border = "1px solid black";
}
```
**Incorrect (interleaved reads and writes force reflows):**
```typescript
function layoutThrashing(element: HTMLElement) {
element.style.width = "100px";
const width = element.offsetWidth; // Forces reflow
element.style.height = "200px";
const height = element.offsetHeight; // Forces another reflow
}
```
**Correct (batch writes, then read once):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = "100px";
element.style.height = "200px";
element.style.backgroundColor = "blue";
element.style.border = "1px solid black";
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect();
}
```
**Correct (batch reads, then writes):**
```typescript
function avoidThrashing(element: HTMLElement) {
// Read phase - all layout queries first
const rect1 = element.getBoundingClientRect();
const offsetWidth = element.offsetWidth;
const offsetHeight = element.offsetHeight;
// Write phase - all style changes after
element.style.width = "100px";
element.style.height = "200px";
}
```
**Better: use CSS classes**
```css
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
```
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add("highlighted-box");
const { width, height } = element.getBoundingClientRect();
}
```
**React example:**
```tsx
// Incorrect: interleaving style changes with layout queries
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = "100px";
const width = ref.current.offsetWidth; // Forces layout
ref.current.style.height = "200px";
}
}, [isHighlighted]);
return <div ref={ref}>Content</div>;
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>;
}
```
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.
@@ -0,0 +1,80 @@
---
title: Cache Repeated Function Calls
impact: MEDIUM
impactDescription: avoid redundant computation
tags: javascript, cache, memoization, performance
---
## Cache Repeated Function Calls
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
**Incorrect (redundant computation):**
```typescript
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Correct (cached results):**
```typescript
// Module-level cache
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
)
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null;
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache;
}
isLoggedInCache = document.cookie.includes("auth=");
return isLoggedInCache;
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null;
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
@@ -0,0 +1,28 @@
---
title: Cache Property Access in Loops
impact: LOW-MEDIUM
impactDescription: reduces lookups
tags: javascript, loops, optimization, caching
---
## Cache Property Access in Loops
Cache object property lookups in hot paths.
**Incorrect (3 lookups × N iterations):**
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value);
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value);
}
```
@@ -0,0 +1,70 @@
---
title: Cache Storage API Calls
impact: LOW-MEDIUM
impactDescription: reduces expensive I/O
tags: javascript, localStorage, storage, caching, performance
---
## Cache Storage API Calls
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
**Incorrect (reads storage on every call):**
```typescript
function getTheme() {
return localStorage.getItem("theme") ?? "light";
}
// Called 10 times = 10 storage reads
```
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>();
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value);
storageCache.set(key, value); // keep cache in sync
}
```
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null;
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split("; ").map((c) => c.split("=")),
);
}
return cookieCache[name];
}
```
**Important (invalidate on external changes):**
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener("storage", (e) => {
if (e.key) storageCache.delete(e.key);
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
storageCache.clear();
}
});
```
@@ -0,0 +1,32 @@
---
title: Combine Multiple Array Iterations
impact: LOW-MEDIUM
impactDescription: reduces iterations
tags: javascript, arrays, loops, performance
---
## Combine Multiple Array Iterations
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
**Incorrect (3 iterations):**
```typescript
const admins = users.filter((u) => u.isAdmin);
const testers = users.filter((u) => u.isTester);
const inactive = users.filter((u) => !u.isActive);
```
**Correct (1 iteration):**
```typescript
const admins: User[] = [];
const testers: User[] = [];
const inactive: User[] = [];
for (const user of users) {
if (user.isAdmin) admins.push(user);
if (user.isTester) testers.push(user);
if (!user.isActive) inactive.push(user);
}
```
@@ -0,0 +1,50 @@
---
title: Early Return from Functions
impact: LOW-MEDIUM
impactDescription: avoids unnecessary computation
tags: javascript, functions, optimization, early-return
---
## Early Return from Functions
Return early when result is determined to skip unnecessary processing.
**Incorrect (processes all items even after finding answer):**
```typescript
function validateUsers(users: User[]) {
let hasError = false;
let errorMessage = "";
for (const user of users) {
if (!user.email) {
hasError = true;
errorMessage = "Email required";
}
if (!user.name) {
hasError = true;
errorMessage = "Name required";
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true };
}
```
**Correct (returns immediately on first error):**
```typescript
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: "Email required" };
}
if (!user.name) {
return { valid: false, error: "Name required" };
}
}
return { valid: true };
}
```
@@ -0,0 +1,55 @@
---
title: Use flatMap to Map and Filter in One Pass
impact: LOW-MEDIUM
impactDescription: eliminates intermediate array
tags: javascript, arrays, flatMap, filter, performance
---
## Use flatMap to Map and Filter in One Pass
**Impact: LOW-MEDIUM (eliminates intermediate array)**
Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
**Incorrect (2 iterations, intermediate array):**
```typescript
const userNames = users
.map((user) => (user.isActive ? user.name : null))
.filter(Boolean);
```
**Correct (1 iteration, no intermediate array):**
```typescript
const userNames = users.flatMap((user) => (user.isActive ? [user.name] : []));
```
**More examples:**
```typescript
// Extract valid emails from responses
// Before
const emails = responses
.map((r) => (r.success ? r.data.email : null))
.filter(Boolean);
// After
const emails = responses.flatMap((r) => (r.success ? [r.data.email] : []));
// Parse and filter valid numbers
// Before
const numbers = strings.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
// After
const numbers = strings.flatMap((s) => {
const n = parseInt(s, 10);
return isNaN(n) ? [] : [n];
});
```
**When to use:**
- Transforming items while filtering some out
- Conditional mapping where some inputs produce no output
- Parsing/validating where invalid inputs should be skipped
@@ -0,0 +1,45 @@
---
title: Hoist RegExp Creation
impact: LOW-MEDIUM
impactDescription: avoids recreation
tags: javascript, regexp, optimization, memoization
---
## Hoist RegExp Creation
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
**Incorrect (new RegExp every render):**
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
```
**Warning (global regex has mutable state):**
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g;
regex.test("foo"); // true, lastIndex = 3
regex.test("foo"); // false, lastIndex = 0
```
@@ -0,0 +1,37 @@
---
title: Build Index Maps for Repeated Lookups
impact: LOW-MEDIUM
impactDescription: 1M ops to 2K ops
tags: javascript, map, indexing, optimization, performance
---
## Build Index Maps for Repeated Lookups
Multiple `.find()` calls by the same key should use a Map.
**Incorrect (O(n) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map((order) => ({
...order,
user: users.find((u) => u.id === order.userId),
}));
}
```
**Correct (O(1) per lookup):**
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map((u) => [u.id, u]));
return orders.map((order) => ({
...order,
user: userById.get(order.userId),
}));
}
```
Build map once (O(n)), then all lookups are O(1).
For 1000 orders × 1000 users: 1M ops → 2K ops.
@@ -0,0 +1,50 @@
---
title: Early Length Check for Array Comparisons
impact: MEDIUM-HIGH
impactDescription: avoids expensive operations when lengths differ
tags: javascript, arrays, performance, optimization, comparison
---
## Early Length Check for Array Comparisons
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).
**Incorrect (always runs expensive comparison):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join();
}
```
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.
**Correct (O(1) length check first):**
```typescript
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true;
}
// Only sort when lengths match
const currentSorted = current.toSorted();
const originalSorted = original.toSorted();
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true;
}
}
return false;
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
- It returns early when a difference is found
@@ -0,0 +1,82 @@
---
title: Use Loop for Min/Max Instead of Sort
impact: LOW
impactDescription: O(n) instead of O(n log n)
tags: javascript, arrays, performance, sorting, algorithms
---
## Use Loop for Min/Max Instead of Sort
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
**Incorrect (O(n log n) - sort to find latest):**
```typescript
interface Project {
id: string;
name: string;
updatedAt: number;
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
return sorted[0];
}
```
Sorts the entire array just to find the maximum value.
**Incorrect (O(n log n) - sort for oldest and newest):**
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
}
```
Still sorts unnecessarily when only min/max are needed.
**Correct (O(n) - single loop):**
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null;
let latest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i];
}
}
return latest;
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null };
let oldest = projects[0];
let newest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
}
return { oldest, newest };
}
```
Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9];
const min = Math.min(...numbers);
const max = 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.
@@ -0,0 +1,106 @@
---
title: Defer Non-Critical Work with requestIdleCallback
impact: MEDIUM
impactDescription: keeps UI responsive during background tasks
tags: javascript, performance, idle, scheduling, analytics
---
## 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
function handleSearch(query: string) {
const results = 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
function handleSearch(query: string) {
const results = 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
requestIdleCallback(
() => analytics.track("page_view", { path: location.pathname }),
{ timeout: 2000 },
);
```
**Chunking large tasks:**
```typescript
function processLargeDataset(items: Item[]) {
let index = 0;
function processChunk(deadline: IdleDeadline) {
// Process items while we have idle time (aim for <50ms chunks)
while (index < items.length && deadline.timeRemaining() > 0) {
processItem(items[index]);
index++;
}
// Schedule next chunk if more items remain
if (index < items.length) {
requestIdleCallback(processChunk);
}
}
requestIdleCallback(processChunk);
}
```
**With fallback for unsupported browsers:**
```typescript
const scheduleIdleWork =
window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1));
scheduleIdleWork(() => {
// Non-critical work
});
```
**When to use:**
- Analytics and telemetry
- Saving state to localStorage/IndexedDB
- Prefetching resources for likely next actions
- Processing non-urgent data transformations
- Lazy initialization of non-critical features
**When NOT to use:**
- User-initiated actions that need immediate feedback
- Rendering updates the user is waiting for
- Time-sensitive operations
@@ -0,0 +1,24 @@
---
title: Use Set/Map for O(1) Lookups
impact: LOW-MEDIUM
impactDescription: O(n) to O(1)
tags: javascript, set, map, data-structures, performance
---
## Use Set/Map for O(1) Lookups
Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
```
@@ -0,0 +1,57 @@
---
title: Use toSorted() Instead of sort() for Immutability
impact: MEDIUM-HIGH
impactDescription: prevents mutation bugs in React state
tags: javascript, arrays, immutability, react, state, mutation
---
## 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.
**Incorrect (mutates original array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Correct (creates new array):**
```typescript
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
}
```
**Why this matters in React:**
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
**Browser support (fallback for older browsers):**
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value);
```
**Other immutable array methods:**
- `.toSorted()` - immutable sort
- `.toReversed()` - immutable reverse
- `.toSpliced()` - immutable splice
- `.with()` - immutable element replacement
@@ -0,0 +1,26 @@
---
title: Use Activity Component for Show/Hide
impact: MEDIUM
impactDescription: preserves state/DOM
tags: rendering, activity, visibility, state-preservation
---
## Use Activity Component for Show/Hide
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
**Usage:**
```tsx
import { Activity } from "react";
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? "visible" : "hidden"}>
<ExpensiveMenu />
</Activity>
);
}
```
Avoids expensive re-renders and state loss.
@@ -0,0 +1,38 @@
---
title: Animate SVG Wrapper Instead of SVG Element
impact: LOW
impactDescription: enables hardware acceleration
tags: rendering, svg, css, animation, performance
---
## Animate SVG Wrapper Instead of SVG Element
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
**Incorrect (animating SVG directly - no hardware acceleration):**
```tsx
function LoadingSpinner() {
return (
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
);
}
```
**Correct (animating wrapper div - hardware accelerated):**
```tsx
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
);
}
```
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.
@@ -0,0 +1,32 @@
---
title: Use Explicit Conditional Rendering
impact: LOW
impactDescription: prevents rendering 0 or NaN
tags: rendering, conditional, jsx, falsy-values
---
## Use Explicit Conditional Rendering
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect (renders "0" when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return <div>{count && <span className="badge">{count}</span>}</div>;
}
// When count = 0, renders: <div>0</div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
**Correct (renders nothing when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
}
// When count = 0, renders: <div></div>
// When count = 5, renders: <div><span class="badge">5</span></div>
```
@@ -0,0 +1,38 @@
---
title: CSS content-visibility for Long Lists
impact: HIGH
impactDescription: faster initial render
tags: rendering, css, content-visibility, long-lists
---
## CSS content-visibility for Long Lists
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map((msg) => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
);
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
@@ -0,0 +1,36 @@
---
title: Hoist Static JSX Elements
impact: LOW
impactDescription: avoids re-creation
tags: rendering, jsx, static, optimization
---
## Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation.
**Incorrect (recreates element every render):**
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />;
}
function Container() {
return <div>{loading && <LoadingSkeleton />}</div>;
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container() {
return <div>{loading && loadingSkeleton}</div>;
}
```
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.
@@ -0,0 +1,72 @@
---
title: Prevent Hydration Mismatch Without Flickering
impact: MEDIUM
impactDescription: avoids visual flicker and hydration errors
tags: rendering, ssr, hydration, localStorage, flicker
---
## Prevent Hydration Mismatch Without Flickering
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.
**Incorrect (breaks SSR):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem("theme") || "light";
return <div className={theme}>{children}</div>;
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect (visual flickering):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState("light");
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem("theme");
if (stored) {
setTheme(stored);
}
}, []);
return <div className={theme}>{children}</div>;
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct (no flicker, no hydration mismatch):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
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.
@@ -0,0 +1,26 @@
---
title: Suppress Expected Hydration Mismatches
impact: LOW-MEDIUM
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. Dont overuse it.
**Incorrect (known mismatch warnings):**
```tsx
function Timestamp() {
return <span>{new Date().toLocaleString()}</span>;
}
```
**Correct (suppress expected mismatch only):**
```tsx
function Timestamp() {
return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
}
```
@@ -0,0 +1,89 @@
---
title: Use React DOM Resource Hints
impact: HIGH
impactDescription: reduces load time for critical resources
tags: rendering, preload, preconnect, prefetch, resource-hints
---
## Use React DOM Resource Hints
**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";
export default function App() {
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";
export default function RootLayout({ 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):**
```tsx
import { preloadModule, preinitModule } from "react-dom";
function Navigation() {
const preloadDashboard = () => {
preloadModule("/dashboard.js", { as: "script" });
};
return (
<nav>
<a href="/dashboard" onMouseEnter={preloadDashboard}>
Dashboard
</a>
</nav>
);
}
```
**When to use each:**
| API | Use case |
| --------------- | ------------------------------------------- |
| `prefetchDNS` | Third-party domains you'll connect to later |
| `preconnect` | APIs or CDNs you'll fetch from immediately |
| `preload` | Critical resources needed for current page |
| `preloadModule` | JS modules for likely next navigation |
| `preinit` | Stylesheets/scripts that must execute early |
| `preinitModule` | ES modules that must execute early |
Reference: [React DOM Resource Preloading APIs](https://react.dev/reference/react-dom#resource-preloading-apis)
@@ -0,0 +1,71 @@
---
title: Use defer or async on Script Tags
impact: HIGH
impactDescription: eliminates render-blocking
tags: rendering, script, defer, async, performance
---
## Use defer or async on Script Tags
**Impact: HIGH (eliminates render-blocking)**
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.
**Incorrect (blocks rendering):**
```tsx
export default function Document() {
return (
<html>
<head>
<script src="https://example.com/analytics.js" />
<script src="/scripts/utils.js" />
</head>
<body>{/* content */}</body>
</html>
);
}
```
**Correct (non-blocking):**
```tsx
export default function Document() {
return (
<html>
<head>
{/* Independent script - use async */}
<script src="https://example.com/analytics.js" async />
{/* DOM-dependent script - use defer */}
<script src="/scripts/utils.js" defer />
</head>
<body>{/* content */}</body>
</html>
);
}
```
**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
```tsx
import Script from "next/script";
export default function Page() {
return (
<>
<Script
src="https://example.com/analytics.js"
strategy="afterInteractive"
/>
<Script src="/scripts/utils.js" strategy="beforeInteractive" />
</>
);
}
```
Reference: [MDN - Script element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
@@ -0,0 +1,28 @@
---
title: Optimize SVG Precision
impact: LOW
impactDescription: reduces file size
tags: rendering, svg, optimization, svgo
---
## Optimize SVG Precision
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
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
```
**Correct (1 decimal place):**
```svg
<path d="M 10.3 20.8 L 30.9 40.2" />
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```
@@ -0,0 +1,75 @@
---
title: Use useTransition Over Manual Loading States
impact: LOW
impactDescription: reduces re-renders and improves code clarity
tags: rendering, transitions, useTransition, loading, state
---
## Use useTransition Over Manual Loading States
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
**Incorrect (manual loading state):**
```tsx
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async (value: string) => {
setIsLoading(true);
setQuery(value);
const data = await fetchResults(value);
setResults(data);
setIsLoading(false);
};
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isLoading && <Spinner />}
<ResultsList results={results} />
</>
);
}
```
**Correct (useTransition with built-in pending state):**
```tsx
import { useTransition, useState } from "react";
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => {
setQuery(value); // Update input immediately
startTransition(async () => {
// Fetch and update results
const data = await fetchResults(value);
setResults(data);
});
};
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
);
}
```
**Benefits:**
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
- **Error resilience**: Pending state correctly resets even if the transition throws
- **Better responsiveness**: Keeps the UI responsive during updates
- **Interrupt handling**: New transitions automatically cancel pending ones
Reference: [useTransition](https://react.dev/reference/react/useTransition)
@@ -0,0 +1,39 @@
---
title: Defer State Reads to Usage Point
impact: MEDIUM
impactDescription: avoids unnecessary subscriptions
tags: rerender, searchParams, localStorage, optimization
---
## Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect (subscribes to all searchParams changes):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
```
**Correct (reads on demand, no subscription):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
```
@@ -0,0 +1,45 @@
---
title: Narrow Effect Dependencies
impact: LOW
impactDescription: minimizes effect re-runs
tags: rerender, useEffect, dependencies, optimization
---
## Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect (re-runs on any user field change):**
```tsx
useEffect(() => {
console.log(user.id);
}, [user]);
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id);
}, [user.id]);
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode();
}
}, [width]);
// Correct: runs only on boolean transition
const isMobile = width < 768;
useEffect(() => {
if (isMobile) {
enableMobileMode();
}
}, [isMobile]);
```
@@ -0,0 +1,40 @@
---
title: Calculate Derived State During Rendering
impact: MEDIUM
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
function Form() {
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
function Form() {
const [firstName, setFirstName] = useState("First");
const [lastName, setLastName] = useState("Last");
const fullName = firstName + " " + lastName;
return <p>{fullName}</p>;
}
```
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
@@ -0,0 +1,29 @@
---
title: Subscribe to Derived State
impact: MEDIUM
impactDescription: reduces re-render frequency
tags: rerender, derived-state, media-query, optimization
---
## Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect (re-renders on every pixel change):**
```tsx
function Sidebar() {
const width = useWindowWidth(); // updates continuously
const isMobile = width < 768;
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```
**Correct (re-renders only when boolean changes):**
```tsx
function Sidebar() {
const isMobile = useMediaQuery("(max-width: 767px)");
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```
@@ -0,0 +1,77 @@
---
title: Use Functional setState Updates
impact: MEDIUM
impactDescription: prevents stale closures and unnecessary callback recreations
tags: react, hooks, useState, useCallback, callbacks, closures
---
## Use Functional setState Updates
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
function TodoList() {
const [items, setItems] = useState(initialItems);
// Callback must depend on items, recreated on every items change
const addItems = useCallback(
(newItems: Item[]) => {
setItems([...items, ...newItems]);
},
[items],
); // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter((item) => item.id !== id));
}, []); // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```
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
function TodoList() {
const [items, setItems] = useState(initialItems);
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems((curr) => [...curr, ...newItems]);
}, []); // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems((curr) => curr.filter((item) => item.id !== id));
}, []); // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```
**Benefits:**
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.
@@ -0,0 +1,58 @@
---
title: Use Lazy State Initialization
impact: MEDIUM
impactDescription: wasted computation on every render
tags: react, hooks, useState, performance, initialization
---
## Use Lazy State Initialization
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
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
const [query, setQuery] = useState("");
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem("settings") || "{}"),
);
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
**Correct (runs only once):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
const [query, setQuery] = useState("");
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem("settings");
return stored ? JSON.parse(stored) : {};
});
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
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.
@@ -0,0 +1,36 @@
---
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):**
```tsx
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```
**Correct (stable default value):**
```tsx
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```
@@ -0,0 +1,44 @@
---
title: Extract to Memoized Components
impact: MEDIUM
impactDescription: enables early returns
tags: rerender, memo, useMemo, optimization
---
## Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect (computes avatar even when loading):**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user);
return <Avatar id={id} />;
}, [user]);
if (loading) return <Skeleton />;
return <div>{avatar}</div>;
}
```
**Correct (skips computation when loading):**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />;
return (
<div>
<UserAvatar user={user} />
</div>
);
}
```
**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.
@@ -0,0 +1,45 @@
---
title: Put Interaction Logic in Event Handlers
impact: MEDIUM
impactDescription: avoids effect re-runs and duplicate side effects
tags: rerender, useEffect, events, side-effects, dependencies
---
## Put Interaction Logic in Event Handlers
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.
**Incorrect (event modeled as state + effect):**
```tsx
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
post("/api/register");
showToast("Registered", theme);
}
}, [submitted, theme]);
return <button onClick={() => setSubmitted(true)}>Submit</button>;
}
```
**Correct (do it in the handler):**
```tsx
function Form() {
const theme = useContext(ThemeContext);
function handleSubmit() {
post("/api/register");
showToast("Registered", theme);
}
return <button onClick={handleSubmit}>Submit</button>;
}
```
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
@@ -0,0 +1,83 @@
---
title: Don't Define Components Inside Components
impact: HIGH
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.
**Incorrect (remounts on every render):**
```tsx
function UserProfile({ user, theme }) {
// Defined inside to access `theme` - BAD
const Avatar = () => (
<img
src={user.avatarUrl}
className={theme === "dark" ? "avatar-dark" : "avatar-light"}
/>
);
// Defined inside to access `user` - BAD
const Stats = () => (
<div>
<span>{user.followers} followers</span>
<span>{user.posts} posts</span>
</div>
);
return (
<div>
<Avatar />
<Stats />
</div>
);
}
```
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.
**Correct (pass props instead):**
```tsx
function Avatar({ src, theme }: { src: string; theme: string }) {
return (
<img
src={src}
className={theme === "dark" ? "avatar-dark" : "avatar-light"}
/>
);
}
function Stats({ followers, posts }: { followers: number; posts: number }) {
return (
<div>
<span>{followers} followers</span>
<span>{posts} posts</span>
</div>
);
}
function UserProfile({ user, theme }) {
return (
<div>
<Avatar src={user.avatarUrl} theme={theme} />
<Stats followers={user.followers} posts={user.posts} />
</div>
);
}
```
**Symptoms of this bug:**
- Input fields lose focus on every keystroke
- Animations restart unexpectedly
- `useEffect` cleanup/setup runs on every parent render
- Scroll position resets inside the component
@@ -0,0 +1,35 @@
---
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.
**Incorrect:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading;
}, [user.isLoading, notifications.isLoading]);
if (isLoading) return <Skeleton />;
// return some markup
}
```
**Correct:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = user.isLoading || notifications.isLoading;
if (isLoading) return <Skeleton />;
// return some markup
}
```
@@ -0,0 +1,64 @@
---
title: Split Combined Hook Computations
impact: MEDIUM
impactDescription: avoids recomputing independent steps
tags: rerender, useMemo, useEffect, dependencies, optimization
---
## Split Combined Hook Computations
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.
**Incorrect (changing `sortOrder` recomputes filtering):**
```tsx
const sortedProducts = useMemo(() => {
const filtered = products.filter((p) => p.category === category);
const sorted = filtered.toSorted((a, b) =>
sortOrder === "asc" ? a.price - b.price : b.price - a.price,
);
return sorted;
}, [products, category, sortOrder]);
```
**Correct (filtering only recomputes when products or category change):**
```tsx
const filteredProducts = useMemo(
() => products.filter((p) => p.category === category),
[products, category],
);
const sortedProducts = useMemo(
() =>
filteredProducts.toSorted((a, b) =>
sortOrder === "asc" ? a.price - b.price : b.price - a.price,
),
[filteredProducts, sortOrder],
);
```
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.
@@ -0,0 +1,40 @@
---
title: Use Transitions for Non-Urgent Updates
impact: MEDIUM
impactDescription: maintains UI responsiveness
tags: rerender, transitions, startTransition, performance
---
## Use Transitions for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect (blocks UI on every scroll):**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from "react";
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY));
};
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```
@@ -0,0 +1,59 @@
---
title: Use useDeferredValue for Expensive Derived Renders
impact: MEDIUM
impactDescription: keeps input responsive during heavy computation
tags: rerender, useDeferredValue, optimization, concurrent
---
## 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):**
```tsx
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const filtered = items.filter((item) => fuzzyMatch(item, query));
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultsList results={filtered} />
</>
);
}
```
**Correct (input stays snappy, results render when ready):**
```tsx
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(
() => items.filter((item) => fuzzyMatch(item, deferredQuery)),
[items, deferredQuery],
);
const isStale = query !== deferredQuery;
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultsList results={filtered} />
</div>
</>
);
}
```
**When to use:**
- Filtering/searching large lists
- Expensive visualizations (charts, graphs) reacting to input
- Any derived state that causes noticeable render delays
**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.
Reference: [React useDeferredValue](https://react.dev/reference/react/useDeferredValue)
@@ -0,0 +1,73 @@
---
title: Use useRef for Transient Values
impact: MEDIUM
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.
**Incorrect (renders every update):**
```tsx
function Tracker() {
const [lastX, setLastX] = useState(0);
useEffect(() => {
const onMove = (e: MouseEvent) => setLastX(e.clientX);
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
return (
<div
style={{
position: "fixed",
top: 0,
left: lastX,
width: 8,
height: 8,
background: "black",
}}
/>
);
}
```
**Correct (no re-render for tracking):**
```tsx
function Tracker() {
const lastXRef = useRef(0);
const dotRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX;
const node = dotRef.current;
if (node) {
node.style.transform = `translateX(${e.clientX}px)`;
}
};
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
return (
<div
ref={dotRef}
style={{
position: "fixed",
top: 0,
left: 0,
width: 8,
height: 8,
background: "black",
transform: "translateX(0px)",
}}
/>
);
}
```
@@ -0,0 +1,74 @@
---
title: Use after() for Non-Blocking Operations
impact: MEDIUM
impactDescription: faster response times
tags: server, async, logging, analytics, side-effects
---
## Use after() for Non-Blocking Operations
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.
**Incorrect (blocks response):**
```tsx
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request);
// Logging blocks the response
const userAgent = request.headers.get("user-agent") || "unknown";
await logUserAction({ userAgent });
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
```
**Correct (non-blocking):**
```tsx
import { after } from "next/server";
import { headers, cookies } from "next/headers";
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request);
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get("user-agent") || "unknown";
const sessionCookie =
(await cookies()).get("session-id")?.value || "anonymous";
logUserAction({ sessionCookie, userAgent });
});
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
@@ -0,0 +1,96 @@
---
title: Authenticate Server Actions Like API Routes
impact: CRITICAL
impactDescription: prevents unauthorized access to server mutations
tags: server, server-actions, authentication, security, authorization
---
## Authenticate Server Actions Like API Routes
**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."
**Incorrect (no authentication check):**
```typescript
"use server";
export async function deleteUser(userId: string) {
// Anyone can call this! No auth check
await db.user.delete({ where: { id: userId } });
return { success: true };
}
```
**Correct (authentication inside the action):**
```typescript
"use server";
import { verifySession } from "@/lib/auth";
import { unauthorized } from "@/lib/errors";
export async function deleteUser(userId: string) {
// Always check auth inside the action
const session = await verifySession();
if (!session) {
throw unauthorized("Must be logged in");
}
// Check authorization too
if (session.user.role !== "admin" && session.user.id !== userId) {
throw unauthorized("Cannot delete other users");
}
await db.user.delete({ where: { id: userId } });
return { success: true };
}
```
**With input validation:**
```typescript
"use server";
import { verifySession } from "@/lib/auth";
import { z } from "zod";
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
});
export async function updateProfile(data: unknown) {
// Validate input first
const validated = updateProfileSchema.parse(data);
// Then authenticate
const session = await verifySession();
if (!session) {
throw new Error("Unauthorized");
}
// Then authorize
if (session.user.id !== validated.userId) {
throw new Error("Can only update own profile");
}
// Finally perform the mutation
await db.user.update({
where: { id: validated.userId },
data: {
name: validated.name,
email: validated.email,
},
});
return { success: true };
}
```
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
@@ -0,0 +1,41 @@
---
title: Cross-Request LRU Caching
impact: HIGH
impactDescription: caches across requests
tags: server, cache, lru, cross-request
---
## Cross-Request LRU Caching
`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";
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000, // 5 minutes
});
export async function getUser(id: string) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
// 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.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
@@ -0,0 +1,76 @@
---
title: Per-Request Deduplication with React.cache()
impact: MEDIUM
impactDescription: deduplicates within request
tags: server, cache, react-cache, deduplication
---
## Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({
where: { id: session.user.id },
});
});
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
**Avoid inline objects as arguments:**
`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
**Incorrect (always cache miss):**
```typescript
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } });
});
// Each call creates new object, never hits cache
getUser({ uid: 1 });
getUser({ uid: 1 }); // Cache miss, runs query again
```
**Correct (cache hit):**
```typescript
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } });
});
// Primitive args use value equality
getUser(1);
getUser(1); // Cache hit, returns cached result
```
If you must pass objects, pass the same reference:
```typescript
const params = { 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.
Reference: [React.cache documentation](https://react.dev/reference/react/cache)
@@ -0,0 +1,65 @@
---
title: Avoid Duplicate Serialization in RSC Props
impact: LOW
impactDescription: reduces network payload by avoiding duplicate serialization
tags: server, rsc, serialization, props, client-components
---
## Avoid Duplicate Serialization in RSC Props
**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.
**Incorrect (duplicates array):**
```tsx
// RSC: sends 6 strings (2 arrays × 3 items)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
```
**Correct (sends 3 strings):**
```tsx
// RSC: send once
<ClientList usernames={usernames} />;
// Client: transform there
("use client");
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
```
**Nested deduplication behavior:**
Deduplication works recursively. Impact varies by data type:
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
```tsx
// string[] - duplicates everything
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
// object[] - duplicates array structure only
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
```
**Operations breaking deduplication (create new references):**
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
**More examples:**
```tsx
// ❌ Bad
<C users={users} active={users.filter(u => u.active)} />
<C product={product} productName={product.name} />
// ✅ Good
<C users={users} />
<C product={product} />
// Do filtering/destructuring in client
```
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
@@ -0,0 +1,145 @@
---
title: Hoist Static I/O to Module Level
impact: HIGH
impactDescription: avoids repeated file/network I/O per request
tags: server, io, performance, next.js, route-handlers, og-image
---
## Hoist Static I/O to Module Level
**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'
export async function GET(request: Request) {
// Runs on EVERY request - expensive!
const fontData = await fetch(
new URL('./fonts/Inter.ttf', import.meta.url)
).then(res => res.arrayBuffer())
const logoData = await fetch(
new URL('./images/logo.png', import.meta.url)
).then(res => res.arrayBuffer())
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logoData} />
Hello World
</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
const fontData = fetch(
new URL('./fonts/Inter.ttf', import.meta.url)
).then(res => res.arrayBuffer())
const logoData = fetch(
new URL('./images/logo.png', import.meta.url)
).then(res => res.arrayBuffer())
export async function GET(request: Request) {
// Await the already-started promises
const [font, logo] = await Promise.all([fontData, logoData])
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logo} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: font }] }
)
}
```
**Correct (synchronous fs at module level):**
```typescript
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
import { readFileSync } from 'fs'
import { join } from 'path'
// Synchronous read at module level - blocks only during module init
const fontData = readFileSync(
join(process.cwd(), 'public/fonts/Inter.ttf')
)
const logoData = readFileSync(
join(process.cwd(), 'public/images/logo.png')
)
export async function GET(request: Request) {
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logoData} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: fontData }] }
)
}
```
**Incorrect (reads config on every call):**
```typescript
import fs from "node:fs/promises";
export async function processRequest(data: Data) {
const config = JSON.parse(await fs.readFile("./config.json", "utf-8"));
const template = await fs.readFile("./template.html", "utf-8");
return render(template, data, config);
}
```
**Correct (hoists config and template to module level):**
```typescript
import fs from "node:fs/promises";
const configPromise = fs.readFile("./config.json", "utf-8").then(JSON.parse);
const templatePromise = fs.readFile("./template.html", "utf-8");
export async function processRequest(data: Data) {
const [config, template] = await Promise.all([
configPromise,
templatePromise,
]);
return render(template, data, config);
}
```
When to use this pattern:
- Loading fonts for OG image generation
- Loading static logos, icons, or watermarks
- 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.
@@ -0,0 +1,83 @@
---
title: Parallel Data Fetching with Component Composition
impact: CRITICAL
impactDescription: eliminates server-side waterfalls
tags: server, rsc, parallel-fetching, composition
---
## Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
**Incorrect (Sidebar waits for Page's fetch to complete):**
```tsx
export default async function Page() {
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
);
}
async function Sidebar() {
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
```
**Correct (both fetch simultaneously):**
```tsx
async function Header() {
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
);
}
```
**Alternative with children prop:**
```tsx
async function Header() {
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
function Layout({ children }: { children: ReactNode }) {
return (
<div>
<Header />
{children}
</div>
);
}
export default function Page() {
return (
<Layout>
<Sidebar />
</Layout>
);
}
```
@@ -0,0 +1,32 @@
---
title: Parallel Nested Data Fetching
impact: CRITICAL
impactDescription: eliminates server-side waterfalls
tags: server, rsc, parallel-fetching, promise-chaining
---
## Parallel Nested Data Fetching
When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.
**Incorrect (a single slow item blocks all nested fetches):**
```tsx
const chats = await Promise.all(chatIds.map((id) => getChat(id)));
const chatAuthors = await Promise.all(
chats.map((chat) => getUser(chat.author)),
);
```
If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.
**Correct (each item chains its own nested fetch):**
```tsx
const chatAuthors = await Promise.all(
chatIds.map((id) => getChat(id).then((chat) => getUser(chat.author))),
);
```
Each item independently chains `getChat``getUser`, so a slow chat doesn't block author fetches for the others.
@@ -0,0 +1,38 @@
---
title: Minimize Serialization at RSC Boundaries
impact: HIGH
impactDescription: reduces data transfer size
tags: server, rsc, serialization, props
---
## Minimize Serialization at RSC Boundaries
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.
**Incorrect (serializes all 50 fields):**
```tsx
async function Page() {
const user = await fetchUser(); // 50 fields
return <Profile user={user} />;
}
("use client");
function Profile({ user }: { user: User }) {
return <div>{user.name}</div>; // uses 1 field
}
```
**Correct (serializes only 1 field):**
```tsx
async function Page() {
const user = await fetchUser();
return <Profile name={user.name} />;
}
("use client");
function Profile({ name }: { name: string }) {
return <div>{name}</div>;
}
```
-7
View File
@@ -1,7 +0,0 @@
src/
vite.config.ts
postcss.config.js
tailwind.config.js
index.html
public/
*.md
+22
View File
@@ -0,0 +1,22 @@
**/node_modules
**/build
**/dist
**/release
**/.turbo
**/.git
**/.claude
**/.deps
*.md
# Go build artifacts
apps/core/bin/
apps/core/server
# Electron source (not needed, but keep package.json files for workspace/build)
apps/electron/src/
apps/electron/scripts/
docs/
# OS files
.DS_Store
**/.DS_Store
+3 -4
View File
@@ -1,4 +1,3 @@
# FIXME: APP_ 开头的会暴漏给前端
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
APP_NAME=mediago-community
APP_ID=mediago.caorushizi.cn
APP_COPYRIGHT=caorushizi
+1
View File
@@ -1,4 +1,5 @@
APP_TD_APPID=
APP_SENTRY_DSN=
GH_TOKEN=
LOAD_DEVTOOLS=
+1
View File
@@ -1,3 +1,4 @@
APP_TD_APPID=
APP_SENTRY_DSN=
GH_TOKEN=
+3
View File
@@ -0,0 +1,3 @@
# Shell scripts MUST keep LF endings — CRLF from Windows checkouts breaks
# /bin/sh inside Linux containers (`/bin/sh^M: bad interpreter`).
*.sh text eol=lf
+58 -56
View File
@@ -1,56 +1,58 @@
# GitHub Copilot Instruction: Code Review and Optimization
## Role and Objective
You are a **top-tier software architect and performance optimization expert**.
Your goal is to help me — a senior TypeScript full-stack engineer — elevate the quality of my code to a new level.
When reviewing my code, **do not explain basic concepts**. I need **precise, deep, and forward-thinking insights**.
Your core mission is **optimization**, including but not limited to:
- **Performance improvement**: Identify and optimize performance bottlenecks, reduce unnecessary computation and resource consumption.
- **Code refactoring**: Suggest more elegant and efficient implementations to improve code structure.
- **Design patterns**: Identify opportunities to apply or refine design patterns to enhance scalability and maintainability.
- **Best practices**: Ensure the code adheres to the latest best practices for the TypeScript ecosystem (Node.js, React, API layers, build pipelines).
- **Potential risks**: Anticipate and highlight deep issues such as concurrency problems, security vulnerabilities, or resource leaks.
---
## Review Perspective and Principles
1. **High-Standard Review**
Review the code as if it were going to **production for millions of users** and needs to be **maintained long-term**.
2. **Deep Analysis, Not Surface Advice**
Dont focus on trivial issues like typos or syntax sugar.
Instead, explain **why** a refactor or change matters — e.g.:
> “Switching from a synchronous file read to `fs.promises` can free the event loop, improving throughput under load.”
3. **Performance First**
- Evaluate time and space complexity; suggest algorithmic or structural improvements.
- Examine I/O, database queries, and network calls for efficiency — recommend batching, caching, or async processing.
- Recommend appropriate data structures or API strategies for scalability (e.g., pagination, streaming responses).
4. **Architecture and Design**
- Follow **SOLID** principles. Explicitly identify violations and propose refactoring approaches.
- Encourage **composition over inheritance** and **dependency injection**.
- Suggest modularization and clear separation between layers (e.g., API, service, repository, UI).
5. **Code Style and Standards**
- Code must be clear, consistent, and self-explanatory.
- Follow the projects existing conventions unless the change brings substantial clarity or performance gain.
- For complex logic, suggest adding comments explaining the **rationale (“why”)**, not just the **action (“what”)**.
---
## Specific Instructions
- **When reviewing code, include the optimized code snippet directly**, with short comments highlighting key changes and their reasoning.
- **If you find potential bugs or unhandled edge cases**, explicitly point them out and provide a fix suggestion.
- **Avoid subjective stylistic comments** unless they impact clarity, performance, or maintainability.
- **When I ask “Can this code be optimized?”**, provide a holistic evaluation covering performance, readability, scalability, and maintainability.
---
At the end of every response, please add:
> “AI-generated suggestions may contain errors; use your own judgment when applying them.”
# GitHub Copilot Instruction: Code Review and Optimization
## Role and Objective
You are a **top-tier software architect and performance optimization expert**.
Your goal is to help me — a senior TypeScript full-stack engineer — elevate the quality of my code to a new level.
When reviewing my code, **do not explain basic concepts**. I need **precise, deep, and forward-thinking insights**.
Your core mission is **optimization**, including but not limited to:
- **Performance improvement**: Identify and optimize performance bottlenecks, reduce unnecessary computation and resource consumption.
- **Code refactoring**: Suggest more elegant and efficient implementations to improve code structure.
- **Design patterns**: Identify opportunities to apply or refine design patterns to enhance scalability and maintainability.
- **Best practices**: Ensure the code adheres to the latest best practices for the TypeScript ecosystem (Node.js, React, API layers, build pipelines).
- **Potential risks**: Anticipate and highlight deep issues such as concurrency problems, security vulnerabilities, or resource leaks.
---
## Review Perspective and Principles
1. **High-Standard Review**
Review the code as if it were going to **production for millions of users** and needs to be **maintained long-term**.
2. **Deep Analysis, Not Surface Advice**
Dont focus on trivial issues like typos or syntax sugar.
Instead, explain **why** a refactor or change matters — e.g.:
> “Switching from a synchronous file read to `fs.promises` can free the event loop, improving throughput under load.”
3. **Performance First**
- Evaluate time and space complexity; suggest algorithmic or structural improvements.
- Examine I/O, database queries, and network calls for efficiency — recommend batching, caching, or async processing.
- Recommend appropriate data structures or API strategies for scalability (e.g., pagination, streaming responses).
4. **Architecture and Design**
- Follow **SOLID** principles. Explicitly identify violations and propose refactoring approaches.
- Encourage **composition over inheritance** and **dependency injection**.
- Suggest modularization and clear separation between layers (e.g., API, service, repository, UI).
5. **Code Style and Standards**
- Code must be clear, consistent, and self-explanatory.
- Follow the projects existing conventions unless the change brings substantial clarity or performance gain.
- For complex logic, suggest adding comments explaining the **rationale (“why”)**, not just the **action (“what”)**.
---
## Specific Instructions
- **When reviewing code, include the optimized code snippet directly**, with short comments highlighting key changes and their reasoning.
- **If you find potential bugs or unhandled edge cases**, explicitly point them out and provide a fix suggestion.
- **Avoid subjective stylistic comments** unless they impact clarity, performance, or maintainability.
- **When I ask “Can this code be optimized?”**, provide a holistic evaluation covering performance, readability, scalability, and maintainability.
---
At the end of every response, please add:
> “AI-generated suggestions may contain errors; use your own judgment when applying them.”
+4 -4
View File
@@ -30,18 +30,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
with:
version: "10.15.0"
run_install: false
standalone: true
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "20.19.5"
node-version: "24.14.0"
cache: "pnpm"
- name: Install dependencies
+18 -5
View File
@@ -26,17 +26,17 @@ jobs:
# create steps
steps:
- name: Check out git repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
with:
version: "10.15.0"
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "20.19.5"
node-version: "24.14.0"
cache: "pnpm"
- name: Install Python
@@ -44,16 +44,29 @@ jobs:
with:
python-version: "3.10"
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25.0"
cache-dependency-path: |
apps/core/go.sum
- name: Install swag
run: go install github.com/swaggo/swag/cmd/swag@latest
- name: Install dependencies
run: pnpm install
- name: Download third-party deps
run: pnpm deps:download
- name: Build & release app
run: pnpm release:electron
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
APP_TD_APPID: ${{ secrets.APP_TD_APPID }}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: mediago-${{ matrix.os }}
path: apps/electron/release/mediago-*
+123 -122
View File
@@ -1,152 +1,153 @@
name: Make MediaGo Docker Image
name: Build & Push Docker Image
on:
workflow_dispatch:
inputs:
tag:
description: "Image tag to publish under (e.g. 3.5.0 to rebuild a released version, or 3.5.0-fix.1 for a side-by-side). Leave blank for dev-<run_id>."
required: false
type: string
push_latest:
description: "Also tag the resulting image as `latest` (use when rebuilding a stable release)."
required: false
type: boolean
default: false
push:
tags:
- 'v*'
- "v*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# Docker Hub mirror — same image, different registry. Credentials
# come from the DOCKERHUB_USERNAME / DOCKERHUB_TOKEN secrets (token,
# not account password). If either secret is empty, the Docker Hub
# login step is skipped and only GHCR gets pushed.
DOCKERHUB_IMAGE: caorushizi/mediago
jobs:
build:
name: Build Docker Image
strategy:
matrix:
include:
- runner: ubuntu-latest
arch: amd64
platform: linux/amd64
- runner: ubuntu-22.04-arm
arch: arm64
platform: linux/arm64
runs-on: ${{ matrix.runner }}
docker:
name: Build and push multi-arch image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "10.15.0"
run_install: false
standalone: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.19.5"
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm release:server
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Aliyun Container Registry
uses: docker/login-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: registry.cn-beijing.aliyuncs.com
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD }}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# GitHub Actions forbids `secrets.*` inside `if:` expressions
# directly — it fails with `Unrecognized named-value: 'secrets'`.
# The blessed workaround: pull the secret through `env:` (which
# IS allowed) inside a single check step, write a boolean to
# step outputs, and have every downstream step guard on the
# output instead of the raw secret.
- name: Detect Docker Hub credentials
id: dockerhub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
if [ -n "$DOCKERHUB_USERNAME" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
# Only log in (and later push) to Docker Hub when both secrets are
# configured. Lets forks / trial runs without Docker Hub
# credentials still build-and-push to GHCR.
- name: Login to Docker Hub
if: steps.dockerhub.outputs.enabled == 'true'
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Compose the list of target images based on which registries we
# actually have credentials for. Without this, metadata-action
# would always generate Docker Hub tags and build-push-action
# would then fail with 401 on forks that haven't set the secrets.
- name: Resolve image targets
id: targets
run: |
{
echo "images<<EOF"
echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
if [ "${{ steps.dockerhub.outputs.enabled }}" = "true" ]; then
echo "${{ env.DOCKERHUB_IMAGE }}"
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Docker meta
id: meta
run: |
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG_NAME="${GITHUB_REF#refs/tags/}"
# 如果是 beta 版本,只推送 tag
if [[ "${TAG_NAME}" == *"beta"* ]]; then
TAGS="registry.cn-beijing.aliyuncs.com/caorushizi/mediago:${TAG_NAME}-${{ matrix.arch }}"
else
# 正式版本,推送 tag 和 latest
TAGS="registry.cn-beijing.aliyuncs.com/caorushizi/mediago:latest-${{ matrix.arch }}\nregistry.cn-beijing.aliyuncs.com/caorushizi/mediago:${TAG_NAME}-${{ matrix.arch }}"
fi
else
# 非 tag 触发,只推送 dev 版本
DEV_TAG="dev-$(date +%Y%m%d-%H%M%S)"
TAGS="registry.cn-beijing.aliyuncs.com/caorushizi/mediago:${DEV_TAG}-${{ matrix.arch }}"
fi
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo -e "$TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
uses: docker/metadata-action@v6
with:
images: ${{ steps.targets.outputs.images }}
tags: |
# 1. Manual rebuild with an explicit tag (e.g. `3.5.0`, `3.5.0-fix.1`)
type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }}
# 2. Tag push: v1.0.0 → 1.0.0
type=semver,pattern={{version}}
# 3. `latest`: on stable semver tag pushes, or when the manual
# rebuild explicitly asks for it
type=raw,value=latest,enable=${{ (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'beta')) || inputs.push_latest }}
# 4. Fallback for manual triggers with no custom tag
type=raw,value=dev-${{ github.run_id }},enable=${{ !startsWith(github.ref, 'refs/tags/') && inputs.tag == '' }}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: ./apps/server
platforms: ${{ matrix.platform }}
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
# `metadata-action` already emits the standard OCI labels
# (image.title / description / source / licenses / version /
# revision / created). `revision` uses github.sha, so two
# builds that re-use a tag are distinguishable via
# `docker inspect`.
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
create-manifest:
name: Create Multi-Arch Manifest
needs: build
runs-on: ubuntu-latest
steps:
- name: Login to Aliyun Container Registry
uses: docker/login-action@v3
with:
registry: registry.cn-beijing.aliyuncs.com
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
IMAGE=registry.cn-beijing.aliyuncs.com/caorushizi/mediago
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG_NAME="${GITHUB_REF#refs/tags/}"
# 创建 tag 镜像
docker buildx imagetools create -t ${IMAGE}:${TAG_NAME} \
${IMAGE}:${TAG_NAME}-amd64 \
${IMAGE}:${TAG_NAME}-arm64
# 如果不是 beta,创建 latest
if [[ "${TAG_NAME}" != *"beta"* ]]; then
docker buildx imagetools create -t ${IMAGE}:latest \
${IMAGE}:latest-amd64 \
${IMAGE}:latest-arm64
fi
else
# 非 tag 触发,只创建 dev 版本
DEV_TAG="dev-$(date +%Y%m%d-%H%M%S)"
docker buildx imagetools create -t ${IMAGE}:${DEV_TAG} \
${IMAGE}:${DEV_TAG}-amd64 \
${IMAGE}:${DEV_TAG}-arm64
fi
- name: Build Summary
run: |
echo "### 🎉 Multi-Arch Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY
echo "### 🎉 Docker Image Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Image:** \`registry.cn-beijing.aliyuncs.com/caorushizi/mediago\`" >> $GITHUB_STEP_SUMMARY
echo "**Architectures:** amd64, arm64" >> $GITHUB_STEP_SUMMARY
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG_NAME="${GITHUB_REF#refs/tags/}"
echo "**Tag:** ${TAG_NAME}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Available Tags:**" >> $GITHUB_STEP_SUMMARY
echo "- \`${TAG_NAME}\`" >> $GITHUB_STEP_SUMMARY
if [[ "${TAG_NAME}" != *"beta"* ]]; then
echo "- \`latest\`" >> $GITHUB_STEP_SUMMARY
fi
else
DEV_TAG="dev-$(date +%Y%m%d-%H%M%S)"
echo "**Version:** ${DEV_TAG}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Available Tags:**" >> $GITHUB_STEP_SUMMARY
echo "- \`${DEV_TAG}\`" >> $GITHUB_STEP_SUMMARY
echo "**Registries:**" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.dockerhub.outputs.enabled }}" = "true" ]; then
echo "- \`docker.io/${{ env.DOCKERHUB_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY
fi
echo "**Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${{ steps.meta.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" | while read tag; do
echo "- \`${tag}\`" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Pull:**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.dockerhub.outputs.enabled }}" = "true" ]; then
echo "docker pull ${{ env.DOCKERHUB_IMAGE }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
+10
View File
@@ -9,4 +9,14 @@ log
.claude
.store
release
go.work.sum
apps/core/bin/
apps/electron/bin/
apps/main/bin/
.deps/
tmp/
# Local planning notes that should not be published with the open-source repo.
docs/.vitepress/seo-keyword-map*.md
docs/.vitepress/private/
+1
View File
@@ -0,0 +1 @@
lint-staged
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 80,
"indentStyle": "space",
"indentWidth": 2,
"endOfLine": "lf",
"sortPackageJson": false,
"ignorePatterns": [
"build",
"dist",
"release",
"coverage",
"docs/.vitepress/cache",
"docs/.vitepress/dist"
],
"overrides": [
{
"files": ["*.md"],
"options": {
"proseWrap": "preserve"
}
}
]
}
+53 -6
View File
@@ -1,6 +1,53 @@
{
"rules": {
"no-unused-vars": "error",
"no-console": "warn"
}
}
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn",
"pedantic": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"no-unused-vars": "error",
"no-console": "warn",
"no-debugger": "error",
"no-duplicate-imports": "error",
"eqeqeq": "warn",
"typescript/no-explicit-any": "warn",
"typescript/no-non-null-assertion": "warn",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"unicorn/no-null": "off",
"unicorn/prefer-node-protocol": "error"
},
"overrides": [
{
"files": ["**/*.tsx", "**/*.jsx"],
"rules": {
"react/jsx-no-duplicate-props": "error",
"react/no-direct-mutation-state": "error",
"react/jsx-key": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
},
{
"files": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"],
"rules": {
"no-console": "off",
"typescript/no-explicit-any": "off"
}
}
],
"ignorePatterns": [
"node_modules",
"build",
"dist",
"release",
"*.min.js",
"docs/.vitepress/cache",
"docs/.vitepress/dist"
]
}
-3
View File
@@ -1,3 +0,0 @@
# Ignore artifacts:
build
coverage
-5
View File
@@ -1,5 +0,0 @@
{
"plugins": [
"@prettier/plugin-oxc"
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["oxc.oxc-vscode", "EditorConfig.EditorConfig"],
"unwantedRecommendations": ["esbenp.prettier-vscode"]
}
+15 -5
View File
@@ -1,5 +1,15 @@
{
"cSpell.words": ["intlify", "unplugin", "vitepress", "waline"],
"typescript.experimental.useTsgo": true,
"oxc.enable": true
}
{
"cSpell.words": ["intlify", "unplugin", "vitepress", "waline"],
"oxc.enable": true,
"oxc.lint.enable": true,
"oxc.fmt.experimental": true,
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
},
"prettier.enable": false,
"[go]": {
"editor.defaultFormatter": "golang.go"
}
}
+21 -16
View File
@@ -1,16 +1,21 @@
# Repository Guidelines
## Project Structure & Module Organization
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.
# Repository Guidelines
## Project Structure & Module Organization
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.
+93
View File
@@ -0,0 +1,93 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
MediaGo is a cross-platform video downloader supporting m3u8/HLS streams. The codebase is a pnpm monorepo with three products:
1. **Desktop app** (`apps/electron` + `apps/ui`) — Electron wrapper that launches Go Core as a subprocess
2. **Web server** (`apps/server` + `apps/ui`) — Node.js launcher that spawns Go Core as a subprocess
3. **Video player** (`apps/core` + `apps/player-ui`) — Player UI embedded in Go Core for video playback
All three products share the Go Core backend (`apps/core`) for download orchestration.
## Common Commands
```bash
pnpm install # Install all dependencies (run once per clone)
pnpm dev:electron # Start Electron desktop dev environment (HMR)
pnpm dev:server # Start server dev environment (HMR)
pnpm build:electron # Production build for Electron
pnpm build:web # Build UI only (server mode)
pnpm core:dev # Start Go Core dev server (port 9900)
pnpm core:build # Compile Go Core binary
pnpm player:dev # Start Player dev (alias for core:dev)
pnpm player:build # Build Player (alias for core:build)
pnpm deps:download # Download third-party tools (ffmpeg, BBDown, etc.)
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)
- **`docs/`** — VitePress documentation (Chinese, English, Japanese)
### Multi-Target Build
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
- **Go builds**: Gulp orchestrating `go build` / `go run` in `apps/core`
- **Linter**: oxlint (config in `.oxlintrc.json`)
- **Formatter**: oxfmt (config in `.oxfmtrc.json`)
- **Pre-commit**: husky + lint-staged (runs oxlint --fix + oxfmt --write on staged files)
- **Electron packaging**: electron-builder
## Style Conventions
- TypeScript, ES modules, 2-space indentation, UTF-8, LF endings
- Components: PascalCase. Utilities: camelCase. Constants: SCREAMING_SNAKE_CASE
- UI port: 8555 (strict). Go Core port: 9900. Player UI port: 8556
+96
View File
@@ -0,0 +1,96 @@
# Contributing to MediaGo
Thanks for your interest in hacking on MediaGo! This doc covers everything
you need to get a local dev build running. For user-facing usage, see the
main [README](./README.md).
## Prerequisites
- **Node.js** ≥ 20 — install from [nodejs.org](https://nodejs.org/)
- **pnpm** ≥ 10 — `npm i -g pnpm`
- **Go** ≥ 1.22 — only needed if you're working on the Go Core backend
(`apps/core/`). Install from [go.dev](https://go.dev/dl/).
## Clone & install
```shell
git clone https://github.com/caorushizi/mediago.git
cd mediago
pnpm install
```
## Repository layout
MediaGo is a pnpm + Turborepo monorepo with three products that share the
same Go Core backend:
```
apps/
core/ Go backend (download orchestration, SSE, REST API)
electron/ Electron desktop main process
server/ Node.js launcher for the self-hosted web build
ui/ Shared React 19 frontend (Electron + Web)
player-ui/ React frontend embedded inside Go Core for playback
packages/
shared/common/ Cross-platform types, constants, i18n resources
core-sdk/ TypeScript SDK for the Go Core REST API
electron-preload/
mediago-extension/ Browser extension (Chrome / Edge)
docs/ VitePress site (zh / en / jp)
extra/ Vendored binaries (e.g. aria2)
scripts/ Dep downloaders, extension packager, etc.
```
Deeper architecture notes live in [`CLAUDE.md`](./CLAUDE.md).
## Everyday commands
```shell
# Download third-party binaries (ffmpeg, yt-dlp, N_m3u8DL-RE, BBDown,
# aria2, mediago-core) for the current platform — run once per clone
pnpm deps:download
# Run the Electron desktop app in dev mode (HMR)
pnpm dev:electron
# Run the self-hosted web server in dev mode
pnpm dev:server
# Build an unpacked Electron directory (fast, for smoke-testing layout)
pnpm pack:electron
# Build full Electron installers for distribution (.exe / .dmg / .deb)
pnpm release:electron
# Lint + format + type-check (what CI runs)
pnpm check
```
The self-hosted web server doesn't have a dedicated packaging script — it
ships via the Docker image published to GHCR, or you can run the build
output (`pnpm -F @mediago/server build`) directly under Node.
## Commit style
This repo uses [Conventional Commits](https://www.conventionalcommits.org/).
Typical shapes:
```
feat(ui): add dark-mode toggle to settings page
fix(core): resume m3u8 downloads after process restart
refactor(extension): split options hook per card
chore(deps): bump axios from 1.14.0 to 1.15.0
```
Commits are lint-staged on commit (oxlint --fix + oxfmt --write on
staged files). Type checks run via `turbo type:check`.
## Pull requests
- Open PRs against the `master` branch.
- Keep each PR focused — one feature / fix per PR if possible.
- Include a short "why" in the description; the "what" is in the diff.
- If the change is user-visible, a line in the PR description that would
fit in a release note is appreciated.
Thanks for contributing! 🚀
+117
View File
@@ -0,0 +1,117 @@
# Build context: repository root
# docker build -t mediago .
# ===== Stage 1: Node Builder (runs natively on build machine) =====
FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS node-builder
RUN apt-get update && apt-get install -y --no-install-recommends unzip && rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
# Map Docker TARGETARCH to Node arch naming for deps download
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "amd64" ]; then \
echo "linux-x64" > /tmp/deps-platform; \
else \
echo "linux-${TARGETARCH}" > /tmp/deps-platform; \
fi
WORKDIR /src
# Install dependencies — copy all workspace package.json files first for caching
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
COPY apps/ui/package.json apps/ui/package.json
COPY apps/player-ui/package.json apps/player-ui/package.json
COPY apps/server/package.json apps/server/package.json
COPY apps/core/package.json apps/core/package.json
COPY apps/electron/package.json apps/electron/package.json
COPY packages/ packages/
RUN pnpm install --frozen-lockfile
# Copy source files and root configs needed by builds
COPY tsconfig*.json turbo.json .env* ./
COPY apps/ui/ apps/ui/
COPY apps/player-ui/ apps/player-ui/
COPY apps/electron/app/package.json apps/electron/app/package.json
COPY scripts/ scripts/
# Vendored binaries (aria2 in particular). `scripts/download-deps.ts`
# treats `source: "local"` entries by copying from `extra/<tool>/<os>/<arch>/`
# into `.deps/`; without this COPY they silently get skipped and the
# resulting image ships with no aria2c.
COPY extra/ extra/
# Build player-ui (will be embedded in Go core binary)
RUN pnpm --filter @mediago/player-ui run build
# Build web UI for server mode
ENV APP_TARGET=server
ENV NODE_ENV=production
RUN pnpm build:web
# Download third-party tools for TARGET platform
RUN pnpm deps:download --platform $(cat /tmp/deps-platform)
# ===== Stage 2: Go Builder =====
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS go-builder
ARG TARGETOS=linux
ARG TARGETARCH=amd64
WORKDIR /src
# Cache Go module downloads
COPY apps/core/go.mod apps/core/go.sum apps/core/
RUN cd apps/core && go mod download
# Copy Go source
COPY apps/core/ apps/core/
# Copy player-ui dist into assets for go:embed
COPY --from=node-builder /src/apps/player-ui/dist apps/core/assets/player/
# Ensure all dependencies are downloaded (go.sum may have been updated)
RUN cd apps/core && go mod download
# Build Go core binary (cross-compile, no CGO)
RUN cd apps/core && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags="-s -w" -o /out/mediago-core ./cmd/server
# ===== Stage 3: Runtime =====
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates libicu72 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Core binary
COPY --from=go-builder /out/mediago-core /usr/local/bin/mediago-core
# Web UI static files
COPY --from=node-builder /src/apps/ui/build/server /app/static
# Third-party tools — copy from the platform-specific directory
ARG TARGETARCH
COPY --from=node-builder /src/.deps/linux-* /app/deps-tmp/
RUN mkdir -p /app/deps && \
if [ -d /app/deps-tmp ]; then \
find /app/deps-tmp -type f -exec cp {} /app/deps/ \; ; \
fi && \
chmod +x /app/deps/* 2>/dev/null || true && \
rm -rf /app/deps-tmp
RUN mkdir -p /app/mediago/data /app/mediago/logs /app/mediago/downloads
# Entrypoint script — isolates the invocation flags from the Dockerfile
# so editing the default args doesn't require rebuilding the full image
# layer and so callers can still append overrides via `docker run`.
COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 8899
VOLUME ["/app/mediago"]
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
-141
View File
@@ -1,141 +0,0 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/en/guides.html?form=github">Quick start</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<br>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<br>
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
## Intro
This project supports m3u8 video extraction tools, streaming media download, and m3u8 download.
- **✅&nbsp; No need to capture packets**: You can easily sniff video resources on web pages using the built-in browser. Choose the resource you want to download from the sniffed resource list—simple and fast.
- **📱&nbsp; Mobile playback**: Easily switch between PC and mobile devices seamlessly. Once downloaded, you can watch the video on your phone.
- **⚡️&nbsp; Batch download**: Supports downloading multiple videos and live streams simultaneously, ensuring high bandwidth is fully utilized.
- **🎉&nbsp; Docker deployment supported**: Supports Docker deployment for the web version, making it convenient and quick.
## Quickstart
To run the code, you'll need Node.js and pnpm. Node.js can be downloaded and installed from the official website, and pnpm can be installed via `npm i -g pnpm`.
## Run the code
```shell
# Code download
git clone https://github.com/caorushizi/mediago.git
# Installation dependency
pnpm i
# Development environment
pnpm dev
# Package run
pnpm release
# Build a docker image
docker buildx build -t caorushizi/mediago:latest .
# docker startup
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
```
## Releases
### v3.0.0 (Released on 2024.10.7)
#### Software Downloads
- [【mediago】 Windows (Installer) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
- [【mediago】 Windows (Portable) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macOS x64 (Intel) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
- [【mediago】 Linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
#### Domestic Downloads
- [【mediago】 Windows (Installer) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
- [【mediago】 Windows (Portable) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macOS x64 (Intel) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
- [【mediago】 Linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
### One-click Docker Panel Deployment (Recommended)
1. Install the BT Panel by visiting [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago) and downloading the official version script.
2. After installation, log into the BT Panel, click on `Docker` in the menu bar. The first time you enter, it will prompt you to install the `Docker` service. Click to install and follow the instructions to complete the installation.
3. Once installed, find `MediaGo` in the application store, click install, configure domain names, and other basic information to complete the installation.
### Software Screenshots
![Homepage](https://static.ziying.site/images/home.png)
### Major Updates
- Support for Docker deployment on the web version
- Updated desktop UI
### Changelog
- Updated desktop UI
- Support for Docker deployment on the web version
- Added video playback, supporting both desktop and mobile playback
- Fixed issue where the macOS version couldn't display the interface
- Optimized the batch download interaction
- Added portable version for Windows (no installation required)
- Optimized the download list, supporting sniffing multiple videos on a page
- Supported manual import/export of the favorites list
- Supported export of the homepage download list
- Optimized the interaction logic for the "New Download" form
- Supported opening the app via UrlScheme and adding download tasks
- Fixed several bugs and enhanced the user experience
## Software Screenshots
![Homepage](https://static.ziying.site/images/home.png)
![Homepage-dark](https://static.ziying.site/images/home-dark.png)
![Settings Page](https://static.ziying.site/images/settings.png)
![Resource Extraction](https://static.ziying.site/images/browser.png)
## Tech Stack
- React <https://react.dev/>
- Electron <https://www.electronjs.org>
- Koa <https://koajs.com>
- Vite <https://cn.vitejs.dev>
- Ant Design <https://ant.design>
- Tailwind CSS <https://tailwindcss.com>
- Shadcn <https://ui.shadcn.com/>
- Inversify <https://inversify.io>
- TypeORM <https://typeorm.io>
## Acknowledgements
- N_m2u8DL-CLI from <https://github.com/nilaoda/N_m3u8DL-CLI>
- N_m3u8DL-RE from <https://github.com/nilaoda/N_m3u8DL-RE>
- mediago from <https://github.com/caorushizi/hls-downloader>
+177
View File
@@ -0,0 +1,177 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/it/guides.html?form=github">Avvio rapido</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/it?form=github">Sito web</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/it/documents.html?form=github">Documentazione</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussioni</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://discord.gg/yxWBVRWGqM">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://www.reddit.com/r/MediaGo_Studio/">Reddit</a>
<br>
<a href="https://github.com/caorushizi/mediago/blob/master/README.md">English</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.zh.md">中文</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<img alt="GitCode" src="https://gitcode.com/caorushizi/mediago/star/badge.svg">
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
Un downloader video multipiattaforma con sniffing integrato: apri una
pagina, scegli la risorsa che ti interessa e salvala. Nessuna cattura dei
pacchetti, nessuna configurazione complicata di estensioni browser, nessuno
strumento da riga di comando da gestire.
L'interfaccia dell'app include attualmente inglese, cinese semplificato e
italiano.
## ✨ Cosa include
### 🌐 Estensione browser per Chrome / Edge
Trovi un video interessante su un sito qualsiasi → clicchi l'estensione →
lo invii a MediaGo con un clic. Rileva automaticamente le risorse video,
mostra il numero di elementi trovati nel badge della toolbar e funziona con
le principali piattaforme video, incluse YouTube, Bilibili e molte altre.
L'estensione è inclusa nell'app desktop: apri **Impostazioni → Altre
impostazioni → Directory estensione browser** per trovare la cartella di
installazione.
### 🎬 YouTube e oltre 1000 siti
Basato su yt-dlp. Supporta YouTube, Twitter/X, Instagram, Reddit e
[oltre mille altri siti video](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md).
### 🦞 Gli assistenti AI possono scaricare per te — OpenClaw Skill
Usi Claude Code, Cursor o un altro assistente AI per programmare? Installa
la skill MediaGo e scrivi semplicemente _"please download this video:
&lt;url&gt;"_. L'assistente gestisce il resto.
```shell
npx clawhub@latest install mediago
```
### 🔌 Funziona con altri strumenti
MediaGo espone una API HTTP completa: script, automazioni e app di terze
parti possono creare attività di download, consultare l'avanzamento e
gestire la lista. L'estensione browser usa la stessa API per parlare con
l'app desktop, e puoi integrarla anche nei tuoi workflow.
### 🎞️ Conversione formato integrata
Dopo il download puoi convertire il file in un altro formato o qualità
direttamente da MediaGo. Non serve aprire uno strumento ffmpeg separato.
### 🐳 Deploy Docker con un solo comando
Installazione headless sul tuo server, poi accesso alla UI web da qualsiasi
dispositivo nella stessa rete:
```shell
docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0
```
Disponibile su [Docker Hub](https://hub.docker.com/r/caorushizi/mediago)
e GHCR (`ghcr.io/caorushizi/mediago`): la stessa immagine, scegli il
registry più veloce per te. Supporta Intel / AMD (amd64) e ARM (arm64).
Nella build desktop, MediaGo ascolta sia su `127.0.0.1` sia sull'IP LAN,
così telefoni e tablet sulla stessa rete Wi-Fi possono aprire direttamente
la UI web.
## 📷 Screenshot
![Home](./images/home_en.png)
![Home — modalità scura](./images/home-dark_en.png)
![Impostazioni](./images/settings_en.png)
![Estrazione risorse](./images/browser_en.png)
## 📥 Download
### v3.5.0 (stabile)
- [Windows — installer](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-win32-x64-3.5.0.exe)
- [Windows — portable](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-portable-win32-x64-3.5.0.exe)
- [macOS — Apple Silicon (arm64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-arm64-3.5.0.dmg)
- [macOS — Intel (x64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-x64-3.5.0.dmg)
- [Linux (deb)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-linux-amd64-3.5.0.deb)
- [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago): `docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0`
- **GHCR**: `docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago ghcr.io/caorushizi/mediago:3.5.0`
Per le versioni precedenti, consulta la [pagina GitHub Releases](https://github.com/caorushizi/mediago/releases).
### 🪄 Deploy Docker con un clic tramite BT Panel
1. Installa [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago)
usando lo script ufficiale.
2. Accedi al pannello, clicca **Docker** nella barra laterale e completa la
configurazione del servizio Docker seguendo le istruzioni.
3. Trova **MediaGo** nello store delle app, clicca **Install**, configura il
dominio e hai finito.
## 📝 Novità in v3.5.0
- **🌐 Estensione browser** — sniffing video su qualsiasi sito e invio a
MediaGo con un clic
- **🎬 YouTube + 1000+ siti** — integrazione con yt-dlp
- **🦞 OpenClaw Skill** — scarica video tramite assistenti AI per programmare
- **🔌 API HTTP** — integrazione con script, automazioni e strumenti di terze parti
- **🎞️ Conversione formato in app** — scegli formato e qualità di output
- **🐳 Deploy Docker più semplice** — monta una sola cartella, immagini multi-arch su GHCR
- **⚡ Avvio più rapido** — backend riscritto, minore consumo di memoria, player video integrato
## 🛠️ Tecnologie
[![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)](https://react.dev/)
[![Electron](https://img.shields.io/badge/Electron-191970?logo=electron&logoColor=white)](https://www.electronjs.org)
[![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)](https://vitejs.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com)
[![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-000?logo=shadcnui&logoColor=white)](https://ui.shadcn.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Ant Design](https://img.shields.io/badge/Ant_Design-0170FE?logo=antdesign&logoColor=white)](https://ant.design)
## 🙏 Ringraziamenti
- [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE)
- [BBDown](https://github.com/nilaoda/BBDown)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [aria2](https://aria2.github.io/)
- [mediago-core](https://github.com/caorushizi/mediago-core)
## ⚖️ Disclaimer
> **Questo progetto è destinato esclusivamente a scopi educativi e di ricerca. Non usarlo per finalità commerciali o illegali.**
>
> 1. Tutto il codice e tutte le funzionalità fornite da questo progetto sono pensati solo come riferimento per lo studio delle tecnologie di streaming. Gli utenti devono rispettare le leggi e i regolamenti della propria giurisdizione.
> 2. Qualsiasi contenuto scaricato tramite questo progetto resta di proprietà dei rispettivi titolari dei diritti. Gli utenti devono eliminare i contenuti scaricati entro 24 ore o ottenere un'autorizzazione adeguata.
> 3. Gli sviluppatori del progetto non sono responsabili delle azioni degli utenti, incluso il download di contenuti protetti da copyright o l'impatto su piattaforme di terze parti.
> 4. È vietato usare questo progetto per scraping massivo, interruzione dei servizi delle piattaforme o qualsiasi attività che violi diritti legittimi altrui.
> 5. Usando questo progetto confermi di aver letto e accettato questo disclaimer. Se non lo accetti, interrompi subito l'uso del progetto ed eliminalo.
---
> Vuoi compilare da sorgente? Consulta [CONTRIBUTING.md](./CONTRIBUTING.md).
>
> Vuoi tradurre MediaGo? Consulta [TRANSLATION.md](./TRANSLATION.md).
+150 -143
View File
@@ -1,143 +1,150 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/jp/guides.html?form=github">早く始めます</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp?form=github">公式サイトです</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp/documents.html?form=github">にやすりをかける</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<br>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<br>
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
## Intro
本プロジェクトはm3u8ビデオ抽出ツール、ストリーミングダウンロード、m3u8ダウンロードをサポートしています。
- **✅&nbsp; パケットキャプチャ不要**: ソフトウェアに内蔵されたブラウザを使用して、ウェブページ内のビデオリソースを簡単に検出し、検出したリソースリストからダウンロードしたいリソースを選択することで、シンプルかつ迅速にダウンロードできます。
- **📱&nbsp; モバイル再生**: PCとモバイルデバイス間で簡単にシームレスに切り替えが可能で、ダウンロードが完了した後はスマートフォンでビデオを視聴できます。
- **⚡️&nbsp; バッチダウンロード**: 複数のビデオやライブストリームリソースを同時にダウンロードでき、高速帯域幅を無駄にしません。
- **🎉&nbsp; Dockerデプロイメントサポート**: WebエンドをDockerでデプロイすることができ、簡単かつ便利です。
## Quickstart
コードを実行するには、Node.jsとpnpmが必要です。Node.jsは公式ウェブサイトからダウンロードしてインストールし、pnpmは`npm i -g pnpm`コマンドでインストールできます。
## コードの実行
```shell
# コードのダウンロードです
git clone https://github.com/caorushizi/mediago.git
# インストール依存症です
pnpm i
# 開発環境です
pnpm dev
# 梱包して運行します
pnpm release
# dockerミラーリングを構築します
docker buildx build -t caorushizi/mediago:latest .
# docker启动
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
```
## Releases
### v3.0.0 (2024.10.7 発売)
#### ソフトウェアダウンロード
- [【mediago】 windows(インストーラー版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
- [【mediago】 windows(ポータブル版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
- [【mediago】 macos arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
#### 国内ダウンロード
- [【mediago】 windows(インストーラー版) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
- [【mediago】 windows(ポータブル版) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
- [【mediago】 macos arm64Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
- [【mediago】 linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
### docker 宝塔パネルワンクリックデプロイ(推奨)
1. 宝塔パネルをインストールし、[宝塔パネル](https://www.bt.cn/new/download.html?r=dk_mediago) の公式サイトから正式版のスクリプトを選択してインストールします。
2. インストール後、宝塔パネルにログインし、メニューから `Docker` をクリックします。初めてアクセスすると、`Docker` サービスをインストールするように指示されるので、「今すぐインストール」をクリックし、指示に従ってインストールを完了します。
3. インストールが完了したら、アプリストアで「MediaGo」を見つけ、インストールをクリックし、ドメイン名などの基本情報を設定してインストールを完了します。
### ソフトウェアスクリーンショット
![ホームページ](https://static.ziying.site/images/home.png)
### 重要な更新
- Web端のdockerデプロイをサポート
- デスクトップ端のUIを更新
### 更新ログ
- デスクトップ端のUIを更新
- Web端のdockerデプロイをサポート
- 新たにビデオ再生機能を追加、デスクトップとモバイル端両方で再生可能
- Macでの画面表示ができない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windowsのポータブル版(インストール不要)を追加
- ダウンロードリストの最適化、ページ内の複数のビデオリソースを嗅ぎ取る機能を追加
- 手動でお気に入りリストのインポートとエクスポートをサポート
- ホームページのダウンロードリストエクスポートをサポート
- 「新規ダウンロード」フォームのインタラクションロジックを最適化
- UrlSchemeでアプリを開き、ダウンロードタスクを追加する機能をサポート
- バグの修正とユーザー体験の向上
## ソフトウェアスクリーンショット
![ホームページ](https://static.ziying.site/images/home.png)
![ホームページ(ダークモード)](https://static.ziying.site/images/home-dark.png)
![設定ページ](https://static.ziying.site/images/settings.png)
![リソース抽出](https://static.ziying.site/images/browser.png)
## 技術スタック
- react <https://react.dev/>
- electron <https://www.electronjs.org>
- koa <https://koajs.com>
- vite <https://cn.vitejs.dev>
- antd <https://ant.design>
- tailwindcss <https://tailwindcss.com>
- shadcn <https://ui.shadcn.com/>
- inversify <https://inversify.io>
- typeorm <https://typeorm.io>
## 感謝
- N_m2u8DL-CLI は <https://github.com/nilaoda/N_m3u8DL-CLI> から来ています
- N_m3u8DL-RE は <https://github.com/nilaoda/N_m3u8DL-RE> から来ています
- mediago は <https://github.com/caorushizi/hls-downloader> から来ています
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/jp/guides.html?form=github">クイックスタート</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp?form=github">公式サイト</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp/documents.html?form=github">ドキュメント</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://discord.gg/yxWBVRWGqM">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://www.reddit.com/r/MediaGo_Studio/">Reddit</a>
<br>
<a href="https://github.com/caorushizi/mediago/blob/master/README.md">English</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.zh.md">中文</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.it.md">Italiano</a>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<img alt="GitCode" src="https://gitcode.com/caorushizi/mediago/star/badge.svg">
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
ビルトインのスニッフィング機能を備えたクロスプラットフォームの動画ダウンローダー —— ページを開いて、欲しいリソースを選んで、保存するだけ。パケットキャプチャ不要、ブラウザ拡張の設定不要、コマンドラインの操作も不要です。
アプリ UI は現在、英語・簡体中国語・イタリア語に対応しています。
## ✨ 主な機能
### 🌐 ブラウザ拡張機能(Chrome / Edge
ウェブを閲覧中に気になる動画を見つけたら → 拡張機能のアイコンをクリック → ワンクリックで MediaGo に送信。ページ内のダウンロード可能なリソースを自動検出し、ツールバーアイコンのバッジに件数を表示します。YouTube、Bilibili をはじめ主要な動画サイトに対応。拡張機能はデスクトップ版インストーラーに同梱されているので、**設定 → その他の設定 → ブラウザ拡張ディレクトリ** から直接インストールフォルダを開けます。
### 🎬 YouTube と 1000+ サイト対応
内部では yt-dlp を使用。YouTube、Twitter/X、Instagram、Reddit など [1000 以上の動画サイト](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) をサポートします。
### 🦞 AI アシスタントで動画をダウンロード — OpenClaw Skill
Claude Code や Cursor などの AI コーディングアシスタントを使っていますか?MediaGo Skill をインストールすれば、AI に「この動画をダウンロードして:&lt;URL&gt;」と言うだけでダウンロードが始まります。
```shell
npx clawhub@latest install mediago
```
### 🔌 他のツールと連携
MediaGo は完全な HTTP API を提供します。スクリプト、自動化ツール、他のアプリから直接ダウンロードタスクの作成、進捗の取得、リスト管理が可能です。ブラウザ拡張機能はこの API を介してデスクトップアプリと通信しており、自分のワークフローに組み込むこともできます。
### 🎞️ 内蔵フォーマット変換
ダウンロード完了後、MediaGo 内で他のフォーマットや画質に変換できます。ffmpeg を別途起動する必要はありません。
### 🐳 Docker でワンライン展開
サーバーにヘッドレスでインストールし、同じネットワーク内のどこからでも Web UI にアクセスできます:
```shell
docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0
```
[Docker Hub](https://hub.docker.com/r/caorushizi/mediago) と GHCR`ghcr.io/caorushizi/mediago`)の両方で配信しています。同じイメージなのでお好みのレジストリを。Intel / AMD (amd64) と ARM (arm64) の両方に対応。デスクトップ版は `127.0.0.1` と LAN IP の両方で待ち受けるため、同じ Wi-Fi のスマートフォンやタブレットからも Web UI を開けます。
## 📷 スクリーンショット
![ホームページ](./images/home.png)
![ホームページ — ダークモード](./images/home-dark.png)
![設定](./images/settings.png)
![リソース抽出](./images/browser.png)
## 📥 ダウンロード
### v3.5.0(安定版)
- [Windows — インストーラー版](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-win32-x64-3.5.0.exe)
- [Windows — ポータブル版](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-portable-win32-x64-3.5.0.exe)
- [macOS — Apple Silicon (arm64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-arm64-3.5.0.dmg)
- [macOS — Intel (x64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-x64-3.5.0.dmg)
- [Linux (deb)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-linux-amd64-3.5.0.deb)
- [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago)`docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0`
- **GHCR**`docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago ghcr.io/caorushizi/mediago:3.5.0`
過去のバージョンは [GitHub Releases ページ](https://github.com/caorushizi/mediago/releases) をご覧ください。
### 🪄 宝塔パネルでワンクリック Docker デプロイ
1. [宝塔パネル公式サイト](https://www.bt.cn/new/download.html?r=dk_mediago) から正式版のスクリプトをダウンロードしてインストール
2. 宝塔パネルにログイン、メニューから **Docker** をクリック。初回アクセス時に Docker サービスのインストールを求められるので、「今すぐインストール」をクリックして完了
3. アプリストアで **MediaGo** を見つけて、インストールをクリック、ドメインなどの基本情報を設定すれば完了
## 📝 v3.5.0 の新機能
- **🌐 ブラウザ拡張機能** — 任意のサイトで動画をスニッフィング、ワンクリックで MediaGo に送信
- **🎬 YouTube + 1000+ サイト** — yt-dlp による対応
- **🦞 OpenClaw Skill** — AI コーディングアシスタント経由でダウンロード
- **🔌 HTTP API** — スクリプト、自動化、サードパーティツールとの統合
- **🎞️ アプリ内フォーマット変換** — 出力形式と画質を選択
- **🐳 Docker デプロイの簡素化** — 単一ディレクトリをマウント、GHCR のマルチアーキテクチャイメージ
- **⚡ 起動の高速化** — バックエンド書き換え、メモリ使用量の削減、内蔵動画プレーヤー
## 🛠️ 技術スタック
[![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)](https://react.dev/)
[![Electron](https://img.shields.io/badge/Electron-191970?logo=electron&logoColor=white)](https://www.electronjs.org)
[![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)](https://vitejs.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com)
[![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-000?logo=shadcnui&logoColor=white)](https://ui.shadcn.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Ant Design](https://img.shields.io/badge/Ant_Design-0170FE?logo=antdesign&logoColor=white)](https://ant.design)
## 🙏 謝辞
- [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE)
- [BBDown](https://github.com/nilaoda/BBDown)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [aria2](https://aria2.github.io/)
- [mediago-core](https://github.com/caorushizi/mediago-core)
## ⚖️ 免責事項
> **本プロジェクトは学習および研究目的にのみ提供されるものであり、商用または違法な目的での使用はご遠慮ください。**
>
> 1. 本プロジェクトが提供するすべてのコードおよび機能は、ストリーミング技術の学習のための参考資料としてのみ使用されます。利用者は所在地域の法令を遵守してください。
> 2. 本プロジェクトを使用してダウンロードされたコンテンツの著作権は、原コンテンツの所有者に帰属します。利用者はダウンロード後 24 時間以内にコンテンツを削除するか、著作権者の許可を取得する必要があります。
> 3. 本プロジェクトの開発者は、著作権で保護されたコンテンツのダウンロードや第三者プラットフォームへの影響を含め、利用者の行動に対して一切の責任を負いません。
> 4. 大規模なスクレイピング、プラットフォームサービスの妨害、その他他者の合法的権利を侵害する行為に本プロジェクトを使用することは禁止されています
> 5. 本プロジェクトを使用することにより、あなたはこの免責事項を読み、同意したものとみなされます。同意しない場合は、直ちに本プロジェクトの使用を停止し、削除してください。
---
> ソースからビルドする場合は [CONTRIBUTING.md](./CONTRIBUTING.md)(英語)を参照してください。
>
> MediaGo の翻訳をご検討中の方は [TRANSLATION.md](./TRANSLATION.md)(英語)をご参照ください。
+170 -140
View File
@@ -1,140 +1,170 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<br>
<a href="https://github.com/caorushizi/mediago/blob/master/README.en.md">English</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
## Intro
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
- **✅&nbsp; 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
- **📱&nbsp; 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
- **⚡️&nbsp; 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
- **🎉&nbsp; 支持 docker 部署** 支持 docker 部署 web 端,方便快捷。
## Quickstart
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
## 运行代码
```shell
# 代码下载
git clone https://github.com/caorushizi/mediago.git
# 安装依赖
pnpm install
# 首次安装需要 rebuild 一下
pnpm rebuild:workspace
# electron 开发环境
pnpm dev:electron
# electron 打包运行
pnpm release:electron
# server 开发环境
pnpm dev:server
# server 打包运行
pnpm release:server
```
## Releases
### v3.0.0 (2024.10.7 发布)
#### 软件下载
- [【mediago】 windows(安装版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
- [【mediago】 windows(便携版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
- [【mediago】 macos arm64apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
### docker 宝塔面板一键部署(推荐)
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/new/download.html?r=dk_mediago) 官网,选择正式版的脚本下载安装
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
3. 安装完成后在应用商店中找到`MediaGo`,点击安装,配置域名等基本信息即可完成安装
### 软件截图
![首页](./images/home.png)
### 重要更新
- 支持 docker 部署 web 端
- 更新桌面端 UI
### 更新日志
- 更新桌面端 UI
- 支持 docker 部署 web 端
- 新增视频播放,支持桌面端和移动端播放
- 修复 mac 打开无法展示界面的问题
- 优化了批量下载的交互
- 添加了 windows 的便携版(免安装哦)
- 优化了下载列表,支持页面中多个视频的嗅探
- 支持收藏列表手动导入导出
- 支持首页下载列表导出
- 优化了【新建下载】表单的交互逻辑
- 支持 UrlScheme 打开应用,并添加下载任务
- 修复了一些 bug 并提升用户体验
## 软件截图
![首页](./images/home.png)
![首页-dark](./images/home-dark.png)
![设置页面](./images/settings.png)
![资源提取](./images/browser.png)
## 技术栈
- react <https://react.dev/>
- electron <https://www.electronjs.org>
- koa <https://koajs.com>
- vite <https://cn.vitejs.dev>
- antd <https://ant.design>
- tailwindcss <https://tailwindcss.com>
- shadcn <https://ui.shadcn.com/>
- inversify <https://inversify.io>
- typeorm <https://typeorm.io>
## 鸣谢
- N_m2u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
- N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
- mediago 来自于 <https://github.com/caorushizi/hls-downloader>
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/en/guides.html?form=github">Quick Start</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://discord.gg/yxWBVRWGqM">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://www.reddit.com/r/MediaGo_Studio/">Reddit</a>
<br>
<a href="https://github.com/caorushizi/mediago/blob/master/README.zh.md">中文</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.it.md">Italiano</a>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<img alt="GitCode" src="https://gitcode.com/caorushizi/mediago/star/badge.svg">
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
A cross-platform video downloader with built-in sniffing — point it at a
page, pick what you want, and save. No packet capture, no browser
extensions to configure, no fiddling with command-line tools.
The app UI currently ships with English, Simplified Chinese, and Italian.
## ✨ What's inside
### 🌐 Browser extension for Chrome / Edge
See something you want on any site → click the extension → send it to
MediaGo. Detects video resources automatically, shows the count on the
toolbar badge, works with most mainstream video platforms including
YouTube, Bilibili and more. Ships bundled with the Desktop app — open
**Settings → More Settings → Browser extension directory** to find the
install folder.
### 🎬 YouTube and 1000+ sites
Powered by yt-dlp under the hood. Supports YouTube, Twitter/X, Instagram,
Reddit and [over a thousand more video sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md).
### 🦞 AI assistants can download for you — OpenClaw Skill
Using Claude Code, Cursor or another AI coding assistant? Install the
MediaGo skill and just say _"please download this video: &lt;url&gt;"_.
The AI handles the rest.
```shell
npx clawhub@latest install mediago
```
### 🔌 Works with other tools
MediaGo exposes a full HTTP API — scripts, automation tools and other
apps can create download tasks, query progress and manage the list
directly. The browser extension uses this same API to talk to the desktop
app; anyone else can tap in too.
### 🎞️ Built-in format conversion
After a download finishes, convert it to another format or quality
without leaving MediaGo. No more opening a separate tool for ffmpeg.
### 🐳 One-line Docker deployment
Headless install on your server, then access the web UI from anywhere on
the same network:
```shell
docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0
```
Available on [Docker Hub](https://hub.docker.com/r/caorushizi/mediago) and GHCR (`ghcr.io/caorushizi/mediago`) — same image, pick whichever registry is faster for you. Supports both Intel / AMD (amd64) and ARM (arm64). On the desktop build,
MediaGo listens on both `127.0.0.1` and your LAN IP out of the box, so
phones and tablets on the same Wi-Fi can open the web UI too.
## 📷 Screenshots
![Home](./images/home_en.png)
![Home — dark mode](./images/home-dark_en.png)
![Settings](./images/settings_en.png)
![Resource extraction](./images/browser_en.png)
## 📥 Download
### v3.5.0 (stable)
- [Windows — installer](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-win32-x64-3.5.0.exe)
- [Windows — portable](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-portable-win32-x64-3.5.0.exe)
- [macOS — Apple Silicon (arm64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-arm64-3.5.0.dmg)
- [macOS — Intel (x64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-x64-3.5.0.dmg)
- [Linux (deb)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-linux-amd64-3.5.0.deb)
- [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago): `docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0`
- **GHCR**: `docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago ghcr.io/caorushizi/mediago:3.5.0`
Browsing older releases? See the [GitHub Releases page](https://github.com/caorushizi/mediago/releases).
### 🪄 One-click Docker deployment via BT Panel
1. Install [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago) using the official script.
2. Log in to the panel, click **Docker** in the sidebar and finish the
Docker service setup (just follow the prompts).
3. Find **MediaGo** in the app store, click **Install**, configure your
domain, and you're done.
## 📝 What's new in v3.5.0
- **🌐 Browser extension** — sniff videos on any site, send to MediaGo
in one click
- **🎬 YouTube + 1000+ sites** — powered by yt-dlp
- **🦞 OpenClaw Skill** — download videos via AI coding assistants
- **🔌 HTTP API** — integrate with scripts, automation and third-party tools
- **🎞️ In-app format conversion** — choose output format and quality
- **🐳 Simpler Docker deployment** — mount a single folder, multi-arch images on GHCR
- **⚡ Faster startup** — backend rewrite, lower memory footprint, built-in video player
## 🛠️ Built with
[![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)](https://react.dev/)
[![Electron](https://img.shields.io/badge/Electron-191970?logo=electron&logoColor=white)](https://www.electronjs.org)
[![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)](https://vitejs.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com)
[![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-000?logo=shadcnui&logoColor=white)](https://ui.shadcn.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Ant Design](https://img.shields.io/badge/Ant_Design-0170FE?logo=antdesign&logoColor=white)](https://ant.design)
## 🙏 Acknowledgements
- [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE)
- [BBDown](https://github.com/nilaoda/BBDown)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [aria2](https://aria2.github.io/)
- [mediago-core](https://github.com/caorushizi/mediago-core)
## ⚖️ Disclaimer
> **This project is for educational and research purposes only. Do not use it for any commercial or illegal purposes.**
>
> 1. All code and functionality provided by this project are intended solely as a reference for learning about streaming media technologies. Users must comply with the laws and regulations of their jurisdiction.
> 2. Any content downloaded using this project remains the property of its original copyright holders. Users should delete downloaded content within 24 hours or obtain proper authorization.
> 3. The developers of this project are not responsible for any actions taken by users, including but not limited to downloading copyrighted content or impacting third-party platforms.
> 4. Using this project for mass scraping, disrupting platform services, or any activity that infringes upon the legitimate rights of others is strictly prohibited.
> 5. By using this project you acknowledge that you have read and agree to this disclaimer. If you do not agree, stop using the project and delete it immediately.
---
> Building from source? See [CONTRIBUTING.md](./CONTRIBUTING.md).
>
> Translating MediaGo? See [TRANSLATION.md](./TRANSLATION.md).
+153
View File
@@ -0,0 +1,153 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://discord.gg/yxWBVRWGqM">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://www.reddit.com/r/MediaGo_Studio/">Reddit</a>
<br>
<a href="https://github.com/caorushizi/mediago/blob/master/README.md">English</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/caorushizi/mediago/blob/master/README.it.md">Italiano</a>
<br>
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
<img alt="GitCode" src="https://gitcode.com/caorushizi/mediago/star/badge.svg">
<br>
<a href="https://trendshift.io/repositories/11083" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<hr />
</div>
跨平台视频下载器,内置嗅探 —— 打开网页、选一下想要的资源、保存完事。不用抓包、不用折腾浏览器插件、不用面对命令行。
应用界面目前内置中文、英文和意大利语。
## ✨ 主打功能
### 🌐 浏览器扩展(Chrome / Edge
浏览网页时遇到想下的视频 → 点扩展图标 → 一键发到 MediaGo。自动识别页面里的可下载资源,工具栏图标显示检测到的数量,主流视频网站(包括 YouTube、Bilibili 等)都能覆盖。扩展随桌面端安装包一起打包,在 **设置 → 更多设置 → 浏览器扩展目录** 就能找到安装文件夹。
### 🎬 支持 YouTube 和 1000+ 站点
底层用的是 yt-dlp。支持 YouTube、Twitter/X、Instagram、Reddit 等 [一千多个视频站点](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md)。
### 🦞 让 AI 助手帮你下载 —— OpenClaw Skill
在用 Claude Code、Cursor 等 AI 编程助手?装上 MediaGo Skill 后直接跟 AI 说"帮我下载这个视频:&lt;链接&gt;"就行,剩下的交给 AI。
```shell
npx clawhub@latest install mediago
```
### 🔌 可以和其他工具联动
MediaGo 提供一整套 HTTP 接口 —— 脚本、自动化工具、其他 App 都能直接调用 MediaGo 创建下载任务、查询进度、管理列表。浏览器扩展就是通过这套接口和桌面端对话的,你也可以接入自己的工作流。
### 🎞️ 内置格式转换
下载完成后可以直接在 MediaGo 里转换格式、选画质,不用再打开别的软件。
### 🐳 Docker 一键部署
服务器端一条命令部署,局域网内任意设备都能打开 Web 界面:
```shell
docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0
```
在 [Docker Hub](https://hub.docker.com/r/caorushizi/mediago) 和 GHCR`ghcr.io/caorushizi/mediago`)上同步发布 —— 同一份镜像,哪个源更快用哪个。支持 Intel / AMD (amd64) 和 ARM (arm64) 两种架构。桌面版同时监听 `127.0.0.1` 和局域网 IP,同一个 Wi-Fi 下的手机、平板可以直接打开 Web 界面。
## 📷 软件截图
![首页](./images/home.png)
![首页 — 深色模式](./images/home-dark.png)
![设置](./images/settings.png)
![资源提取](./images/browser.png)
## 📥 下载
### v3.5.0(正式版)
- [Windows — 安装版](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-win32-x64-3.5.0.exe)
- [Windows — 便携版](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-portable-win32-x64-3.5.0.exe)
- [macOS — Apple Silicon (arm64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-arm64-3.5.0.dmg)
- [macOS — Intel (x64)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-darwin-x64-3.5.0.dmg)
- [Linux (deb)](https://github.com/caorushizi/mediago/releases/download/v3.5.0/mediago-community-setup-linux-amd64-3.5.0.deb)
- [**Docker Hub**](https://hub.docker.com/r/caorushizi/mediago)`docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago caorushizi/mediago:3.5.0`
- **GHCR**`docker run -d --name mediago -p 8899:8899 -v /path/to/mediago:/app/mediago ghcr.io/caorushizi/mediago:3.5.0`
查看历史版本请移步 [GitHub Releases](https://github.com/caorushizi/mediago/releases)。
### 🪄 宝塔面板一键部署 Docker
1. 安装宝塔面板,前往 [宝塔面板官网](https://www.bt.cn/new/download.html?r=dk_mediago) 选择正式版的脚本下载安装
2. 登录宝塔面板,在菜单栏中点击 **Docker**,首次进入会提示安装 Docker 服务,点击立即安装并按提示完成
3. 在应用商店中找到 **MediaGo**,点击安装,配置域名等基本信息即可
## 📝 v3.5.0 更新要点
- **🌐 浏览器扩展**:任意网站一键嗅探视频、一键发到 MediaGo
- **🎬 YouTube + 1000+ 站点**:集成 yt-dlp
- **🦞 OpenClaw Skill**:通过 AI 编程助手下载视频
- **🔌 开放 HTTP 接口**:接入脚本、自动化工具和其他应用
- **🎞️ 内置格式转换**:选输出格式和画质
- **🐳 Docker 部署简化**:挂载一个目录即可,多架构镜像已迁至 GHCR
- **⚡ 启动更快**:后端重写,资源占用更低,内置视频播放器
## 🛠️ 技术栈
[![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)](https://react.dev/)
[![Electron](https://img.shields.io/badge/Electron-191970?logo=electron&logoColor=white)](https://www.electronjs.org)
[![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)](https://vitejs.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com)
[![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-000?logo=shadcnui&logoColor=white)](https://ui.shadcn.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Ant Design](https://img.shields.io/badge/Ant_Design-0170FE?logo=antdesign&logoColor=white)](https://ant.design)
## 🙏 鸣谢
- [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE)
- [BBDown](https://github.com/nilaoda/BBDown)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [aria2](https://aria2.github.io/)
- [mediago-core](https://github.com/caorushizi/mediago-core)
## ⚖️ 免责声明
> **本项目仅供学习和研究使用,请勿用于任何商业或非法用途。**
>
> 1. 本项目提供的所有代码和功能仅作为学习流媒体技术的参考,使用者需自行遵守所在地区的法律法规。
> 2. 使用本项目下载的任何内容,其版权归原始内容所有者所有。使用者应在下载后 24 小时内删除,或取得版权方授权。
> 3. 本项目开发者不对使用者的任何行为承担责任,包括但不限于:下载受版权保护的内容、对第三方平台造成的影响等。
> 4. 禁止将本项目用于大规模抓取、破坏平台服务或任何侵犯他人合法权益的行为。
> 5. 使用本项目即表示您已阅读并同意本免责声明。如不同意,请立即停止使用并删除本项目。
---
> 想从源码构建?见 [CONTRIBUTING.md](./CONTRIBUTING.md)(英文)。
>
> 想为 MediaGo 做翻译?见 [TRANSLATION.md](./TRANSLATION.md)(英文)。
尾注: 感谢[吾爱破解论坛](https://www.52pojie.cn/)
lp_Zain@www.52pojie.cn
+185
View File
@@ -0,0 +1,185 @@
# Contributing translations
Thanks for helping translate MediaGo! This guide walks you through adding a
new language from scratch, with a live-preview workflow so you can iterate
without rebuilding the app.
## TL;DR
```shell
git clone https://github.com/caorushizi/mediago.git
cd mediago
pnpm install
pnpm deps:download
pnpm dev:electron
```
1. Copy `packages/shared/common/src/i18n/resources/en.ts` to `<lang>.ts`,
translate every value.
2. Register the new locale in the shared resources, resolver, UI dropdown,
and browser extension (see below).
3. Open **Settings → Language**, pick your locale, and iterate. Vite HMR
reflects edits in the running app within a second.
4. Open a PR — we review and merge.
## Where strings live
- **Main app UI** (desktop + self-hosted web):
`packages/shared/common/src/i18n/resources/{en,it,zh}.ts`
- **Browser extension** (separate, smaller catalog):
`packages/mediago-extension/src/i18n/resources/{en,it,zh}.ts`
Resources are plain TypeScript modules — each file exports a flat object of
`key: "translation"` pairs. Keys are shared across languages; only values
change.
## Adding a new language end-to-end
Example: French (`fr`). Adjust the code to whichever language you're adding.
### 1. Create the resource file
Copy `en.ts` to `fr.ts` in the same directory and translate every **value**.
Leave all keys untouched:
```ts
// packages/shared/common/src/i18n/resources/fr.ts
export const fr = {
// ...translated values...
followSystem: "Système",
chinese: "中文",
english: "English",
french: "Français", // add your language's own name
displayLanguage: "Langue",
// ...
} as const;
```
Don't forget to add the new `french: "Français"` key to **every** resource
file (`en.ts`, `it.ts`, `zh.ts`, and your new `fr.ts`) so the Settings
dropdown can render it in each language.
### 2. Register the resource
Core app registration:
**`packages/shared/common/src/i18n/resources/index.ts`** — import the new
locale and add it to both exports:
```ts
import { fr } from "./fr";
// ...
export const i18nResources = { en, it, zh, fr } as const;
export const SUPPORTED_LANGUAGES = ["en", "it", "zh", "fr"] as const;
export { en, it, zh, fr };
```
**`packages/shared/common/src/i18n/config.ts`** — widen the
`resolveAppLanguage` return type and the check inside:
```ts
export type ResolvedAppLanguage = "zh" | "en" | "it" | "fr";
export function resolveAppLanguage(
language: string | undefined,
systemLocale: string | undefined,
): ResolvedAppLanguage {
if (
language === "zh" ||
language === "en" ||
language === "it" ||
language === "fr"
) {
return language;
}
// ...existing fallback...
}
```
If the new locale should follow the OS/browser locale automatically, add the
matching `systemLocale` prefix check in the same function.
**`packages/shared/common/src/types/index.ts`** — extend the `AppLanguage`
enum:
```ts
export enum AppLanguage {
System = "system",
ZH = "zh",
EN = "en",
FR = "fr",
}
```
**`apps/ui/src/App.tsx`** — if Ant Design ships a locale for your language,
import it and include it in `getAntdLocale`. Otherwise, explicitly fall back
to `enUS` for Ant Design components while your app strings still use your
translated resource file.
### 3. Add the Settings dropdown option
**`apps/ui/src/pages/setting-page/index.tsx`** — inside the **Language**
`<Select>`, add one line:
```tsx
options={[
{ label: t("followSystem"), value: AppLanguage.System },
{ label: t("chinese"), value: AppLanguage.ZH },
{ label: t("english"), value: AppLanguage.EN },
{ label: t("french"), value: AppLanguage.FR }, // new
]}
```
### 4. (Optional) Translate the browser extension
Same pattern, under `packages/mediago-extension/src/i18n/resources/`. It's a
much smaller catalog and lives in its own `index.ts`. Also update:
- `packages/mediago-extension/src/i18n/index.ts` for language resolution.
- `packages/mediago-extension/src/shared/types.ts` for the persisted language
union.
- `packages/mediago-extension/src/options/components/LanguageCard.tsx` for
the options-page selector.
- `packages/mediago-extension/public/_locales/<lang>/messages.json` for the
Chrome extension name, description, and action tooltip.
## Live preview workflow
The dev server uses **Vite HMR** — edits to any resource file are reflected
in the running app almost instantly, no restart required.
```shell
pnpm install
pnpm deps:download # fetch ffmpeg / BBDown (first clone only)
pnpm dev:electron # starts Electron with HMR
```
Once the window is up, open **Settings → Language** and switch to your new
locale. Then edit `fr.ts` in your editor — save, and the UI updates live.
Use this to catch overflowing strings, awkward wrapping, and untranslated
values before opening the PR.
## Submitting the PR
- **Branch**: `i18n/add-<lang>` — e.g. `i18n/add-fr`.
- **Commit**: follow [Conventional Commits](https://www.conventionalcommits.org/),
e.g. `feat(i18n): add French translation`.
- In the PR description, please include:
- A screenshot of **Settings → Language** with your new locale selected.
- A screenshot of at least one main screen (e.g. the download list) in
the new language.
- Confirmation that `pnpm check` passes locally.
## Tips
- **Keep placeholders intact.** Tokens like `{{count}}` or `{name}` are
interpolated at runtime — copy them verbatim into your translation.
- **Natural phrasing beats literal translation.** The English source is a
guide, not a cage. Idiomatic phrasing in your language is always better.
- **Unsure how a string is used?** Grep the key across the repo (e.g.
`rg '"displayLanguage"'`) — you'll find the component that renders it,
which gives you the UI context.
Questions? Comment on [issue #638](https://github.com/caorushizi/mediago/issues/638)
or open a new discussion. Thanks for contributing! 🌍

Some files were not shown because too many files have changed in this diff Show More