Spotify-style music player with YouTube Music integration, real-time lyrics, custom playlists, and PWA support
  • TypeScript 54%
  • Kotlin 26.4%
  • Rust 15.9%
  • JavaScript 1.4%
  • CSS 0.9%
  • Other 1.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
vndangkhoa d2ba1a8c59
Some checks failed
🐳 Build & Push Docker Image – kv-music / build-and-push (push) Has been cancelled
docs: modernize README with comparison matrix, badges, and quickstart
2026-09-14 21:24:57 +07:00
.github/workflows ci: add multi-registry Docker build/push workflow & build-push.sh script 2026-07-31 14:52:48 +07:00
.opencode/plans feat: track sharing (OG previews), downloads (audio/video), Media Session & SPA fallback 2026-08-13 17:27:29 +07:00
android-app chore(android): release 1.2.2 (versionCode 3, settings update check) 2026-09-07 21:25:42 +07:00
backend-rust feat(settings): check for updates with changelog notes (web + android) 2026-09-07 21:18:39 +07:00
docs feat(android): Liquid Glass redesign with rebuilt data layer 2026-08-16 19:59:26 +07:00
frontend-vite feat(settings): check for updates with changelog notes (web + android) 2026-09-07 21:18:39 +07:00
.dockerignore chore: clean up repo - remove 163MB node binaries, .deb files, legacy Windows scripts & obsolete docs 2026-08-08 20:52:44 +07:00
.gitignore perf(feed,library,waveform): song-by-song lists, opt-in waveform fetch, hold-to-seek 2026-09-07 20:11:40 +07:00
build-push.sh ci: add multi-registry Docker build/push workflow & build-push.sh script 2026-07-31 14:52:48 +07:00
CHANGELOG.md docs(changelog): record 1.2.2 release notes 2026-09-07 21:32:29 +07:00
deploy_commands.sh Update: Mobile UX, Show All, Infinite Scroll & New Categories 2026-01-01 11:12:21 +07:00
docker-compose.yml feat: auto cookie refresh, IPv6 support & fixes 2026-08-11 13:40:02 +07:00
Dockerfile feat(settings): check for updates with changelog notes (web + android) 2026-09-07 21:18:39 +07:00
README.md docs: modernize README with comparison matrix, badges, and quickstart 2026-09-14 21:24:57 +07:00
start.sh feat: Redesign UI to match NhạcCủaTui, update kv-music branding, PWA & start scripts 2026-07-31 14:48:26 +07:00
youtube-integration-knowledge.md docs: YouTube integration knowledge base (bot detection, IPv6, cookies, formats, debugging) 2026-08-11 17:22:59 +07:00

🎧 KV-Music — Modern Self-Hosted YouTube Music Streaming & Sync Hub

KV Music Logo

A sleek, high-performance music streaming app powered by YouTube Music, Rust Axum, React 19, time-synced lyrics, and multi-device account pairing.

GitHub Stars GitHub Forks React Rust Vite Docker Image Android Native License: MIT

Features • Comparison • Architecture • Quick Start • Docker • API • License


⚡ Why KV-Music?

Commercial music streaming services charge heavy subscription fees, inject intrusive ads, restrict background mobile playback, and silo your data. Self-hosted servers like Navidrome require maintaining your own audio files on disk.

KV-Music combines the boundless catalog of YouTube Music with the freedom of self-hosting. Stream millions of tracks and official music videos with zero ads, real-time synchronized scrolling lyrics, full account persistence, and cross-device 6-digit pair codes.

📊 Competitive Matrix

Feature 🎧 KV-Music 🔴 YouTube Music (Free) 🟢 Spotify (Free) 📻 Navidrome
Catalog Access Full YouTube Music + Video YouTube Music Spotify Catalog Local Files Only
Zero Ad Interruptions ✅ 100% Ad-Free ❌ Frequent Ads ❌ Audio Ads ✅ Ad-Free
Background & Lockscreen ✅ Native PWA + MediaSession ❌ Premium Paywall ⚠️ Limited ✅ Yes
Real-Time Synced Lyrics ✅ LRCLIB, SimpMusic, ZingMP3 ⚠️ Static / Delayed ⚠️ Spotify Lyrics ⚠️ Local .lrc only
Cross-Device Pairing ✅ Instant 6-Digit Pair Code ❌ Google Login only ❌ Spotify Connect only ❌ Credentials only
1-Tap Audio/Video Toggle ✅ Instant Switch ❌ Premium Paywall ❌ Canvas Only ❌ Audio Only
Resource Efficiency ⚡ Compiled Rust Axum (<30MB) 🌐 Google Cloud 🌐 Cloud 🟡 Go Backend
Self-Hosted Privacy ✅ 100% Local Storage / NAS ❌ Full Google Tracking ❌ Full Tracking ✅ 100% Local

Features

Playback & Discovery

  • YouTube Music Integration - Search and stream millions of songs via YouTube
  • Smart Audio Format Negotiation - Serves WebM/Opus by default (open codec, plays everywhere including VS Code's webview) and auto-switches to m4a/AAC for browsers that can't play WebM (e.g. Safari)
  • Universal Search - One search box returns real Songs, Albums, Playlists and Artists
  • Real-Time Charts (BXH) - Official YouTube Music charts: Trending 20, Top 100, Daily Top Music Videos (VN / KR / US / Global)
  • New Releases (MỚI PHÁT HÀNH) - Real latest releases per region from YouTube Music
  • Top Artists - Real artist rankings with actual channel avatars & subscriber counts
  • Video Mode - Toggle between audio and video playback with a single tap
  • Smart Recommendations - Get similar tracks based on what you're playing
  • Queue Management - Full queue with bottom sheet UI and add-to-queue

Accounts & Sync

  • Real Account System - Register / login with password (argon2 hashed), stored server-side and persisted on your NAS
  • Cross-Device Pairing - Pair code links any device to your account (full account takeover)
  • Liked Songs - Heart tracks to save them to your personal collection
  • Custom Playlists - Create and manage unlimited playlists
  • Follow Artists - Track your favorite artists with photos and info
  • Saved Albums - Save full albums from YouTube Music
  • Recently Played - Auto-tracked listening history

Lyrics

  • Real-Time Synced Lyrics - Time-synced lyrics that highlight as songs play
  • Multiple Sources - LRCLIB, SimpMusic, lyrics.ovh for maximum coverage
  • Bottom Sheet Panel - Slide-up lyrics panel with drag-to-dismiss gesture
  • Auto-Scroll - Lyrics follow the current playback position

Interface

  • Responsive Layout - Beautiful on desktop, tablet, and mobile
  • Collapsible Sidebar - Desktop sidebar with quick navigation (toggle via hamburger)
  • Right Panel - Now Playing card with toggle button in header
  • Dark Glassmorphism UI - Modern translucent design with blur effects
  • PWA Support - Install as a standalone app on any device
  • Mobile Search Bar - Always-visible search bar in the mobile header
  • Mobile Bottom Nav - Quick access to Discovery and Library on mobile
  • FullPlayer - Immersive player with Song/Video toggle, action row, and playback controls
  • MiniPlayer - Compact bottom bar with progress indicator, skip, and play controls

Player Features

  • Audio/Video Overlap Prevention - Smart switching between audio and video modes
  • Mobile Full Player - Dedicated mobile player with drag-to-dismiss, swipe between queue/lyrics
  • Synced Lyrics - Time-aligned lyrics that scroll with playback (SimpMusic first for Vietnamese)
  • Volume Control - Desktop volume slider (hidden on mobile)
  • Shuffle & Repeat - Full playback mode controls
  • Share & Download - Share track URL or download directly from mobile player

Quick Start

services:
  kv-music:
    # Pull from any of these registries:
    # docker.io/vndangkhoa/kv-music:latest
    # ghcr.io/vndangkhoa/kv-music:latest
    # git.khoavo.myds.me/vndangkhoa/kv-music:latest
    image: vndangkhoa/kv-music:latest
    container_name: kv-music
    restart: unless-stopped
    ports:
      - "3110:8080"
    environment:
      - PORT=8080
      - RUST_LOG=info
      - PYTHONUNBUFFERED=1
      - COOKIE_FILE=/app/cookies.txt
      # - FORCE_IPV6=0   # uncomment to force IPv4 for yt-dlp
    dns:
      - 8.8.8.8
      - 1.1.1.1
    volumes:
      - ./data:/tmp/kv-music-downloads
      - ./cache:/tmp/kv-music-cache
      - ./cookies.txt:/app/cookies.txt:ro
      - ./users:/app/data
    networks:
      - kvnet
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

networks:
  kvnet:
    enable_ipv6: true
    ipam:
      config:
        - subnet: 172.20.0.0/16
        - subnet: fd00:1::/64
mkdir -p data cache users
docker compose up -d

Open http://localhost:3110 and start listening!

IPv6 & DNS note: YouTube's bot detection blocks many IPv4 routes but allows IPv6. The compose file above gives the container IPv6 (requires Docker with IPv6 enabled: "ipv6": true, "fixed-cidr-v6": "fd00::/64" in /etc/docker/daemon.json or Docker Desktop Engine settings) and uses external DNS (Docker's embedded DNS strips AAAA records). The backend probes actual IPv6 connectivity (TCP connect, 3s timeout) and only then adds --force-ipv6 to yt-dlp; if IPv6 is assigned but not routed (common on Synology Docker), it automatically falls back to IPv4. Set FORCE_IPV6=0 to disable, FORCE_IPV6=1 to force.

1-Click Docker Deploy (no cookies)

docker run -d --name kv-music -p 3110:8080 \
  -v kv-music-data:/tmp/kv-music-downloads \
  -v kv-music-cache:/tmp/kv-music-cache \
  -v kv-music-users:/app/data \
  git.khoavo.myds.me/vndangkhoa/kv-music:latest

Note: the docker run one-liner uses Docker's default bridge network (IPv4-only). If YouTube bot-blocks your IPv4 route, prefer the docker-compose setup above (IPv6-enabled network + external DNS), or add: --network host --dns 8.8.8.8


Docker Deployment

Property Value
Registry git.khoavo.myds.me/vndangkhoa/kv-music
Tag latest
Port 3110 → 8080
Platform linux/amd64
Base debian:bookworm-slim

Environment Variables

Variable Default Description
PORT 8080 Backend server port
RUST_LOG info Log level (info, debug, warn, error)
PYTHONUNBUFFERED 1 Python stdout buffering (recommended)
COOKIE_FILE /app/cookies.txt Path to the Netscape-format cookies file for YouTube
FORCE_IPV6 auto Force yt-dlp to use IPv6 (1 = always, 0 = never; default: auto-probes IPv6 connectivity and falls back to IPv4 on network errors)

Volumes

Container Path Purpose
/tmp/kv-music-downloads Downloaded audio files (auto-cleaned)
/tmp/kv-music-cache Search and metadata cache
/app/data Persistent user accounts (users.json) and auto-refreshed cookies (cookies.txt)
/app/cookies.txt YouTube cookies file (optional, read-only mount; auto-refreshed file in /app/data takes priority)

Networks

Network Purpose
kvnet Dual-stack (IPv4 + IPv6) bridge network required so yt-dlp can connect to YouTube over IPv6 (see Automatic Cookie Refresh note on bot detection)

Build from Source

git clone https://git.khoavo.myds.me/vndangkhoa/kv-music.git
cd kv-music
docker build -t kv-music:latest .
docker run -d -p 3110:8080 -v kv-music-users:/app/data kv-music:latest

Note: BuildKit is required for cargo cache mounts. Enable with export DOCKER_BUILDKIT=1 or use Docker Desktop.

Note: the simple docker run above uses the default IPv4-only bridge network. If YouTube bot-blocks your IPv4 route, add --network host --dns 8.8.8.8 (or use the docker-compose setup with the IPv6 kvnet network).

yt-dlp Updates

yt-dlp is auto-updated to the latest nightly binary on every container start (nightly builds include the newest YouTube anti-bot workarounds such as PO tokens), and you can also click Cài Đặt → Check Update in the app to update it on demand.

YouTube increasingly rate-limits server-side requests (HTTP 429 / "Sign in to confirm you're not a bot"). KV Music can fetch fresh YouTube session cookies automatically — no manual export needed:

  • Click Cài Đặt → Lấy Cookie Mới (Settings → Fetch Fresh Cookies) in the app, or
  • On server start, if no cookie file exists at all, fresh cookies are fetched automatically, or
  • Automatically when YouTube rejects the current cookies: if yt-dlp reports "cookies are no longer valid" / "Sign in to confirm you're not a bot", the backend discards the rejected cookie file, refreshes the anonymous session and retries the download — no manual re-export needed.

Fresh cookies are written to a writable, persistent location (/app/data/cookies.txt in Docker, data/cookies.txt locally). After a refresh, all in-memory caches are cleared, so every subsequent fetch (search, charts, new releases, artists, streams, browse) uses the new cookies.

IPv6 matters too: YouTube's bot detection also blocks many residential IPv4 routes while allowing IPv6. The docker-compose kvnet network is dual-stack so yt-dlp connects over IPv6 when the host supports it (the backend auto-adds --force-ipv6; set FORCE_IPV6=0 to disable). Docker's embedded DNS strips AAAA records, so the compose file points at 8.8.8.8 / 1.1.1.1 instead.

Important — anonymous cookies cannot override an IP-level block. If your NAS IP itself is bot-flagged by YouTube (common on residential/Synology connections), "Sign in to confirm you're not a bot" persists even with fresh anonymous cookies. Two real fixes exist: enable routed IPv6 in the container (preferred, no cookies needed), or export a logged-in cookies.txt from your browser — a logged-in session defeats the IP-level block (see Troubleshooting on a NAS).


YouTube Cookies

YouTube increasingly rate-limits and blocks server-side requests (HTTP 429 "Too Many Requests" / "Sign in to confirm you're not a bot"). Passing a cookies.txt file exported from a real browser session fixes this.

Step 2: Export cookies for youtube.com

  1. Log in to YouTube (or YouTube Music) in your browser. Logging in gives the most reliable session, but a logged-out session also works in most cases.
  2. Open the cookie exporter extension.
  3. Important: export cookies only for youtube.com (and music.youtube.com) — not all sites.
  4. Save the exported file as cookies.txt in the same directory as your docker-compose.yml.

The file must be in Netscape cookie format, which looks like this:

# Netscape HTTP Cookie File
# This file is generated by yt-dlp.  Do not edit.

.youtube.com	TRUE	/	TRUE	1799121502	__Secure-YNID	20.YT=a5DZ...
.youtube.com	TRUE	/	TRUE	0	YSC	s7lGd3GOn1U
.youtube.com	TRUE	/	TRUE	1799140599	VISITOR_INFO1_LIVE	4OUa9K8zXNE

Step 3: Restart the container

docker compose up -d --force-recreate

The backend automatically picks up /app/cookies.txt (via COOKIE_FILE). After exporting fresh cookies, restart the container again — cookies expire, so re-export them whenever you see 429 / bot-detection errors.

Verify it works

docker exec kv-music yt-dlp --js-runtimes node --cookies /app/cookies.txt "ytsearch1:test audio" --dump-json --flat-playlist | head -c 200

If you see HTTP Error 429 / Sign in to confirm you're not a bot, make sure the container has IPv6 (dual-stack kvnet network from docker-compose) — see Automatic Cookie Refresh.


Troubleshooting on a NAS

Symptom: "cannot play music", stream endpoint returns HTTP 500

Open the 500 response body (or docker logs kv-music) to see yt-dlp's real error:

1. The provided YouTube account cookies are no longer valid

Your mounted cookies.txt export expired (browser sessions rotate). The backend now detects this, discards the rejected file and auto-refreshes — but the strongest fix is re-exporting a fresh logged-in cookies.txt (see YouTube Cookies).

2. Sign in to confirm you're not a bot (no cookie warning)

Your NAS IP itself is bot-flagged and anonymous cookies can't override that. Check which network path the container actually has:

# Inside the container - is IPv6 routed?
docker exec kv-music sh -c "curl -6 -m 8 -s -o /dev/null -w IPv6:%{http_code} https://www.youtube.com/ || echo IPv6-FAILED"
# IPv4?
docker exec kv-music sh -c "curl -4 -m 8 -s -o /dev/null -w IPv4:%{http_code} https://www.youtube.com/ || echo IPv4-FAILED"
  • IPv6:200 → the container should already stream over IPv6 (backend probes and adds --force-ipv6). If streams still fail, the probe result may be stale — restart the container.
  • IPv6:000 + IPv4:200 → the kvnet dual-stack network has IPv6 assigned but not routed (very common on Synology Docker). The container falls back to IPv4, which YouTube blocks. Fixes, in order of preference:
    1. Enable routed IPv6: enable IPv6 on your router (ISP must provide it) and on the Synology (Control Panel → Network → Network Interface → IPv6, e.g. DHCPv6), then restart the container until IPv6:200.
    2. Export a fresh logged-in cookies.txt from your browser — a logged-in session defeats IP-level bot checks even over a flagged IPv4 route (this is the reliable fix if IPv6 is unavailable).

Other playback issues

  • NotSupportedError: no supported source was found (browser console): the app serves WebM/Opus by default (open codec — plays in Chrome/Firefox/Edge and codec-restricted clients like VS Code's webview) and requests m4a/AAC (?fmt=m4a) only when the browser can't play WebM/Opus (e.g. Safari). If you still hit this, hard-refresh (Ctrl+Shift+R) — an old tab keeps a stuck error state from before the format fix.
  • Songs previously played still fail: the server caches downloads; after a backend upgrade, clear the old cache volume (docker compose down && rm -rf cache) so stale files are re-downloaded in the new format.
  • Search shows "No results found": hard-refresh the page (Ctrl+Shift+R) — an old tab runs pre-fix frontend code.

Tech Stack

Layer Technology Purpose
Frontend React 18, TypeScript, Zustand UI framework & state management
Build Vite 5, PWA Fast bundling & offline support
Styling TailwindCSS + animate Utility-first CSS with animations
Backend Rust, Axum High-performance HTTP server
Streaming yt-dlp + Node.js 22 YouTube audio extraction
Lyrics LRCLIB, SimpMusic, lyrics.ovh Free synced lyrics APIs
Auth argon2 + bearer tokens Password hashing & sessions
Storage JSON file (/app/data/users.json) Server-side user accounts + auto-refreshed cookies (NAS volume)
Container Docker, Debian Deployment packaging

Local Development

Prerequisites

Tool Version Purpose
Node.js 22+ Frontend build & yt-dlp JS runtime
Rust 1.88+ Backend compilation
Python 3.11+ yt-dlp dependency
ffmpeg Any Audio processing
yt-dlp Latest YouTube audio extraction

Backend (Rust)

cd backend-rust
cargo run --release

Server starts at http://localhost:8080. User accounts persist to backend-rust/data/users.json locally (or /app/data/users.json in Docker).

Frontend (React)

cd frontend-vite
npm install
npm run dev

Dev server at http://localhost:5173 with API proxy to localhost:8080.

Project Structure

kv-music/
├── frontend-vite/              # React frontend
│   ├── src/
│   │   ├── components/         # UI components
│   │   │   ├── player/         # MiniPlayer, FullPlayer, MobileFullPlayer, ProgressBar
│   │   │   ├── layout/         # AppLayout, NowPlayingBar, Header
│   │   │   ├── BottomSheet.tsx # Reusable bottom sheet with drag-to-dismiss
│   │   │   ├── LoginModal.tsx  # Login / Register / Pair code UI
│   │   │   ├── CoverImage.tsx  # Image with fallback
│   │   │   ├── Lyrics.tsx      # Synced lyrics display
│   │   │   └── ...
│   │   ├── stores/             # Zustand state management
│   │   │   ├── playerStore.ts  # Playback, queue, liked tracks
│   │   │   ├── libraryStore.ts # Library, playlists, artists, albums
│   │   │   ├── authStore.ts    # Real account API calls (register/login/pair)
│   │   │   └── uiStore.ts      # UI state (sidebar, panels)
│   │   ├── pages/              # Route pages
│   │   │   ├── Search.tsx      # Universal search (songs/albums/playlists/artists)
│   │   │   ├── ChartsSection.tsx # Real YT Music charts (BXH)
│   │   │   ├── ArtistsPage.tsx # Top artists ranking with real avatars
│   │   │   └── ...
│   │   ├── hooks/              # Custom React hooks
│   │   ├── services/           # API calls (library.ts)
│   │   └── types/              # TypeScript interfaces
│   ├── public/                 # Static assets and PWA manifest
│   └── tailwind.config.js      # TailwindCSS configuration
├── backend-rust/               # Rust backend
│   └── src/
│       ├── main.rs             # Entry point, routes, server setup
│       ├── api.rs              # HTTP route handlers
│       ├── auth.rs             # Accounts: register/login/pair (argon2)
│       ├── spotdl.rs           # yt-dlp integration, search, charts, streams
│       └── models.rs           # Data models and serialization
├── Dockerfile                  # Multi-stage Docker build
├── docker-compose.yml          # Docker Compose config
└── .dockerignore               # Build context exclusions

API Endpoints

Music

Method Path Description
GET /api/search?q={query} Search songs (yt-dlp)
GET /api/universal-search?q={query} Search songs + albums + playlists + artists (YT Music API)
GET /api/collection?id={id} Get tracks of an album (MPRE...) or playlist (VLPL...)
GET /api/stream/{video_id} Stream audio (with yt-dlp)
GET /api/charts?chart_type={type} Real YT Music charts (vn/us/kr/cn, top-hits, trending, top-albums, hits-collection)
GET /api/new-releases?region={vn|us} Real latest releases
GET /api/artists?region={vn|us|kr|cn} Top artists with real avatars
GET /api/artist/info?q={artist} Artist photo
GET /api/browse?country={VN} Browse categories per country
GET /api/recommendations?seed={seed} Smart recommendations
GET /api/lyrics?track={t}&artist={a}&video_id={id} Fetch synced lyrics (SimpMusic, LRCLIB, lyrics.ovh, ZingMP3)
GET /api/video-stats?id={id} Get video stats (views, likes)

Auth

Method Path Description
POST /api/auth/register Register {name, email, password, avatar_color} (min 6 chars, unique email)
POST /api/auth/login Login {email, password} → {user, token}
POST /api/auth/logout Logout {token}
POST /api/auth/me Get current user from {token}
POST /api/auth/pair/generate Generate a new pair code for the logged-in account
POST /api/auth/pair/link Link device via {code} → full account takeover

System

Method Path Description
POST /api/settings/update-ytdlp Run yt-dlp -U self-update, returns output
POST /api/settings/fetch-cookies Automatically fetch fresh YouTube session cookies (writes Netscape cookie file, clears all caches so every fetch re-uses the new cookies)

Browser Support

Browser Status
Chrome 90+ Supported
Firefox 90+ Supported
Safari 15+ Supported
Edge 90+ Supported
Mobile Chrome Supported
Mobile Safari Supported

Changelog

See CHANGELOG.md for release history.


Support

If you find this project helpful, consider buying me a coffee! Your support helps keep this project alive and maintained.

Donate via MoMo / VietQR



🌟 Star History

Star History Chart

License

MIT License - Free to use, modify, and distribute.