milk-git55 commited on
Commit
49519f6
·
verified ·
1 Parent(s): 9d45d60

Upload music_api.py

Browse files
Files changed (1) hide show
  1. music_api.py +592 -0
music_api.py ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 修复版音乐API - 适配 Hugging Face Spaces (含自动扫描功能)
4
+ """
5
+
6
+ import os
7
+ import sqlite3
8
+ from pathlib import Path
9
+ from typing import Optional, List, Dict
10
+ import urllib.parse
11
+
12
+ from fastapi import FastAPI, Query, HTTPException
13
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ import uvicorn
17
+
18
+ # ==================== 第三方库导入 ====================
19
+ # 用于读取音频元数据
20
+ from mutagen import File as MutagenFile
21
+
22
+ # ==================== 配置部分 ====================
23
+
24
+ class Config:
25
+ """配置类"""
26
+ APP_NAME = "音乐API"
27
+ VERSION = "4.1.0" # 版本升级
28
+
29
+ # 路径配置
30
+ BASE_DIR = Path(__file__).resolve().parent
31
+ MUSIC_DIR = BASE_DIR / "static" / "music"
32
+ COVERS_DIR = BASE_DIR / "static" / "covers"
33
+ DATABASE_PATH = BASE_DIR / "music.db"
34
+
35
+ # 服务器配置
36
+ HOST = "0.0.0.0"
37
+ PORT = int(os.environ.get("PORT", 7860))
38
+ DEBUG = True
39
+
40
+ def __init__(self):
41
+ """确保目录存在"""
42
+ self.MUSIC_DIR.mkdir(parents=True, exist_ok=True)
43
+ self.COVERS_DIR.mkdir(parents=True, exist_ok=True)
44
+
45
+ config = Config()
46
+
47
+ # ==================== 辅助函数 ====================
48
+
49
+ def extract_audio_metadata(file_path: Path) -> Dict:
50
+ """
51
+ 使用 mutagen 提取音频文件的元数据
52
+ """
53
+ try:
54
+ # 尝试加载文件
55
+ audio = MutagenFile(file_path)
56
+ if audio is None:
57
+ return None
58
+
59
+ # 初始化默认值
60
+ title = file_path.stem # 默认使用文件名
61
+ artist = "Unknown Artist"
62
+ album = ""
63
+ genre = ""
64
+ duration = 0
65
+
66
+ # 尝试获取时长
67
+ if hasattr(audio, 'info') and audio.info.length:
68
+ duration = int(audio.info.length)
69
+
70
+ # 尝试获取标签 (兼容 MP3 ID3, MP4, OGG, FLAC 等)
71
+ # 常见的键名映射
72
+ common_keys = {
73
+ 'title': ['TIT2', '\xa9nam', 'TITLE', 'Title', 'title'],
74
+ 'artist': ['TPE1', '\xa9ART', 'ARTIST', 'Artist', 'artist'],
75
+ 'album': ['TALB', '\xa9alb', 'ALBUM', 'Album', 'album'],
76
+ 'genre': ['TCON', '\xa9gen', 'GENRE', 'Genre', 'genre']
77
+ }
78
+
79
+ # 辅助函数:获取第一个匹配的标签值
80
+ def get_tag(target_type):
81
+ keys = common_keys.get(target_type, [])
82
+ for k in keys:
83
+ if k in audio:
84
+ val = audio[k]
85
+ if isinstance(val, list) and len(val) > 0:
86
+ return str(val[0])
87
+ return str(val)
88
+ return None
89
+
90
+ # 覆盖默认值
91
+ found_title = get_tag('title')
92
+ if found_title:
93
+ title = found_title
94
+
95
+ found_artist = get_tag('artist')
96
+ if found_artist:
97
+ artist = found_artist
98
+
99
+ found_album = get_tag('album')
100
+ if found_album:
101
+ album = found_album
102
+
103
+ found_genre = get_tag('genre')
104
+ if found_genre:
105
+ genre = found_genre
106
+
107
+ return {
108
+ "title": title,
109
+ "artist": artist,
110
+ "album": album,
111
+ "genre": genre,
112
+ "duration": duration,
113
+ "file_size": file_path.stat().st_size
114
+ }
115
+
116
+ except Exception as e:
117
+ print(f"⚠️ 无法读取 {file_path.name} 的元数据: {e}")
118
+ return None
119
+
120
+ # ==================== 数据库部分 ====================
121
+
122
+ class Database:
123
+ """数据库管理类"""
124
+
125
+ @staticmethod
126
+ def get_connection():
127
+ """获取数据库连接"""
128
+ conn = sqlite3.connect(str(config.DATABASE_PATH))
129
+ conn.row_factory = sqlite3.Row
130
+ return conn
131
+
132
+ @staticmethod
133
+ def init_database():
134
+ """初始化数据库表结构"""
135
+ conn = Database.get_connection()
136
+ cursor = conn.cursor()
137
+
138
+ cursor.execute('''
139
+ CREATE TABLE IF NOT EXISTS music (
140
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
141
+ title TEXT NOT NULL,
142
+ artist TEXT NOT NULL,
143
+ album TEXT,
144
+ genre TEXT,
145
+ duration INTEGER,
146
+ file_name TEXT NOT NULL UNIQUE,
147
+ file_size INTEGER,
148
+ file_hash TEXT,
149
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
150
+ )
151
+ ''')
152
+
153
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_title ON music(title)')
154
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_artist ON music(artist)')
155
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_filename ON music(file_name)')
156
+
157
+ conn.commit()
158
+ conn.close()
159
+
160
+ @staticmethod
161
+ def scan_music_files() -> Dict:
162
+ """
163
+ 扫描 static/music 目录并更新数据库
164
+ """
165
+ added_count = 0
166
+ skipped_count = 0
167
+ error_files = []
168
+
169
+ # 支持的音频后缀
170
+ audio_extensions = ['.mp3', '.flac', '.wav', '.ogg', '.m4a', '.wma']
171
+
172
+ # 获取所有文件
173
+ if not config.MUSIC_DIR.exists():
174
+ return {"status": "error", "message": "音乐目录不存在"}
175
+
176
+ files = list(config.MUSIC_DIR.rglob('*'))
177
+
178
+ conn = Database.get_connection()
179
+ cursor = conn.cursor()
180
+
181
+ for file_path in files:
182
+ if not file_path.is_file():
183
+ continue
184
+ if file_path.suffix.lower() not in audio_extensions:
185
+ continue
186
+
187
+ file_name = file_path.name
188
+
189
+ # 提取元数据
190
+ meta = extract_audio_metadata(file_path)
191
+ if not meta:
192
+ # 如果读取失败,创建一个基本记录
193
+ meta = {
194
+ "title": file_path.stem,
195
+ "artist": "Unknown",
196
+ "album": "",
197
+ "genre": "",
198
+ "duration": 0,
199
+ "file_size": file_path.stat().st_size
200
+ }
201
+ error_files.append(file_name)
202
+
203
+ # 尝试插入数据库 (如果 file_name 已存在则忽略)
204
+ try:
205
+ cursor.execute('''
206
+ INSERT OR IGNORE INTO music
207
+ (title, artist, album, genre, duration, file_name, file_size)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?)
209
+ ''', (
210
+ meta['title'],
211
+ meta['artist'],
212
+ meta['album'],
213
+ meta['genre'],
214
+ meta['duration'],
215
+ file_name,
216
+ meta['file_size']
217
+ ))
218
+
219
+ if cursor.rowcount > 0:
220
+ added_count += 1
221
+ print(f"✅ 新增: {meta['title']} - {meta['artist']}")
222
+ else:
223
+ skipped_count += 1
224
+ print(f"⏭️ 跳过: {file_name} (已存在)")
225
+
226
+ except Exception as e:
227
+ print(f"❌ 数据库错误 ({file_name}): {e}")
228
+ error_files.append(file_name)
229
+
230
+ conn.commit()
231
+ conn.close()
232
+
233
+ return {
234
+ "status": "success",
235
+ "scanned": len(files),
236
+ "added": added_count,
237
+ "skipped": skipped_count,
238
+ "errors": len(error_files),
239
+ "message": f"扫描完成。新增 {added_count} 首歌曲。"
240
+ }
241
+
242
+ @staticmethod
243
+ def get_song_cover_url(song_id: int) -> str:
244
+ """获取歌曲封面URL"""
245
+ cover_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
246
+
247
+ for ext in cover_extensions:
248
+ cover_path = config.COVERS_DIR / f"{song_id}{ext}"
249
+ if cover_path.exists():
250
+ return f"/static/covers/{song_id}{ext}"
251
+
252
+ default_path = config.COVERS_DIR / f"{song_id}_default.jpg"
253
+ if default_path.exists():
254
+ return f"/static/covers/{song_id}_default.jpg"
255
+
256
+ return f"/api/music/{song_id}/default-cover"
257
+
258
+ @staticmethod
259
+ def get_all_songs(limit: int = 1000) -> List[Dict]:
260
+ """获取所有歌曲"""
261
+ conn = Database.get_connection()
262
+ cursor = conn.cursor()
263
+ cursor.execute(f"SELECT * FROM music ORDER BY title LIMIT ?", (limit,))
264
+ rows = cursor.fetchall()
265
+ conn.close()
266
+
267
+ songs = []
268
+ for row in rows:
269
+ song_id = row["id"]
270
+ songs.append({
271
+ "id": song_id,
272
+ "title": row["title"],
273
+ "artist": row["artist"],
274
+ "album": row["album"] or "",
275
+ "genre": row["genre"] or "",
276
+ "duration": row["duration"] or 0,
277
+ "file_name": row["file_name"],
278
+ "file_size": row["file_size"] or 0,
279
+ "cover_url": Database.get_song_cover_url(song_id),
280
+ "play_url": f"/api/music/{song_id}/play",
281
+ "download_url": f"/api/music/{song_id}/download"
282
+ })
283
+ return songs
284
+
285
+ @staticmethod
286
+ def search_songs(query: str = None, artist: str = None, limit: int = 100) -> List[Dict]:
287
+ """搜索歌曲"""
288
+ conn = Database.get_connection()
289
+ cursor = conn.cursor()
290
+
291
+ sql = "SELECT * FROM music WHERE 1=1"
292
+ params = []
293
+
294
+ if query:
295
+ sql += " AND (title LIKE ? OR artist LIKE ? OR album LIKE ?)"
296
+ search_term = f"%{query}%"
297
+ params.extend([search_term, search_term, search_term])
298
+
299
+ if artist:
300
+ sql += " AND artist LIKE ?"
301
+ params.append(f"%{artist}%")
302
+
303
+ sql += " ORDER BY title LIMIT ?"
304
+ params.append(limit)
305
+
306
+ cursor.execute(sql, params)
307
+ rows = cursor.fetchall()
308
+ conn.close()
309
+
310
+ songs = []
311
+ for row in rows:
312
+ song_id = row["id"]
313
+ songs.append({
314
+ "id": song_id,
315
+ "title": row["title"],
316
+ "artist": row["artist"],
317
+ "album": row["album"] or "",
318
+ "genre": row["genre"] or "",
319
+ "duration": row["duration"] or 0,
320
+ "file_name": row["file_name"],
321
+ "file_size": row["file_size"] or 0,
322
+ "cover_url": Database.get_song_cover_url(song_id),
323
+ "play_url": f"/api/music/{song_id}/play",
324
+ "download_url": f"/api/music/{song_id}/download"
325
+ })
326
+ return songs
327
+
328
+ @staticmethod
329
+ def get_song_by_id(song_id: int) -> Optional[Dict]:
330
+ """根据ID获取歌曲"""
331
+ conn = Database.get_connection()
332
+ cursor = conn.cursor()
333
+ cursor.execute("SELECT * FROM music WHERE id = ?", (song_id,))
334
+ row = cursor.fetchone()
335
+ conn.close()
336
+
337
+ if not row:
338
+ return None
339
+
340
+ return {
341
+ "id": song_id,
342
+ "title": row["title"],
343
+ "artist": row["artist"],
344
+ "album": row["album"] or "",
345
+ "genre": row["genre"] or "",
346
+ "duration": row["duration"] or 0,
347
+ "file_name": row["file_name"],
348
+ "file_size": row["file_size"] or 0,
349
+ "cover_url": Database.get_song_cover_url(song_id),
350
+ "play_url": f"/api/music/{song_id}/play",
351
+ "download_url": f"/api/music/{song_id}/download"
352
+ }
353
+
354
+ # ==================== FastAPI应用部分 ====================
355
+
356
+ app = FastAPI(
357
+ title=config.APP_NAME,
358
+ description="音乐API系统 - 封面URL直接绑定",
359
+ version=config.VERSION,
360
+ docs_url="/docs"
361
+ )
362
+
363
+ app.add_middleware(
364
+ CORSMiddleware,
365
+ allow_origins=["*"],
366
+ allow_credentials=True,
367
+ allow_methods=["*"],
368
+ allow_headers=["*"],
369
+ )
370
+
371
+ app.mount("/static", StaticFiles(directory="static"), name="static")
372
+
373
+ # ==================== API 文档页面 HTML (保持不变,为节省空间此处省略完整HTML,请使用上一次的HTML内容) ====================
374
+ # 为了方便您直接复制,这里仍然包含完整的 HTML
375
+ API_DOCS_HTML = """
376
+ <!DOCTYPE html>
377
+ <html lang="zh-CN">
378
+ <head>
379
+ <meta charset="UTF-8">
380
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
381
+ <title>音乐API文档 - v4.1.0</title>
382
+ <style>
383
+ :root {
384
+ --bg-color: #1a1a2e; --card-bg: #16213e; --text-primary: #e0e0e0; --text-secondary: #a0a0a0;
385
+ --accent-color: #0f3460; --highlight: #e94560; --method-get: #61affe; --method-post: #49cc90;
386
+ --code-bg: #0f0f1a;
387
+ }
388
+ * { margin: 0; padding: 0; box-sizing: border-box; }
389
+ body { font-family: 'Segoe UI', sans-serif; background: var(--bg-color); color: var(--text-primary); line-height: 1.6; padding: 20px; }
390
+ .container { max-width: 1000px; margin: 0 auto; }
391
+ header { text-align: center; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 1px solid var(--accent-color); }
392
+ h1 { font-size: 2.5rem; margin-bottom: 10px; color: #fff; }
393
+ .version { background: var(--highlight); padding: 2px 8px; border-radius: 4px; font-size: 0.9rem; }
394
+ .endpoint-card { background: var(--card-bg); border-radius: 8px; padding: 20px; margin-bottom: 20px; border: 1px solid var(--accent-color); }
395
+ .endpoint-header { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; flex-wrap: wrap; }
396
+ .method { color: #fff; padding: 4px 10px; border-radius: 4px; font-weight: bold; font-size: 0.9rem; min-width: 60px; text-align: center; }
397
+ .method.get { background: var(--method-get); }
398
+ .method.post { background: var(--method-post); }
399
+ .path { font-family: 'Courier New', monospace; color: #fff; background: rgba(0,0,0,0.3); padding: 4px 10px; border-radius: 4px; flex: 1; word-break: break-all; }
400
+ table { width: 100%; border-collapse: collapse; margin-top: 10px; }
401
+ th, td { text-align: left; padding: 10px; border-bottom: 1px solid var(--accent-color); font-size: 0.9rem; }
402
+ .code-block { background: var(--code-bg); padding: 15px; border-radius: 6px; overflow-x: auto; margin-top: 10px; font-family: monospace; font-size: 0.9rem; color: #a5d6ff; }
403
+ .desc { color: var(--text-secondary); margin-bottom: 15px; }
404
+ </style>
405
+ </head>
406
+ <body>
407
+ <div class="container">
408
+ <header>
409
+ <h1>音乐API <span class="version">v4.1.0</span></h1>
410
+ <p style="color:#aaa">基于 FastAPI 的轻量级音乐流媒体服务 | <a href="/docs" style="color:#61affe">Swagger文档</a></p>
411
+ </header>
412
+
413
+ <div style="background: var(--card-bg); padding: 20px; border-radius: 8px; margin-bottom: 30px; border-left: 4px solid #49cc90;">
414
+ <h2 style="margin-bottom: 10px;">🚀 快速开始</h2>
415
+ <p style="color:#ccc">1. 上传音乐文件到 <code>static/music/</code> 目录</p>
416
+ <p style="color:#ccc">2. 访问此接口扫描文件: <a href="/api/scan" target="_blank" style="color:#fff; background:#49cc90; padding:2px 8px; text-decoration:none; border-radius:4px;">GET /api/scan</a></p>
417
+ <p style="color:#ccc">3. 访问首页查看列表或使用 API 播放</p>
418
+ </div>
419
+
420
+ <h3 style="margin-bottom:20px;">接口列表</h3>
421
+
422
+ <div class="endpoint-card">
423
+ <div class="endpoint-header">
424
+ <span class="method get">GET</span>
425
+ <span class="path">/api/scan</span>
426
+ </div>
427
+ <p class="desc">扫描 static/music 目录并更新数据库(新增歌曲)</p>
428
+ </div>
429
+
430
+ <div class="endpoint-card">
431
+ <div class="endpoint-header">
432
+ <span class="method get">GET</span>
433
+ <span class="path">/api/music</span>
434
+ </div>
435
+ <p class="desc">获取歌曲列表或搜索</p>
436
+ </div>
437
+
438
+ <div class="endpoint-card">
439
+ <div class="endpoint-header">
440
+ <span class="method get">GET</span>
441
+ <span class="path">/api/music/{id}/play</span>
442
+ </div>
443
+ <p class="desc">播放指定ID的音乐</p>
444
+ </div>
445
+
446
+ <div style="text-align: center; margin-top: 50px; color: #666; font-size: 0.8rem;">
447
+ Powered by FastAPI & HuggingFace Spaces
448
+ </div>
449
+ </div>
450
+ </body>
451
+ </html>
452
+ """
453
+
454
+ # ==================== 启动事件 ====================
455
+
456
+ @app.on_event("startup")
457
+ def startup_event():
458
+ Database.init_database()
459
+
460
+ # ==================== API路由部分 ====================
461
+
462
+ @app.get("/")
463
+ async def get_api_docs():
464
+ return HTMLResponse(API_DOCS_HTML)
465
+
466
+ @app.get("/api/scan")
467
+ async def scan_library():
468
+ """扫描音乐文件并导入数据库"""
469
+ result = Database.scan_music_files()
470
+ return JSONResponse(result)
471
+
472
+ @app.get("/api/music")
473
+ async def get_music(
474
+ q: Optional[str] = Query(None, description="搜索关键词"),
475
+ artist: Optional[str] = Query(None, description="艺术家"),
476
+ limit: int = Query(100, ge=1, le=1000, description="返回数量")
477
+ ):
478
+ return Database.search_songs(q, artist, limit)
479
+
480
+ @app.get("/api/music/{music_id}")
481
+ async def get_music_by_id(music_id: int):
482
+ song = Database.get_song_by_id(music_id)
483
+ if not song:
484
+ raise HTTPException(status_code=404, detail="歌曲不存在")
485
+ return song
486
+
487
+ @app.get("/api/music/{music_id}/play")
488
+ async def play_music(music_id: int):
489
+ song = Database.get_song_by_id(music_id)
490
+ if not song:
491
+ raise HTTPException(status_code=404, detail="歌曲不存在")
492
+
493
+ file_path = config.MUSIC_DIR / song['file_name']
494
+ if not file_path.exists():
495
+ raise HTTPException(status_code=404, detail="音乐文件不存在")
496
+
497
+ encoded_filename = urllib.parse.quote(song['file_name'])
498
+
499
+ return FileResponse(
500
+ file_path,
501
+ filename=encoded_filename,
502
+ media_type="audio/mpeg",
503
+ headers={
504
+ "Content-Disposition": f"inline; filename*=UTF-8''{encoded_filename}",
505
+ "Accept-Ranges": "bytes"
506
+ }
507
+ )
508
+
509
+ @app.get("/api/music/{music_id}/download")
510
+ async def download_music(music_id: int):
511
+ song = Database.get_song_by_id(music_id)
512
+ if not song:
513
+ raise HTTPException(status_code=404, detail="歌曲不存在")
514
+
515
+ file_path = config.MUSIC_DIR / song['file_name']
516
+ if not file_path.exists():
517
+ raise HTTPException(status_code=404, detail="音乐文件不存在")
518
+
519
+ encoded_filename = urllib.parse.quote(song['file_name'])
520
+
521
+ return FileResponse(
522
+ file_path,
523
+ filename=encoded_filename,
524
+ media_type="application/octet-stream",
525
+ headers={
526
+ "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
527
+ }
528
+ )
529
+
530
+ @app.get("/api/music/{music_id}/default-cover")
531
+ async def get_default_cover(music_id: int):
532
+ from io import BytesIO
533
+ from PIL import Image, ImageDraw, ImageFont
534
+ import base64
535
+
536
+ img = Image.new('RGB', (300, 300), color=(106, 17, 203))
537
+ draw = ImageDraw.Draw(img)
538
+ try:
539
+ font = ImageFont.truetype("arial.ttf", 100)
540
+ except:
541
+ font = ImageFont.load_default()
542
+ draw.text((150, 150), "♪", fill=(255, 255, 255), font=font, anchor="mm")
543
+
544
+ buffer = BytesIO()
545
+ img.save(buffer, format="PNG")
546
+ img_base64 = base64.b64encode(buffer.getvalue()).decode()
547
+
548
+ return {
549
+ "image": f"data:image/png;base64,{img_base64}",
550
+ "song_id": music_id,
551
+ "is_default": True
552
+ }
553
+
554
+ @app.get("/api/stats")
555
+ async def get_stats():
556
+ conn = Database.get_connection()
557
+ cursor = conn.cursor()
558
+
559
+ cursor.execute("SELECT COUNT(*) as total FROM music")
560
+ total = cursor.fetchone()[0]
561
+
562
+ cursor.execute("SELECT SUM(file_size) as total_size FROM music")
563
+ total_size = cursor.fetchone()[0] or 0
564
+
565
+ cover_files = list(config.COVERS_DIR.glob("*.jpg")) + list(config.COVERS_DIR.glob("*.png"))
566
+ cover_count = len(cover_files)
567
+
568
+ conn.close()
569
+
570
+ return {
571
+ "total_songs": total,
572
+ "total_size_gb": round(total_size / (1024**3), 2),
573
+ "cover_count": cover_count,
574
+ "cover_percentage": round(cover_count / total * 100, 1) if total > 0 else 0
575
+ }
576
+
577
+ @app.get("/api/health")
578
+ async def health_check():
579
+ return {
580
+ "status": "ok",
581
+ "service": config.APP_NAME,
582
+ "version": config.VERSION,
583
+ "endpoints": {
584
+ "API文档": "/",
585
+ "扫描歌曲": "/api/scan",
586
+ "Swagger交互文档": "/docs",
587
+ "搜索音乐": "/api/music?q=关键词",
588
+ "播放歌曲": "/api/music/{id}/play",
589
+ "下载歌曲": "/api/music/{id}/download",
590
+ "系统统计": "/api/stats"
591
+ }
592
+ }