DownVid
This supports 1000+ websites, with a clean interface, across macOS / Windows / Linux.
AI translation
LunoRX เป็นแอปเล่นเพลง YouTube Music บน Mac, Windows และ Linux ฟรีและเปิดซอร์ส มีเนื้อเพลงเลื่อนตามเสียงร้อง และเก็บเพลย์ลิสต์ไว้บนเครื่องของคุณ
Translation pending
MediaRust · TypeScriptGitHub @LunoRX-RSOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Desktop music player for YouTube Music: Rust core, libmpv audio, word-synced lyrics, local-first playlists. Tauri 2 + Next.js.
Translation pending
Loading the full README.
Read the full README ↗A desktop music player for YouTube Music. A Rust core handles search, playback, the queue and sync, and the interface only shows what the core can actually do.
Tauri 2 · Rust (edition 2024) · Next.js 16 static export · React 19 · libmpv · macOS first, Windows and Linux supported
Version 1.0.0. It plays real YouTube audio and keeps your playlists on your own machine, with optional sync. Spotify search and playback are not supported. The Windows and Linux installers have not been run on a clean machine, and the macOS app is ad-hoc signed, so macOS asks you to approve it once. The Status table lists what works and what does not.
[!IMPORTANT]
Disclaimer
No affiliation. LunoRX and its contributors are not affiliated with, authorized by, endorsed by, or in any way officially connected with Google LLC, YouTube, YouTube Music, Spotify AB, or any of their subsidiaries or affiliates. The same goes for the lyric services the app can query (LRCLIB, LyricsPlus, Netease Cloud Music, QQ Music, Kugou, Boidu). This is an independent, unofficial, open-source project.
Trademarks. "Google", "YouTube", "YouTube Music", "Spotify" and all related names, marks, emblems and images are registered trademarks of their respective owners. They are used here only for identification and reference and do not imply any association with, or endorsement by, the trademark holders. There is no intention to infringe on these trademarks or to harm their owners.
Third-party services. The app talks to YouTube Music directly and, for lyrics, to several unofficial endpoints. These services have their own terms and may change, restrict or stop working without notice. You are responsible for using the app in a way that complies with the terms that apply to you. Security model and Lyrics describe exactly what is sent where.
Limitation of liability. The software is provided "AS IS", without warranty of any kind, and you use it at your own risk. In no event shall the authors or contributors be liable for any claim, damages or other liability, including any legal consequences, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use of or other dealings in it. You are responsible for any outcome of using this software. The MIT licence covers this repository.
Full-size originals are in assets/. All four were taken on macOS with real YouTube Music data.
LunoRX is a desktop-first music player built around one library that can hold tracks from more than one provider. Each playlist entry records which provider it came from. Your data lives on your machine first. An optional backend that you can host yourself syncs playlists between devices, and the app works without it.
Three rules shape the code:
PLAYING once the player confirms it, not when a Play command is sent.Listening
Finding music
youtube.com, music.youtube.com or youtu.be link into Search and it plays at once.⌘K / Ctrl+K.Lyrics
.lrc file. A calibration pass measures your input delay. Files are plain text in ~/Documents/LunoRX Lyrics/.Your data
https://play.lunorx.xyz/s/<token>), read only, shown on every device. Recreate it to replace the old one, or stop sharing. A shared page has an "Open in LunoRX" button and an embed.Interface
prefers-reduced-motion, and transparency follows prefers-reduced-transparency. The target is WCAG 2.2 AA.| Area | State |
|---|---|
| YouTube Music search | Works. Direct, no API key. |
| YouTube audio playback | Works through libmpv on a developer machine. libmpv must be installed. |
| YouTube sign-in | Google session cookies in the OS key store. This is a project-owner exception, described under Security model. It is not approval from the provider. |
| Spotify | Not supported. No search and no playback. The app reports it as unsupported. |
| Lyrics | Five unofficial endpoints, used with the owner's acceptance. Each one can be turned off. See Lyrics. |
| Sync with the backend | Works against a running backend: sign in or create an account, upload, background sync, share links, descriptions. Not production-hardened. Needs LUNORX_BACKEND_URL (default https://api.lunorx.xyz) to answer. |
| Library: albums and songs tabs | Partly sample data, labelled as a demo in the UI. Playlists and likes are real. |
| macOS bundle | Built by the release workflow with libmpv inside. Ad-hoc signed, not notarized. |
| Linux | Runs from source on Arch Linux under Hyprland (Wayland), with playback and MPRIS media controls. Other distributions, X11 and other window managers are targets that have not been run. Installers are not verified. Sign-in needs a Secret Service (GNOME Keyring or KWallet). See Linux notes. |
| Windows | NSIS installer built by the release workflow. Not verified on a real machine. |
| Release signing | macOS apps are ad-hoc signed (no Apple Developer account). Windows installers are unsigned. Update files are signed with the project's minisign key, and the app installs only updates that match it. |
| Licences | This repository is MIT. YouTube access comes from innerlight (MIT, in the sibling innerlight repository). libmpv is LGPL. THIRD_PARTY_NOTICES.md lists every component and licence. |
No clone, no build. One command installs the released AppImage for your user (no root) and adds LunoRX to your application menu:
curl -fsSL https://raw.githubusercontent.com/LunoRX-RS/desktop/main/scripts/install-linux.sh | sh
Start it from the menu, or run lunorx. The script finds the newest release, checks the download against the release's SHA256SUMS.txt and refuses to install it if it does not match. Read it first if you like: scripts/install-linux.sh.
| You want | Run |
|---|---|
| Any distribution, no root (default) | curl -fsSL <url> | sh |
| A system package on Debian, Ubuntu and relatives | curl -fsSL <url> | sh -s -- --deb |
| A specific release | curl -fsSL <url> | sh -s -- --version v1.0.0 |
| See what it would do, download nothing | curl -fsSL <url> | sh -s -- --dry-run |
| Remove it again | curl -fsSL <url> | sh -s -- --uninstall |
<url> is the address above. Your library and settings live in ~/.local/share/io.lunorx.desktop and are kept when you uninstall.
Things to know:
--force tries anyway). It does not support Alpine (musl)..deb pulls its dependencies in through apt.lunorx launcher notices and unpacks the AppImage on start instead, so it still runs, a little slower to open.--deb path has not been run at all.| Tool | Used for |
|---|---|
Rust stable with edition 2024 (cargo, rustfmt, clippy) | The core and the Tauri shell |
| Bun, or Node 22 with npm | The frontend. tauri.conf.json calls bun run dev and bun run build, so Bun has to be on the PATH for tauri dev and tauri build as configured |
| Tauri 2 platform prerequisites | See the Tauri docs for your OS |
| libmpv | Native playback. macOS: brew install mpv. Debian/Ubuntu: libmpv-dev. Arch: mpv |
| A Secret Service (Linux only) | GNOME Keyring or KWallet, to store the sign-in token |
On Debian/Ubuntu the build also needs build-essential pkg-config libssl-dev libdbus-1-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev. On Arch: base-devel webkit2gtk-4.1 libayatana-appindicator librsvg openssl dbus mpv.
org.mpris.MediaPlayer2.io.lunorx.desktop). Desktop media widgets, playerctl and the media keys your desktop routes to MPRIS players control it. On a bare window manager, run a daemon such as playerctld or bind the keys to playerctl.gnome-keyring-daemon from your session.C. The app sets it for itself, so a UTF-8 system locale such as en_US.UTF-8 is fine.WEBKIT_FORCE_VBLANK_TIMER=1 unless you set it yourself. Without it the window stopped repainting on a hybrid Intel/NVIDIA laptop under Hyprland (the page froze while audio kept playing). Set WEBKIT_FORCE_VBLANK_TIMER=0 to opt out.Building, running and testing need no secrets.
bun install # or: npm ci
bun run tauri dev # starts next dev on :1420, then the native app
The app starts without a backend. Sync actions report an error category until a backend answers at http://127.0.0.1:8080, the default base URL in src-tauri/src/sync/settings.rs.
bun run build # next build, static export to out/
bun run tauri build # bundles for the current OS
out/ has to exist before any Rust build of the crate, because tauri::generate_context! embeds it (frontendDist: ../out). Without it the Rust build fails.
bun run tauri dev # the interface inside the native app, with hot reload
The interface talks to the Rust core through Tauri IPC, so run it inside the app. bun run dev alone serves the pages in a browser, but there is no core behind them: every command fails, and the screens show their error states.
┌──────────────────────────────────────────────────────────────┐
│ Desktop UI (src/) Next.js static export · React 19 │
│ presentation · navigation · input · login UI │
└───────────────┬──────────────────────────────────────────────┘
│ Tauri IPC (typed DTOs, validated arguments)
┌───────────────▼──────────────────────────────────────────────┐
│ Commands (src-tauri/src/commands/) thin boundary │
│ validate input → call core → return DTO. No business logic. │
└───────────────┬──────────────────────────────────────────────┘
│
┌───────────────▼──────────────────────────────────────────────┐
│ Core (src-tauri/src/core/) no UI, no Tauri types │
│ search · playback state machine · queue · lyrics · │
│ library · discovery · provider registry · caches │
├──────────────────────────────────────────────────────────────┤
│ Sync (src-tauri/src/sync/) backend client: auth, │
│ local store, offline queue, conflicts │
├──────────────────────────────────────────────────────────────┤
│ Platform (src-tauri/src/platform/) macOS media, menu │
└──────────────────────────────────────────────────────────────┘
│ HTTP (optional)
LUNOR-X backend accounts + playlists only
The dependency rules are checked in review:
sync/.desktop/
├─ src/ frontend
│ ├─ app/ Next.js entry, global CSS and design tokens
│ ├─ components/ shared UI, layout shell (Sidebar, TopBar, players)
│ ├─ features/ one folder per screen: home, explore, search,
│ │ library, artists, playlists, queue, player,
│ │ settings, sync, accounts, connections, offline…
│ ├─ lib/ stores (Zustand), playback, lyrics, prefetch, app style
│ └─ services/tauri/ one typed wrapper per IPC command group
├─ src-tauri/
│ ├─ src/core/ domain, search, playback/, lyrics, library, discovery
│ ├─ src/commands/ IPC handlers: player, search, library, lyrics, sync…
│ ├─ src/sync/ backend client, local store, offline queue
│ ├─ src/platform/ macOS Now Playing, media keys, menu
│ ├─ capabilities/ Tauri permission sets
│ ├─ build.rs declares the app manifest (every command, by name)
│ └─ tests/ ACL guards per command group, opt-in live tests
├─ tests/ frontend tests (bun:test)
├─ scripts/release/ release helpers
├─ .github/workflows/ release workflow
├─ AGENTS.md CLAUDE.md rules for coding agents working here
├─ PRODUCT.md DESIGN.md product context and the visual system
└─ RELEASING.md how a release is made
A new command has to be registered in four places, and a test fails if one is missing:
src-tauri/src/commands/*.rsgenerate_handler! in src-tauri/src/lib.rssrc-tauri/build.rs (AppManifest::commands)allow-<command-name> in src-tauri/capabilities/default.jsonThe tests/acl_*.rs files read those files and check that every handler in a group is both declared and allowed. Arguments are validated at the boundary (sizes, id shapes, ranges). Errors cross IPC as fixed categories, without raw messages or stack traces.
| Group | Commands |
|---|---|
| Player | player_load player_play player_pause player_resume player_stop player_seek player_set_volume player_state player_progress player_skip player_observe player_track_info player_open_external player_audio_settings player_set_audio_settings |
| Search and discovery | search_query search_cancel search_suggestions discovery_feed providers_list |
| Library | library_likes library_set_like library_artist library_album library_history library_clear_history |
| Lyrics | lyrics_source_fetch lyrics_source_search lyrics_source_bind lyrics_source_unbind lyrics_settings_get lyrics_settings_set · lrc_load lrc_save lrc_delete lrc_reveal lrc_list |
| Sync | sync_sign_in sync_sign_out sync_status sync_create_local_playlist sync_add_local_entry sync_remove_local_entry sync_upload_playlist sync_flush_pending sync_discard_pending sync_resolve_conflict sync_list_local_playlists |
| Providers | youtube_connect youtube_connect_finish youtube_disconnect youtube_connection_status provider_credential_set provider_credential_status provider_credential_clear |
| Updates | update_check update_install |
| Offline | offline_status offline_keep offline_cancel offline_remove |
Events come back through the same typed channel: core://player/state, core://player/progress, core://library/changed, core://history/changed, core://history/error, core://offline/changed and core://youtube/connection. Each subscription is removed when its component unmounts.
UI intent ─► player_load ─► core state machine ─► native host (libmpv)
▲ │
└── confirmed events ──┘
innerlight resolves YouTube audio (one standard request, the signed-in cookie when connected; a sign-in or verification challenge is reported and not worked around), a bounded-range loopback proxy fetches it, and libmpv plays it. This path relies on the project-owner exception described under Security model.Sources are tried in the order you set. The default order is your own file, then word-timed sources, then line-timed ones, then plain text.
| Source | Provides | Notes |
|---|---|---|
| My LRC | Line timing | Files made with Lyric Maker |
| LyricsPlus | Word timing | A cold request can take several seconds. Use "Try again" in the source sheet |
| Netease | Word and line timing | Supports search by title |
| LRCLIB | Line timing, plain text | Fuzzy matching with duration checks |
| QQ Music | Line timing | Supports search by title |
| Kugou | Line timing | Supports search by title |
| Boidu | Word timing | Answers only for tracks it has cached |
| YouTube Music | Plain text | No timestamps |
src-tauri/src/core/playback/lyric_sources/.style-src 'self' with no inline styles, script-src limited to the app and YouTube, and images only from the app, data: and the known thumbnail hosts. This is why motion code mounts on the client only.AGENTS.md records that decision. It is a decision by the project, and the provider has not approved it. Spotify has no such exception and is unsupported.The theme is dark glass and blur with one accent, Lumen Lavender (#c5b4ff), which marks live state and nothing else. Panels have no borders. Only the sidebar and the player bar use blur, and everything else is flat. Components are ln- classes plus Tailwind v4 tokens declared in src/app/globals.css.
Read both before UI work. Loading, empty, unavailable, unauthorized, unsupported, failed, retrying and offline each have their own look, and an unsupported action is explained on screen instead of being hidden.
| Action | macOS | Windows / Linux |
|---|---|---|
| Quick search and commands | ⌘K | Ctrl+K |
| Back / Forward | ⌘[ / ⌘] | Ctrl+[ / Ctrl+] |
| Collapse the sidebar | ⌘B | Ctrl+B |
| Play or pause (not while typing) | Space | Space |
| Close Now Playing, palette or dialog | Esc | Esc |
These are the defaults for your system. In Settings > Keyboard shortcuts you can press Change, record any combination you like, and Reset it again. Choices are stored per device, so macOS, Windows and Linux machines each keep their own. Keys are matched by physical key, so they work on any keyboard layout. Lyric Maker and the macOS Playback menu have their own shortcuts, listed on the same page.
Run the gates for the part you changed. The policy is zero warnings. Blanket #[allow] is not accepted, and a targeted one needs the smallest possible scope and a one-line reason. Use --locked, and do not update dependencies to silence a lint.
Rust, from src-tauri/:
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
Frontend, from the repository root:
bun run lint # tsc --noEmit
bun run build # next build, static export
bun test tests/ # frontend tests (bun:test)
| Suite | What it covers |
|---|---|
Unit tests (#[cfg(test)] next to the code) | Domain, search engine, playback state machine, queue, autoplay, lyrics matching and parsing, lyric sources against local mock servers, library, sync engine, persistence, and the token store (an in-memory double, so tests never touch the real keychain) |
tests/acl_*.rs | Every command is in the manifest and allowed in the capability set |
tests/backend_sync_live.rs | A real sign-in and playlist flow. Runs only when LUNORX_LIVE_BACKEND_URL is set, and otherwise skips |
tests/youtube_live.rs | Live YouTube Data API search. Runs only when LUNORX_LIVE_YOUTUBE_KEY is set, and otherwise skips |
tests/*.test.mjs | Frontend store logic |
The last full Rust run passed 648 tests with no failures, and bun test passed 23. Run them yourself for a current number. On hosted Windows CI the one test that opens a real audio device is skipped.
Not covered: end-to-end UI tests, a release bundle run on Windows or Linux, load tests, and playback against a real player in CI.
A release is a version tag (for example v1.0.0). A tag without -rc is a full release and feeds the Stable update channel; every tag feeds Dev. Pushing the tag builds the installers and publishes them with SHA256SUMS.txt. The version lives in package.json and src-tauri/Cargo.toml, and the workflow rejects a tag that does not match both. RELEASING.md has the full steps, the platform table and the signing secrets.
| File | Purpose |
|---|---|
| AGENTS.md | Working rules: architecture, hard rules, commands, definition of done |
| CLAUDE.md | Notes for Claude Code sessions |
| PRODUCT.md | Who the app is for and what it must not become |
| DESIGN.md | Tokens, components, motion and accessibility rules |
| CONTRIBUTING.md | Branching and quality gates |
| INSTALL.md | Installing a release, and the macOS "could not verify" warning |
| RELEASING.md | Tags, installers, signing |
| SECURITY.md | How to report a vulnerability (privately, through GitHub) |
| THIRD_PARTY_NOTICES.md | Components, licences and what needs attention |
| CODE_OF_CONDUCT.md | Community rules |
| LICENSE | MIT, covering this repository |
| and |
offline_set_limit| What using the app and a LunoRX account means, and what is stored and sent |