一個完整的 Spotify 音樂聆聽分析工具,支援數據同步和歷史記錄追蹤。
- 📊 音樂數據分析:專輯、單曲、藝人、曲風、時段分析
- 📸 數據快照:一鍵生成分享圖片
- 🎯 時間篩選:7天/30天/90天/180天/365天 多維度分析
- 💾 歷史記錄:自動同步播放記錄到資料庫
- 🌓 純黑設計:OLED 友好的純黑背景
- 📱 響應式:完美支援桌面和移動設備
本專案採用 Next.js 14 (App Router) 架構,結合 PostgreSQL 資料庫來儲存歷史播放記錄。以下是系統如何運作的詳細說明。
┌─────────────────────────────────────────────────────────────┐
│ 前端層 (Client) │
├─────────────────────────────────────────────────────────────┤
│ React Components (Analytics, Albums, etc.) │
│ ├── Zustand Store (認證狀態、播放器狀態) │
│ ├── TanStack Query (數據獲取與快取) │
│ └── Spotify Web API Client (前端 API 封裝) │
└─────────────────────────────────────────────────────────────┘
↕ HTTP/API
┌─────────────────────────────────────────────────────────────┐
│ Next.js API Routes │
├─────────────────────────────────────────────────────────────┤
│ /api/auth/spotify - 用戶認證與 token 儲存 │
│ /api/sync - 手動觸發數據同步 │
│ /api/cron/sync - 定時任務觸發同步 │
│ /api/analytics/* - 分析數據 API │
│ /api/users/me - 獲取當前用戶資訊 │
└─────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────┐
│ 服務層 (Server) │
├─────────────────────────────────────────────────────────────┤
│ Sync Service - 從 Spotify API 同步播放記錄 │
│ Data Service - 合併即時與歷史數據 │
│ Database Models - 資料庫操作封裝 │
└─────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL 資料庫 │
├─────────────────────────────────────────────────────────────┤
│ users - 用戶資訊與 OAuth tokens │
│ play_history - 歷史播放記錄 │
│ daily_stats - 每日統計摘要 │
└─────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────┐
│ Spotify Web API │
└─────────────────────────────────────────────────────────────┘
用戶點擊登入
↓
生成 PKCE code_verifier 和 code_challenge
↓
重定向到 Spotify 授權頁面
↓
用戶授權後,Spotify 重定向回 /callback
↓
用 code 和 code_verifier 交換 access_token 和 refresh_token
↓
將 tokens 儲存到:
- localStorage (前端,用於即時 API 調用)
- PostgreSQL (後端,用於定時同步)
↓
認證完成,進入應用
關鍵檔案:
app/lib/spotify-web-api.ts- 前端 Spotify API 封裝,處理 PKCE 流程app/components/SpotifyCallback.tsx- 處理 OAuth callbackapp/api/auth/spotify/route.ts- 後端 API,儲存用戶 tokens 到資料庫
系統採用雙重數據來源策略:
- 來源:Spotify Web API (
/v1/me/player/recently-played) - 獲取時機:用戶瀏覽分析頁面時即時調用
- 限制:Spotify API 最多返回約 50 筆最近播放記錄
- 儲存位置:不儲存,僅用於即時顯示
- 來源:PostgreSQL 資料庫 (
play_history表) - 獲取時機:透過定時任務或手動同步
- 同步機制:
- 首次登入:自動觸發同步
- 定時任務:每天 UTC 00:00 自動同步所有用戶
- 手動同步:用戶可在設定頁面手動觸發
同步流程:
觸發同步 (/api/sync 或 cron job)
↓
從資料庫獲取用戶的 refresh_token
↓
使用 refresh_token 刷新 access_token
↓
調用 Spotify API 獲取最近播放記錄
↓
過濾已同步的記錄(基於 last_sync_at)
↓
將新記錄插入 play_history 表(自動去重)
↓
更新用戶的 last_sync_at
關鍵檔案:
app/server/services/sync-service.ts- 同步服務核心邏輯app/api/sync/route.ts- 手動同步 APIapp/api/cron/sync/route.ts- 定時任務 APIapp/server/models/PlayHistory.ts- 播放記錄資料模型
當用戶查看分析頁面時,系統會:
1. 獲取即時數據
└─> spotifyWebAPI.getRecentlyPlayedMultiple()
(從 Spotify API 獲取最近播放記錄)
2. 獲取歷史數據
└─> backendAPI.getHistoricalTracks()
(從 PostgreSQL 獲取指定時間範圍的播放記錄)
3. 數據合併 (DataMerger)
└─> 合併兩組數據,基於 (trackId, playedAt) 去重
└─> 過濾指定時間範圍內的記錄
4. 數據聚合
├─> aggregateByTrack() - 按單曲聚合
├─> aggregateByAlbum() - 按專輯聚合
├─> aggregateByArtist() - 按藝人聚合
└─> 時段分析 - 按播放時段分組
5. 返回分析結果
└─> 顯示在 Analytics 組件中
關鍵檔案:
app/lib/data-service.ts- 數據服務,包含 DataMerger 類別app/lib/backend-api.ts- 後端 API 客戶端app/components/Analytics.tsx- 分析頁面主組件
app/
├── api/ # Next.js API Routes
│ ├── auth/
│ │ └── spotify/route.ts # 處理 OAuth tokens 儲存
│ ├── sync/route.ts # 手動觸發數據同步
│ ├── cron/sync/route.ts # 定時任務觸發同步
│ ├── analytics/
│ │ ├── tracks/route.ts # 單曲分析 API
│ │ └── artists/route.ts # 藝人分析 API
│ └── users/me/route.ts # 獲取當前用戶資訊
│
├── components/ # React 組件
│ ├── Analytics.tsx # 分析頁面主組件
│ ├── analytics/ # 分析相關子組件
│ │ ├── TracksAnalysis.tsx # 單曲分析
│ │ ├── ArtistsAnalysis.tsx # 藝人分析
│ │ ├── AlbumsAnalysis.tsx # 專輯分析
│ │ ├── GenresAnalysis.tsx # 曲風分析
│ │ └── TimeSegmentAnalysis.tsx # 時段分析
│ ├── Login.tsx # 登入頁面
│ ├── SpotifyCallback.tsx # OAuth callback 處理
│ └── ...
│
├── lib/ # 核心工具庫
│ ├── spotify-web-api.ts # Spotify API 前端封裝
│ ├── data-service.ts # 數據服務(合併、聚合邏輯)
│ ├── backend-api.ts # 後端 API 客戶端
│ ├── cache-manager.ts # 數據快取管理
│ └── config.ts # 配置檔案
│
├── server/ # 後端服務層
│ ├── models/ # 資料庫模型
│ │ ├── User.ts # 用戶模型
│ │ ├── PlayHistory.ts # 播放記錄模型
│ │ └── DailyStats.ts # 每日統計模型
│ ├── services/
│ │ └── sync-service.ts # 同步服務核心邏輯
│ ├── config/
│ │ └── database.ts # 資料庫連接配置
│ └── migrations/ # 資料庫遷移腳本
│
├── store/ # Zustand 狀態管理
│ ├── useAuthStore.ts # 認證狀態
│ └── usePlayerStore.ts # 播放器狀態
│
├── hooks/ # React Hooks
│ ├── useAuth.ts # 認證相關 Hook
│ └── useSpotifyPlayer.ts # Spotify 播放器 Hook
│
└── types/ # TypeScript 類型定義
├── index.ts # 通用類型
└── spotify.ts # Spotify API 類型
儲存用戶資訊和 OAuth tokens。
CREATE TABLE users (
spotify_user_id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255),
display_name VARCHAR(255),
access_token TEXT, -- Spotify access token
refresh_token TEXT, -- Spotify refresh token
token_expires_at TIMESTAMP, -- Token 過期時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_sync_at TIMESTAMP -- 最後同步時間
);用途:
- 儲存用戶的 Spotify OAuth tokens
- 追蹤最後同步時間,避免重複同步
- 支援 token 自動刷新
儲存每筆播放記錄。
CREATE TABLE play_history (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
track_id VARCHAR(255) NOT NULL,
track_name VARCHAR(500) NOT NULL,
artist_id VARCHAR(255),
artist_name VARCHAR(500),
album_id VARCHAR(255),
album_name VARCHAR(500),
played_at TIMESTAMP NOT NULL, -- 播放時間
duration_ms INTEGER, -- 歌曲長度(毫秒)
popularity INTEGER, -- 歌曲熱門度
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, track_id, played_at) -- 防止重複記錄
);用途:
- 儲存所有歷史播放記錄
- 支援長時間範圍的分析(超過 Spotify API 限制)
- 透過
UNIQUE約束自動去重
儲存每日統計摘要(用於快速查詢)。
CREATE TABLE daily_stats (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
stat_date DATE NOT NULL,
total_plays INTEGER DEFAULT 0,
total_minutes DECIMAL(10,2) DEFAULT 0,
unique_tracks INTEGER DEFAULT 0,
unique_artists INTEGER DEFAULT 0,
unique_albums INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, stat_date)
);用途:
- 預先計算每日統計,提升查詢效能
- 支援快速獲取總體統計數據
職責:
- 處理 OAuth 2.0 PKCE 流程
- 管理 access_token 和 refresh_token
- 封裝 Spotify API 調用(最近播放、Top Tracks/Artists 等)
- 自動處理 token 刷新
主要方法:
class SpotifyWebAPI {
// 認證相關
authorize(): void // 發起 OAuth 授權
handleCallback(code: string): Promise<void> // 處理 callback
refreshAccessToken(): Promise<void> // 刷新 token
// 數據獲取
getRecentlyPlayedMultiple(limit: number): Promise<...>
getTopTracksMultiple(timeRange, limit): Promise<...>
getTopArtistsMultiple(timeRange, limit): Promise<...>
getCurrentUser(): Promise<SpotifyUser>
}Token 儲存策略:
- 前端:localStorage(用於即時 API 調用)
- 後端:PostgreSQL(用於定時同步任務)
職責:
- 合併即時數據和歷史數據
- 按不同維度聚合數據(單曲、專輯、藝人、時段)
- 處理時間範圍過濾
核心類別:
負責合併和去重:
class DataMerger {
mergePlayRecords(
realtimeRecords: SpotifyRecentlyPlayedTrack[],
historicalRecords: BackendTrack[],
window: string
): MergedPlayRecord[]
aggregateByTrack(records): AnalyticsTrackData[]
aggregateByAlbum(records): AlbumRow[]
aggregateByArtist(records): AnalyticsArtistData[]
}合併邏輯:
- 使用
(trackId, playedAt)作為唯一鍵去重 - 過濾指定時間範圍外的記錄
- 合併後按
playedAt降序排序
提供統一的數據獲取接口:
class DataService {
getAnalyticsData(window, analysisType): Promise<AnalyticsResponse>
getTopAlbums(window): Promise<AlbumRow[]>
getTimeSegmentAnalysis(window): Promise<...>
}職責:
- 從 Spotify API 獲取播放記錄
- 將新記錄插入資料庫
- 處理 token 刷新
- 避免重複同步
同步策略:
async function syncUserData(spotifyUserId: string) {
// 1. 獲取用戶 tokens(必要時刷新)
// 2. 查詢資料庫中最後同步時間
// 3. 從 Spotify API 獲取最近播放記錄
// 4. 過濾已同步的記錄(基於 played_at > last_sync_at)
// 5. 批量插入新記錄(自動去重)
// 6. 更新 last_sync_at
}去重機制:
- 資料庫層:
UNIQUE(user_id, track_id, played_at)約束 - 應用層:只同步
played_at > last_sync_at的記錄
useAuthStore (app/store/useAuthStore.ts)
- 管理認證狀態(
isAuthenticated,user) - 與
spotifyWebAPI同步狀態
usePlayerStore (app/store/usePlayerStore.ts)
- 管理 Spotify 播放器狀態
- 控制播放、暫停、切換歌曲
用於數據獲取和快取:
- 自動處理 loading/error 狀態
- 智能快取和重新驗證
- 支援樂觀更新
用戶打開 /analytics 頁面
↓
Analytics 組件載入
↓
useQuery 觸發數據獲取
↓
DataService.getAnalyticsData()
├─> spotifyWebAPI.getRecentlyPlayedMultiple()
│ └─> 調用 Spotify API (使用 localStorage 中的 token)
│
└─> backendAPI.getHistoricalTracks()
└─> 調用 /api/analytics/tracks
└─> 查詢 PostgreSQL play_history 表
↓
DataMerger.mergePlayRecords()
└─> 合併、去重、過濾時間範圍
↓
DataMerger.aggregateByTrack/Album/Artist()
└─> 按維度聚合數據
↓
返回分析結果
↓
React 組件渲染圖表和列表
定時任務觸發 /api/cron/sync
↓
驗證 CRON_SECRET
↓
syncAllUsers()
├─> 從資料庫獲取所有活躍用戶
└─> 對每個用戶執行 syncUserData()
├─> 從資料庫獲取 refresh_token
├─> 刷新 access_token(如需要)
├─> 調用 Spotify API 獲取最近播放
├─> 查詢資料庫 last_sync_at
├─> 過濾新記錄
├─> 批量插入 play_history
└─> 更新 last_sync_at
- Node.js 18+
- PostgreSQL 資料庫(或使用 Supabase、Neon 等託管服務)
- Spotify Developer Account
# 1. 複製專案
git clone https://github.com/Waynting/Spotify_Statistic.git
cd Spotify_Statistic
# 2. 安裝依賴
npm install
# 3. 設定環境變數
cp .env.example .env
# 編輯 .env 填入您的配置(見下方說明)
# 4. 執行資料庫遷移
npx tsx app/server/migrations/run-migrations.ts
# 5. 啟動開發環境
npm run dev訪問 http://localhost:3000 開始使用!
詳細的 Spotify 應用設定步驟請參考 docs/SPOTIFY_SETUP.md
- 前往 Spotify Dashboard
- 創建新應用
- 設定 Redirect URI:
- 開發環境:
http://localhost:3000/callback - 生產環境:
https://your-domain.com/callback
- 開發環境:
- 複製 Client ID 和 Client Secret
- 在
.env中設定環境變數
用戶白名單限制:
- 非企業 Spotify 帳號最多只能添加 25 個用戶 到白名單
- 個人使用或小範圍測試通常足夠
- 公開應用需要申請 Spotify 企業帳號
創建 .env 檔案並填入以下變數:
# Spotify OAuth
SPOTIFY_CLIENT_ID=你的_client_id
SPOTIFY_CLIENT_SECRET=你的_client_secret
NEXT_PUBLIC_SPOTIFY_REDIRECT_URI=http://localhost:3000/callback
# 資料庫連接(PostgreSQL)
DATABASE_URL=postgresql://user:password@host:port/database
# Cron Job 密鑰(用於自動同步)
CRON_SECRET=隨機生成的密鑰字串詳細說明請參考 .env.example
詳細部署步驟請參考 docs/DEPLOYMENT.md
- Fork 本專案到您的 GitHub
- 在 Vercel 導入專案
- 設定環境變數(見上方)
- 設定 PostgreSQL 資料庫(可使用 Vercel Postgres)
- 設定 Vercel Cron Jobs(用於定時同步)
- 部署!
- Netlify: 需要設定 Next.js 運行時
- Railway: 支援 PostgreSQL 和 Next.js
- 自託管: 需要 Node.js 環境和 PostgreSQL
# 開發模式
npm run dev
# 建置生產版本
npm run build
# 啟動生產伺服器
npm start
# 執行資料庫遷移
npx tsx app/server/migrations/run-migrations.ts
# 驗證資料庫表結構
npx tsx app/server/migrations/verify-tables.ts
# 測試同步功能
npx tsx app/server/migrations/test-sync.ts# 執行遷移
npx tsx app/server/migrations/run-migrations.ts
# 驗證表結構
npx tsx app/server/migrations/verify-tables.tsFrontend:
├── Next.js 14 (App Router)
├── React 18 + TypeScript
├── Tailwind CSS
├── TanStack Query (數據管理)
├── Zustand (狀態管理)
└── Recharts (圖表)
Backend:
├── Next.js API Routes
├── PostgreSQL
└── Spotify Web API
認證:
└── Spotify OAuth 2.0 with PKCE
系統會自動同步用戶的播放記錄:
- 用戶登入時:自動觸發首次同步
- Cron Job:每天 UTC 00:00 自動同步所有用戶
- 手動同步:用戶可在設定頁面手動觸發
在 vercel.json 中已配置:
{
"crons": [{
"path": "/api/cron/sync",
"schedule": "0 0 * * *"
}]
}資料庫用於儲存歷史播放記錄,讓您可以:
- 查看更長時間範圍的數據(超過 Spotify API 的 50 筆限制)
- 追蹤播放趨勢
- 提供更準確的分析
- 所有數據儲存在您的資料庫中
- OAuth token 加密儲存
- 支援用戶隨時撤銷授權
- 前端 tokens 儲存在 localStorage(受同源策略保護)
重要:Spotify API 對非企業用戶有白名單限制:
- 免費/個人帳號:最多只能添加 25 個用戶 到白名單
- 企業帳號:無限制
這意味著:
- 如果您的應用面向公眾,需要申請 Spotify 企業帳號
- 個人使用或小範圍測試,25 個用戶限制通常足夠
- 超過 25 個用戶後,新用戶將無法完成 OAuth 授權
解決方案:
- 申請 Spotify 企業帳號(需要商業驗證)
- 限制應用使用範圍在 25 人以內
- 使用 Spotify 的 Extended Quota 計劃(需要申請)
本專案採用 MIT 授權,可自由使用。但需遵守 Spotify API 使用條款。注意:商業使用需要 Spotify 企業帳號以支援更多用戶。
MIT License - 可自由使用、修改和分發
歡迎 PR!請確保:
- 通過 TypeScript 檢查
- 遵循現有代碼風格
- 更新相關文檔
Built with ❤️ using Next.js + PostgreSQL + Spotify Web API