No description https://muxite.com
  • Rust 50.6%
  • TypeScript 47.1%
  • JavaScript 0.9%
  • Swift 0.6%
  • CSS 0.5%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
buttilda c6e7cf3ee4
All checks were successful
build / changes (push) Successful in 5s
build / frontend (push) Successful in 58s
build / backend (push) Successful in 1m7s
build / docker (push) Successful in 3m29s
bump version
2026-10-23 13:15:13 +03:00
.forgejo/workflows Stop publishing to the AltStore source now Muxite is in the App Store 2026-10-21 16:39:05 +03:00
backend bump version 2026-10-23 13:15:13 +03:00
frontend bump version 2026-10-23 13:15:13 +03:00
scripts One version for the project, server and app 2026-09-07 21:51:25 +03:00
.dockerignore cleanup TODO 2026-09-14 09:29:05 +03:00
.gitignore Update dependencies 2026-10-16 14:44:38 +03:00
CLAUDE.md Remove YouTube integration 2026-10-14 17:05:44 +03:00
Dockerfile Move muxite's repo 2026-10-16 18:48:34 +03:00
LICENSE license 2026-10-14 19:45:33 +03:00
muxite.code-workspace trim 2026-08-04 20:41:52 +03:00
process-compose.yaml duo and ipad scripts 2026-10-21 13:47:43 +03:00
README.md Stop publishing to the AltStore source now Muxite is in the App Store 2026-10-21 16:39:05 +03:00
TODO.md trim to-do 2026-10-13 12:04:21 +03:00

Muxite

A web UI for inspecting and editing Matroska files in a self-hosted media library. It reads the container itself (no ffprobe) and edits through mkvtoolnix, so metadata changes are instant and only stream removal or conversion needs a remux. An iPhone app wraps the same UI.

Features

  • Streams: list every video, audio, subtitle and attachment track, with codec detail Matroska hides behind one CodecID (DTS vs DTS-HD, E-AC-3 vs E-AC-3 Atmos, TrueHD Atmos). Rename, retag language, set default/forced flags, remove tracks, download any track raw.
  • Subtitles: visual editor for SRT, ASS/SSA and WebVTT (embedded or sidecar files), raw text editor, shift, add from upload, server path, drag-and-drop or OpenSubtitles search. Bitmap subtitles (PGS, VobSub) are decoded to images for side-by-side OCR with saved drafts.
  • Chapters and tags: edit per file, or apply a template across a folder.
  • Bulk scripts: a small rule language (remove subtitle where lang not in (eng, jpn), unset title where type == video, remove tags, …) run against every MKV in a folder, with a dry run that shows the per-file plan first.
  • Files: move, rename, delete files and folders (sidecar subtitles follow), convert MP4/AVI to MKV, verify MKV structure, filter a library for missing subtitles or language tags.
  • Torrents: search every Prowlarr indexer, add to qBittorrent, pick files, hardlink finished downloads into a library. Hidden unless Prowlarr and qBittorrent are configured.
  • Jobs: remuxes run in the background with live progress over a WebSocket and can be cancelled.

Running it

Muxite ships as a Docker image built by the build Forgejo Action on every push to main.

muxite:
  image: git.buttilda.com/muxite/muxite:latest
  container_name: muxite
  volumes:
    - /media:/media
    - ./muxite-auth.toml:/etc/muxite/auth.toml:ro
  restart: unless-stopped
  environment:
    - MEDIA_DIR=/media
    - LIBRARIES=Movies,TV Shows
    - AUTH_FILE=/etc/muxite/auth.toml
    - MUXITE_JWT_SECRET=${MUXITE_JWT_SECRET}

Put it behind any reverse proxy that passes WebSockets (Caddy does by default):

muxite.example.com {
    reverse_proxy muxite:3000
}

Authentication

One account, read from the TOML file at AUTH_FILE. Generate the hash with the image:

docker run --rm -it git.buttilda.com/muxite/muxite muxite hash-password
username = "you"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."

Create the file before the first docker compose up, or Docker makes a directory at the mount path and the server fails with "Is a directory". MUXITE_JWT_SECRET is any long random string (openssl rand -hex 32); changing it signs everyone out. AUTH_DISABLED=1 runs with no login at all and is meant for local development only.

Environment variables

Variable Default Description
MEDIA_DIR /media Root directory containing the libraries
LIBRARIES Movies,TV Shows Comma-separated library folder names under MEDIA_DIR
PORT 3000 HTTP port
AUTH_FILE Credentials file (see above). Required unless AUTH_DISABLED=1
MUXITE_JWT_SECRET Signs session tokens. Required with AUTH_FILE
JOB_PARALLEL 5 Max concurrent remux jobs
OPENSUBTITLES_API_KEY, OPENSUBTITLES_USERNAME, OPENSUBTITLES_PASSWORD Enables OpenSubtitles search
QBITTORRENT_URL http://qbittorrent:8080 qBittorrent WebUI
QBITTORRENT_USERNAME, QBITTORRENT_PASSWORD qBittorrent credentials
TORRENT_DOWNLOADS_DIR /media/downloads Where qBittorrent's save path appears on Muxite's filesystem (for hardlinks)
PROWLARR_URL, PROWLARR_API_KEY Both required for torrent search; the API key is in Prowlarr's Settings → General
INDEXER_PROXY http:// or socks5:// proxy for indexer detail-page fetches only (see below)

Indexer detail fetches

The uploader's notes shown under a search result come from the indexer's own detail page, and those sit behind Cloudflare, which challenges datacenter IPs. INDEXER_PROXY routes only those fetches through a machine with a residential IP; Prowlarr's own API is never proxied. The startup log says which path is in use. To check the proxy itself, this must print JSON:

curl -s --proxy "$INDEXER_PROXY" "https://apibay.org/t.php?id=13114578" | head -c 200

Development

Requires Rust (pinned by backend/rust-toolchain.toml), mkvtoolnix, pnpm and process-compose.

process-compose -p 8083      # backend under watchexec + vite dev server; open http://localhost:5174

The stack runs against the checked-in test-media/ tree with auth disabled. -p 8083 is needed on every invocation (including down and run): process-compose allows one instance per port and other projects on this machine hold 8080 and 8082.

Chores are process-compose tasks:

process-compose -p 8083 run fmt      # rustfmt + prettier
process-compose -p 8083 run check    # what CI checks: fmt, clippy -D warnings, biome, prettier, tsc, build
process-compose -p 8083 run test     # cargo test + vitest
process-compose -p 8083 run iphone   # the app in the simulator against this stack
process-compose -p 8083 run duo      # likewise on an iPhone Duo
process-compose -p 8083 run version -- 1.1.0   # set the version everywhere it lives

Or per side: cargo from backend/ (the toolchain file is looked up from the cwd), and pnpm dev / compile / lint / test / build from frontend/.

To run the frontend against production data with no local backend:

cd frontend && MUXITE_BACKEND=https://muxite.buttilda.com pnpm dev

That is the live app with the real login. Edits made there are real.

Layout

backend/     Rust axum server. ebml.rs is the Matroska parser; media.rs the subtitle
             parsing and mkvtoolnix wrappers; script.rs the bulk-edit DSL; jobs.rs the
             job queue; prowlarr.rs / qbittorrent.rs / opensubtitles.rs the integrations.
frontend/    React 19 + TypeScript SPA: vite, TanStack Router (file-based routes),
             Tailwind 4, react-aria-components wrapped in components/ui/. api/types.ts
             mirrors the serde structs by hand; update it when a Rust response changes.
frontend/src-tauri/   The iPhone app: a Tauri 2 shell around the same bundle, plus a
             native tab bar plugin. gen/apple/ is the generated Xcode project, checked in.
scripts/     version.mjs, which writes one version to every file that carries it.
Dockerfile   Three stages: frontend check + build, Rust build, Alpine runtime with mkvtoolnix.
process-compose.yaml   The dev stack and the tasks above.

The frontend is one bundle for two hosts. lib/net.ts detects whether it is served by the backend (relative URLs, session cookie) or running inside the Tauri app (user-entered server URL, bearer token from POST /api/login, native fetch). Nothing else knows the difference.

iPhone app

cd frontend
pnpm tauri:iphone            # creates + boots a "Muxite iPhone" simulator, builds, runs
pnpm tauri:duo               # likewise "Muxite Duo"
pnpm tauri:ipad              # likewise "Muxite iPad"
pnpm icons                   # regenerate icon sets from frontend/icon.svg

The app asks for a server URL on first launch and keeps it only if /health answers. The web view has no console; uncaught errors go to the native log, which tauri ios dev streams.

The manual testflight Forgejo workflow ships it from the Mac runner: it archives, exports and uploads to App Store Connect. Bump the version first (run version -- x.y.z); App Store Connect refuses a second build for a version it has seen. It needs the APPLE_API_* secrets on the Muxite org. The app record and store listing live outside this repo.

Deploying a change

Commit and push to main, wait for the build Action, then on the server:

docker compose pull muxite && docker compose up -d muxite