一個基於 Web 的 YouTube Music 點歌系統,用戶可以透過瀏覽器搜尋、點歌和控制播放,音訊則透過連接的音箱輸出。
- 🔍 搜尋歌曲:透過歌曲名稱、歌手或 YouTube 連結搜尋
- 🎵 點歌系統:加入歌曲到播放清單
- 🎮 播放控制:播放/暫停、下一首、音量調整
- 📋 播放清單:查看和管理排隊中的歌曲
- 📝 同步歌詞:即時顯示歌詞(支援 LRC 格式)
- 🔄 即時同步:透過 WebSocket 即時更新所有客戶端的狀態
- 🔉 音量平衡:依 YouTube loudness metadata 自動衰減偏 loud 的歌曲,不會提高主音量或額外放大較安靜的曲目
- 💾 持久化同步 Session:容器重啟後保留配對關係,不需要重新配對
- 🏷️ 版本可見性:前端 Header 與後端 API 會顯示目前系統版本與 Git SHA
目前播放鏈路採用「多層 fallback」設計,目標是在 YouTube 對不同 IP、不同 client profile 行為不一致時,仍盡量保持可播放。
- 前端透過
POST /api/queue、POST /api/mix或 radio 補歌,把Track送進QueueService QueueService.playNext()取出下一首歌,先呼叫MusicService.getStreamUrl(videoId)MusicService.getStreamUrl()先嘗試youtubei.js- 如果
youtubei.js拿不到有效 audio URL,會 fallback 到yt-dlp -g - 後端拿到最終的直連音訊 URL 後,呼叫
PlayerService.playUrl() PlayerService啟動mpv --no-video播放該 URL,並透過 mpv IPC 監聽進度、暫停、EOF- 如果直連 URL 播放失敗,才退回
PlayerService.play(videoId),讓 mpv 自己處理 YouTube URL
youtubei.js理論上最快,因為不需要額外啟動 CLIyt-dlp -g在實務上更抗 YouTube 的 bot 判定,尤其在樹莓派或家用網路 IP 上mpv直開 YouTube URL 仍保留作最後保底,避免單一路徑失效時完全不能播
- music.service.ts 負責搜尋、歌詞、mix、串流 URL 提取
- queue.service.ts 負責 queue、mix、radio、播放策略與 fallback 決策
- player.service.ts 負責 mpv 行程、IPC 狀態同步、pause/resume/seek/stop
- ytdlp.ts
負責
yt-dlpextractor args 與 cookies 設定
可以透過環境變數調整 yt-dlp 行為:
YTDLP_EXTRACTOR_ARGS="youtube:player_client=android_vr"
YTDLP_COOKIES_FILE="/app/secrets/youtube-cookies.txt"YTDLP_EXTRACTOR_ARGS用來指定 YouTube extractor profile,預設為youtube:player_client=android_vrYTDLP_COOKIES_FILE當 YouTube 對目前 IP 要求人類驗證時,可掛入已登入帳號匯出的 cookies 檔
可將本服務作為 Folia 的外部播放源:啟用後會在獨立 port 提供 Widdit now-playing-service 相容的 WebSocket(/api/ws/lyric,推送 Track / Lyric / PlayerPauseState / PlayerProgress / PlayerProgressReplay 事件)與進度查詢端點(GET /api/query/progress),Folia 的 Stage 模式即可同步顯示目前播放的曲目、封面與 LRC 歌詞。
FOLIA_BRIDGE=true # 啟用 bridge(預設關閉)
FOLIA_PORT=9863 # Folia 端 hardcode 9863,通常不需更改
FOLIA_HOST=127.0.0.1 # 協定無認證,預設只綁 loopback;跨機需明確設定實作位於 folia-bridge.ts,與既有 /ws 頻道完全獨立。
┌─────────────────┐ ┌─────────────────┐
│ 手機/電腦 │ WebSocket │ 後端 Server │
│ 瀏覽器 │ ◄────────────────► │ (Bun/Hono) │
├─────────────────┤ ├─────────────────┤
│ - 搜尋歌曲 │ │ - 管理播放清單 │
│ - 點歌 │ │ - youtubei.js │
│ - 看播放清單 │ │ - mpv 播放 │
│ - 播放控制 │ │ │
│ - 顯示歌詞 │ │ │
└─────────────────┘ └────────┬────────┘
│
│ mpv (--no-video)
▼
┌─────────────────┐
│ 音箱 / 喇叭 │
└─────────────────┘
- Runtime: Bun
- Backend: Hono (Web 框架)
- 播放器: mpv (音訊播放)
- YouTube API: youtubei.js
- 歌詞: LRCLIB API
- 即時通訊: WebSocket
- React 19 - UI 框架
- TypeScript - 類型安全
- Vite - 構建工具
- Tailwind CSS v4 - 樣式框架
- Zustand - 狀態管理
- COSS UI - 設計系統
# macOS/Linux
curl -fsSL https://bun.sh/install | bash
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"# macOS
brew install mpv
# Ubuntu/Debian
sudo apt install mpv
# Windows
# 從 https://mpv.io 下載並安裝# 後端依賴
bun install
# 前端依賴
cd frontend && npm install && cd ..方式一:分別啟動(推薦)
# 終端 1:啟動後端
bun run dev
# 終端 2:啟動前端
npm run dev:frontend前端會在 http://localhost:5174 啟動,並自動代理 API 到後端 http://localhost:3000。
方式二:僅啟動後端(使用舊版 HTML5 前端)
bun run dev
# 訪問 http://localhost:3000 使用基礎 HTML5 版本# 1. 構建前端和後端
npm run build:all
# 2. 啟動生產服務器
npm run start生產模式下訪問 http://localhost:3000 即可使用完整功能的 React 前端。
docker compose up -d 一次啟動兩個服務:點歌機本體(含 Folia bridge)與網頁版 Folia 歌詞視覺化。其他人只要用瀏覽器開啟主機的 8080 port,就能遠端檢視同步歌詞,不需要在自己的電腦安裝任何東西。
# Linux(含樹莓派):
docker compose up -d
# macOS(Docker Desktop):先啟動主機端 PulseAudio(見下方「macOS 音訊設定」),再:
docker compose -f docker-compose.yml -f docker-compose.macos.yml up -d| 服務 | URL | 說明 |
|---|---|---|
| 點歌機 WebUI | http://<host>:3000 |
搜尋、點歌、佇列管理 |
| Folia 歌詞頁 | http://<host>:8080 |
瀏覽器直接開啟,免安裝,自動連上歌詞串流 |
| Folia bridge 直連 | ws://<host>:9863/api/ws/lyric |
給區網內 Folia Electron 版等原生客戶端直連 |
歌詞頁的運作方式:folia-web 容器以 nginx 服務打過補丁的 Folia web 版(WS 連線網址改為依頁面來源推導),並將 /api/ws/lyric 與 /api/query/progress 反向代理到 bot 容器的 9863 port,因此任何能開啟 8080 的裝置都能即時看到曲目、封面與逐行歌詞。
- 8080 與 9863 均無認證:任何能連到主機的人都能看到正在播放的曲目與歌詞。請只在信任的區網使用,勿直接暴露公網;需要對外時建議加防火牆規則或在前面架帶 Basic Auth 的反向代理。
- WebUI 內的 Folia 開關(
POST /api/folia/disable)關閉後,8080 歌詞頁會斷線並每 2 秒重試,重新啟用(POST /api/folia/enable)或重啟容器即自動恢復。compose 已預設FOLIA_BRIDGE=true,容器重啟後 bridge 一定會回到開啟狀態。
Docker Desktop 的 Linux VM 沒有 /dev/snd,改用 PulseAudio over TCP 把聲音送回 macOS 主機(docker-compose.macos.yml 已設好 PULSE_SERVER,並需要 Docker Compose ≥ v2.24 支援 !reset):
brew install pulseaudio
pulseaudio --daemon --exit-idle-time=-1 \
--load="module-native-protocol-tcp auth-ip-acl=127.0.0.1;172.16.0.0/12;192.168.65.0/24"- 預設拉取 CI 預建的多架構映像
bs10081/folia-web:latest(amd64/arm64)。 - 想從原始碼現場建置(不依賴 Docker Hub):
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build。 - Folia 版本固定於 deploy/folia/FOLIA_COMMIT。升級流程:更新該檔的 commit SHA 與 deploy/folia/Dockerfile 的
ARG FOLIA_COMMIT預設值 → 若補丁套不上則重新產生 deploy/folia/patches/ → push 觸發folia-image.ymlCI 重建。 - Folia 原始專案為 chthollyphile/folia-major(AGPL-3.0)。本 repo 對其的修改以補丁形式公開於
deploy/folia/patches/(端點改為同源推導、Now Playing 預設開啟),映像內保留原始 LICENSE。
本專案支援透過 Docker 部署到樹莓派,提供簡單的容器化部署方案。
目前已經有預先建好的映像可直接使用:
bs10081/youtube-music-bot:latest如果你只是想快速啟動,不需要自己先安裝 Bun、Node.js 或 buildx,直接 pull 這個 image 就可以。
- 樹莓派(推薦 64 位元系統,如 Raspberry Pi OS 64-bit)
- Docker 和 Docker Compose 已安裝
- 音頻設備正常運作
這是最快的方式,適合先確認服務有沒有正常跑起來。
docker run -d \
--name youtube-music-bot \
--restart unless-stopped \
-p 3000:3000 \
--device /dev/snd:/dev/snd \
-e NODE_ENV=production \
-e LOG_LEVEL=INFO \
bs10081/youtube-music-bot:latest啟動後直接開瀏覽器看:
http://<你的主機IP>:3000查看日誌:
docker logs -f youtube-music-bot停止與刪除:
docker stop youtube-music-bot
docker rm youtube-music-bot如果你比較習慣用 compose,先建立一份 docker-compose.yml:
services:
youtube-music-bot:
image: bs10081/youtube-music-bot:latest
container_name: youtube-music-bot
restart: unless-stopped
ports:
- "3000:3000"
devices:
- /dev/snd:/dev/snd
environment:
- NODE_ENV=production
- LOG_LEVEL=INFO
- SYNC_STATE_DB_PATH=/data/sync-state.sqlite
- YTDLP_EXTRACTOR_ARGS=youtube:player_client=android_vr
volumes:
- sync-state:/data
volumes:
sync-state:啟動:
docker compose up -d更新到最新版本:
docker compose pull
docker compose up -d/data/sync-state.sqlite 會保存同步 session 與裝置撤銷狀態,因此只要不要刪掉 volume,容器重啟或重新建立後都不需要重新配對。
如果之後遇到 YouTube anti-bot 變嚴,可以再加 cookies 掛載:
services:
youtube-music-bot:
image: bs10081/youtube-music-bot:latest
volumes:
- ./secrets:/app/secrets:ro
environment:
- YTDLP_EXTRACTOR_ARGS=youtube:player_client=android_vr
- YTDLP_COOKIES_FILE=/app/secrets/youtube-cookies.txt# 1. 複製專案到樹莓派
git clone <your-repo-url> youtube-music-bot
cd youtube-music-bot
# 2. 構建並啟動服務
docker compose up -d
# 3. 查看日誌
docker compose logs -f這個方式適合在 Mac 或 PC 上構建 ARM64 映像,然後傳輸到樹莓派。
步驟 1:在 Mac/PC 上構建 ARM64 映像
# 設置 buildx 構建器(首次需要)
docker buildx create --name arm-builder --use
# 構建 ARM64 映像並保存為 tar
docker buildx build --platform linux/arm64 \
-t youtube-music-bot:latest \
--output type=docker,dest=youtube-music-bot-arm64.tar .步驟 2:傳輸映像到樹莓派
# 使用 scp 傳輸(替換 <樹莓派IP>)
scp youtube-music-bot-arm64.tar pi@<樹莓派IP>:~/
# 或使用 rsync(支援斷點續傳)
rsync -avP youtube-music-bot-arm64.tar pi@<樹莓派IP>:~/
# 同時傳輸 docker-compose.yml
scp docker-compose.yml pi@<樹莓派IP>:~/youtube-music-bot/步驟 3:在樹莓派上載入並啟動
# SSH 進入樹莓派
ssh pi@<樹莓派IP>
# 載入 Docker 映像
docker load -i youtube-music-bot-arm64.tar
# 啟動服務
cd ~/youtube-music-bot
docker compose up -d
# 查看日誌
docker compose logs -f這是目前實際使用的部署方式,適合日常更新生產環境。
如果你是第一次從舊版升級到 0.2.0 之後的版本,舊瀏覽器本地資料裡還沒有 deviceToken,現有同步裝置可能需要重新配對一次;完成一次後,後續容器重啟就不應再要求重新配對。
步驟 1:本地驗證
bun run typecheck
bun test src/__tests__
npm run build:frontend
npm run build步驟 2:提交並推送
git add -A
git commit -m "fix: your-change-summary"
git push origin main步驟 3:建置 ARM64 映像並推送到 Docker Hub
如果你本機已經有設定好 buildx builder,可以直接用下面這段:
GIT_SHA=$(git rev-parse --short HEAD)
docker buildx build \
--builder multiplatform-builder \
--platform linux/arm64 \
-t bs10081/youtube-music-bot:$GIT_SHA \
-t bs10081/youtube-music-bot:latest \
--push .如果你還沒有 builder,先建立一次:
docker buildx create --name multiplatform-builder --use
docker buildx inspect --bootstrap步驟 4:在樹莓派更新 compose 使用的 image tag
假設 SSH alias 是 moli-music,而部署目錄是 ~/Host:
GIT_SHA=$(git rev-parse --short HEAD)
ssh moli-music '
cp ~/Host/docker-compose.yml ~/Host/docker-compose.yml.bak-$(date +%Y%m%d-%H%M%S) &&
sed -i "s|image: bs10081/youtube-music-bot:.*|image: bs10081/youtube-music-bot:'"$GIT_SHA"'|" ~/Host/docker-compose.yml &&
cd ~/Host &&
docker compose pull &&
docker compose up -d &&
docker compose ps
'步驟 5:檢查部署後日誌
ssh moli-music '
cd ~/Host &&
docker compose logs --tail=120 youtube-music-bot
'如果要直接驗證點歌 API:
ssh moli-music 'python3 - <<'"'"'PY'"'"'
import json
import urllib.request
payload = {
"track": {
"videoId": "D2HoBIh3zJ4",
"title": "抽纸",
"artist": "衛蘭",
"duration": 229,
"thumbnail": "https://img.youtube.com/vi/D2HoBIh3zJ4/mqdefault.jpg",
}
}
req = urllib.request.Request(
"http://localhost:3000/api/queue",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=15) as response:
print(response.read().decode())
PY'- 訪問
http://<樹莓派IP>:3000 - 搜尋並加入歌曲到播放清單
- 確認音頻從樹莓派連接的音箱輸出
# 啟動服務
docker compose up -d
# 停止服務
docker compose down
# 重新啟動服務
docker compose restart
# 查看日誌
docker compose logs -f
# 更新映像並重啟
docker compose pull
docker compose up -dALSA(預設):
Docker Compose 配置已掛載 /dev/snd,支援直接使用 ALSA 音頻設備。
PulseAudio(進階):
如果樹莓派使用 PulseAudio,請編輯 docker-compose.yml,取消註解 PulseAudio 相關配置:
volumes:
- /run/user/1000/pulse:/run/user/1000/pulse
environment:
- PULSE_SERVER=unix:/run/user/1000/pulse/native音頻無輸出?
- 確認音頻設備已正確掛載:
ls -l /dev/snd - 檢查容器是否有音頻設備訪問權限
- 測試 mpv 是否正常:
docker exec youtube-music-bot mpv --version
無法連接服務?
- 確認服務正在運行:
docker compose ps - 檢查防火牆設定:
sudo ufw status - 查看詳細日誌:
docker compose logs -f
這通常表示:
- 目前出口 IP 被 YouTube 風控
mpv內建的 YouTube 抽流路徑被擋- 需要 cookies 或不同 extractor profile
先做這個檢查:
docker compose exec -T youtube-music-bot \
yt-dlp --no-warnings --no-playlist -g -f bestaudio/best \
--extractor-args "youtube:player_client=android_vr" \
"https://www.youtube.com/watch?v=D2HoBIh3zJ4"如果這裡能拿到 googlevideo.com 的 URL,代表 yt-dlp fallback 仍然可用,問題通常不在最底層連線。
如果這裡也失敗:
- 嘗試掛
YTDLP_COOKIES_FILE - 嘗試更新
yt-dlp - 嘗試更換
YTDLP_EXTRACTOR_ARGS - 檢查目前網路 IP 是否被更嚴格限制
這通常是 youtubei.js 這條路拿不到可用 audio format。
這不一定是致命錯誤,因為系統會自動 fallback 到 yt-dlp -g。真正要看的不是這一行本身,而是後面有沒有:
Primary stream extraction failed, trying yt-dlp CLI fallbackStream URL obtained via yt-dlp CLIPlayback started successfully via direct stream URL
這通常表示:
mpv收到的來源 URL 無法播放mpv直開 YouTube URL 時被 bot 驗證擋下- URL 過期或格式不支援
建議檢查順序:
- 看前面是
playUrl()還是play(videoId) - 如果是
play(videoId),表示已經走到最後 fallback,通常代表前面的直連 URL 路徑也失敗了 - 如果是
playUrl(),把同一條 URL 拿去容器內直接測:
docker compose exec -T youtube-music-bot \
mpv --no-video "<DIRECT_STREAM_URL>"這表示:
- mpv 行程已經起來
- IPC 已建立
- 但不代表音訊一定真的成功播放到尾
要繼續看後面的 log 是否出現:
Property change {"name":"duration"...}Property change {"name":"pause","data":false}mpv process exited {"code":2...}
如果很快就 exit,通常還是來源 URL、音訊設備或 bot 驗證問題。
表示容器或主機內沒有 yt-dlp。
檢查:
docker compose exec -T youtube-music-bot which yt-dlp
docker compose exec -T youtube-music-bot yt-dlp --version本專案的 Dockerfile 已經在 runtime image 中安裝 yt-dlp。
這通常不是後端播放 bug,而是手動測 API 時 shell quoting 壞掉。
如果要在 SSH 內測 API,建議直接用 Python 送 JSON,而不是在 shell 內手刻巢狀引號。
當「歌曲不會播放」時,建議按這個順序排查:
- 看服務是否正常啟動
docker compose ps
docker compose logs --tail=120 youtube-music-bot- 看
yt-dlp是否能拿到直連 URL
docker compose exec -T youtube-music-bot \
yt-dlp --no-warnings --no-playlist -g -f bestaudio/best \
--extractor-args "youtube:player_client=android_vr" \
"https://www.youtube.com/watch?v=<VIDEO_ID>"- 看
mpv是否能播放直連 URL
docker compose exec -T youtube-music-bot mpv --no-video "<DIRECT_STREAM_URL>"- 看後端實際走的是哪條路
你要在 log 中找到這幾個關鍵訊號:
Fetching direct stream URL for playbackPrimary stream extraction failed, trying yt-dlp CLI fallbackStream URL obtained via yt-dlp CLIPlayback started successfully via direct stream URL
- 如果還是不穩,再加入 cookies
environment:
- YTDLP_EXTRACTOR_ARGS=youtube:player_client=android_vr
- YTDLP_COOKIES_FILE=/app/secrets/youtube-cookies.txt
volumes:
- ./secrets:/app/secrets:ro專案已提供 GitHub Actions workflow 來自動建置 Docker image:
push到main時:自動 build 並 push 到 Docker Hubpull_request時:只驗證 Dockerfile 能不能成功 build,不 pushworkflow_dispatch時:可手動觸發
Workflow 檔案位置:
到 GitHub repository 的 Settings -> Secrets and variables -> Actions,新增:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
建議 DOCKERHUB_TOKEN 使用 Docker Hub 的 access token,不要直接用帳號密碼。
在 main 分支 push 時,workflow 會推這些標籤:
latestmainsha-<commit>
這樣正式環境可以固定拉 latest,也可以針對某次部署鎖定特定 SHA tag。
- 開啟瀏覽器訪問
http://localhost:3000 - 使用搜尋功能找到想聽的歌曲
- 點擊搜尋結果加入播放清單
- 系統會自動開始播放,音訊從連接的音箱輸出
- 可以使用多台裝置同時控制播放
搜尋歌曲。
回應範例:
{
"success": true,
"data": [
{
"videoId": "dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"artist": "Rick Astley",
"duration": 212,
"thumbnail": "https://..."
}
]
}加入歌曲到播放清單。
請求範例:
{
"track": {
"videoId": "dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"artist": "Rick Astley",
"duration": 212,
"thumbnail": "https://..."
}
}取得播放清單。
從播放清單移除歌曲。
取得目前播放狀態。
取得系統版本資訊,會回傳:
{
"success": true,
"data": {
"appVersion": "0.7.10",
"gitSha": "abc1234",
"buildVersion": "0.7.10+abc1234",
"environment": "production"
}
}取得目前歌曲的歌詞。
連接: ws://localhost:3000/ws
// 播放狀態更新
{
"type": "playback_state",
"state": {
"isPlaying": true,
"currentTrack": { ... },
"position": 45.2,
"duration": 212,
"volume": 70,
"queue": [ ... ]
}
}
// 播放清單更新
{
"type": "queue_updated",
"queue": [ ... ]
}
// 歌詞
{
"type": "lyrics",
"lyrics": [
{ "time": 0, "text": "..." },
...
]
}// 播放/暫停
{ "type": "play" }
{ "type": "pause" }
// 下一首
{ "type": "skip" }
// 音量
{ "type": "volume", "value": 80 }youtube-music-bot/
├── package.json
├── tsconfig.json
├── README.md
├── src/ # 後端程式碼
│ ├── index.ts # 入口點
│ ├── server.ts # Hono server + WebSocket
│ ├── routes/
│ │ └── api.ts # REST API 路由
│ ├── services/
│ │ ├── music.service.ts # YouTube Music 服務
│ │ ├── player.service.ts # mpv 播放器控制
│ │ └── queue.service.ts # 播放清單佇列
│ ├── websocket/
│ │ └── handler.ts # WebSocket 事件處理
│ └── types/
│ └── index.ts # 類型定義
├── frontend/ # React 前端
│ ├── src/
│ │ ├── components/ # React 組件
│ │ ├── hooks/ # 自定義 Hooks
│ │ ├── stores/ # Zustand 狀態管理
│ │ ├── services/ # API 服務層
│ │ └── types/ # 前端類型定義
│ └── dist/ # 構建產物(生產模式)
└── public/ # 舊版 HTML5 前端(保留)
├── index.html
├── style.css
└── app.js
確保 mpv 已安裝並在 PATH 中:
which mpv
mpv --version如果 mpv 在自訂路徑,可設定環境變數:
export MPV_PATH=/path/to/mpv- 檢查網路連線
- 確認 mpv 正常運作:
mpv https://www.youtube.com/watch?v=dQw4w9WgXcQ - 查看伺服器日誌是否有錯誤訊息
- 確認伺服器正在運行
- 檢查防火牆設定
- 如果使用代理,確保 WebSocket 連接未被阻擋
MIT License
問題描述:
YouTube API 返回的 streaming_data 中,url、signature_cipher、cipher 屬性皆為 undefined,導致無法直接獲取串流 URL。
影響:
- 目前多數情況會自動 fallback 到
yt-dlp -g再交給mpv播放 - 理想情況下直接使用 youtubei.js 提取的 URL 可減少延遲至約 0.5 秒
相關 Issue:
- LuanRT/YouTube.js#1123 - "Video unavailable for SABR, leading to no valid URL to decipher"
狀態:等待 YouTube.js 更新修復
Workaround:目前系統會自動 fallback 到 yt-dlp -g 取得直連音訊 URL;若還是失敗,最後才退回 mpv 直開 YouTube URL。
基於 youtube-music-cli 專案開發