milk-git55 commited on
Commit
a158854
·
verified ·
1 Parent(s): ae27a4b

Update music_api.py

Browse files
Files changed (1) hide show
  1. music_api.py +143 -207
music_api.py CHANGED
@@ -1,13 +1,13 @@
1
  #!/usr/bin/env python3
2
  """
3
- 修复版音乐API - 支持Dataset读取 + API文件上传(双源存储)
4
  """
5
 
6
  import os
7
  import sqlite3
8
  import shutil
9
  from pathlib import Path
10
- from typing import Optional, List, Dict, Union
11
  import urllib.parse
12
 
13
  from fastapi import FastAPI, Query, HTTPException, UploadFile, File, Form
@@ -16,65 +16,64 @@ from fastapi.staticfiles import StaticFiles
16
  from fastapi.middleware.cors import CORSMiddleware
17
  import uvicorn
18
  from mutagen import File as MutagenFile
 
19
 
20
  # ==================== 配置部分 ====================
21
 
22
  class Config:
23
  """配置类"""
24
  APP_NAME = "音乐API"
25
- VERSION = "4.5.0"
26
 
27
  BASE_DIR = Path(__file__).resolve().parent
28
 
29
- # ===== 挂载的 Dataset (只读) =====
30
- # 假设挂载点为 /dataset
31
- DATASET_DIR = Path("/dataset")
 
 
32
 
33
- # ===== 持久存储 (可写) =====
34
- # 如果开启了 Space Persistent Storage它是 /data,重启后数据保留
35
- # 如果没开,请改为 /tmp (但上传文件重启后丢失)
36
- STORAGE_DIR = Path("/data")
37
 
38
- # ===== 双源目录定义 =====
39
- # 逻辑:优先在 STORAGE_DIR 读取,找不到再去 DATASET_DIR 读取
40
-
41
- # 音乐目录列表 (优先级从前到后)
42
- MUSIC_DIRS = [
43
- STORAGE_DIR / "static" / "music", # 新上传的文件放这里
44
- DATASET_DIR / "static" / "music" # 原始 Dataset 的文件
45
- ]
46
-
47
- # 封面目录列表
48
- COVER_DIRS = [
49
- STORAGE_DIR / "static" / "covers",
50
- DATASET_DIR / "static" / "covers"
51
- ]
52
-
53
- # 数据库路径 (放在持久存储里,重启不丢)
54
- DATABASE_PATH = STORAGE_DIR / "music.db"
55
 
56
  HOST = "0.0.0.0"
57
  PORT = int(os.environ.get("PORT", 7860))
58
  DEBUG = True
59
 
60
  def __init__(self):
61
- # 确保可写目录存在
62
- for d in self.MUSIC_DIRS + self.COVER_DIRS:
63
- d.mkdir(parents=True, exist_ok=True)
64
- # 确保数据库目录存在
65
  self.DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True)
66
 
67
  config = Config()
68
 
69
  # ==================== 辅助函数 ====================
70
 
71
- def find_file(filename: str, directories: List[Path]) -> Optional[Path]:
72
- """在多个目录中查找文件"""
73
- for directory in directories:
74
- path = directory / filename
75
- if path.exists() and path.is_file():
76
- return path
77
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  def extract_audio_metadata(file_path: Path) -> Dict:
80
  """使用 mutagen 提取音频文件的元数据"""
@@ -108,13 +107,11 @@ def extract_audio_metadata(file_path: Path) -> Dict:
108
  if get_tag('genre'): genre = get_tag('genre')
109
  return {"title": title, "artist": artist, "album": album, "genre": genre, "duration": duration, "file_size": file_path.stat().st_size}
110
  except Exception as e:
111
- print(f"⚠️ 无法读取 {file_path.name}: {e}")
112
  return None
113
 
114
  # ==================== 数据库部分 ====================
115
 
116
  class Database:
117
- """数据库管理类"""
118
  @staticmethod
119
  def get_connection():
120
  conn = sqlite3.connect(str(config.DATABASE_PATH))
@@ -135,24 +132,24 @@ class Database:
135
  duration INTEGER,
136
  file_name TEXT NOT NULL UNIQUE,
137
  file_size INTEGER,
 
 
138
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
139
  )
140
  ''')
141
- cursor.execute('CREATE INDEX IF NOT EXISTS idx_title ON music(title)')
142
- cursor.execute('CREATE INDEX IF NOT EXISTS idx_filename ON music(file_name)')
143
  conn.commit()
144
  conn.close()
145
 
146
  @staticmethod
147
- def add_song(title: str, artist: str, album: str, genre: str, duration: int, file_name: str, file_size: int) -> int:
148
- """添加一首歌到数据库"""
149
  conn = Database.get_connection()
150
  cursor = conn.cursor()
151
  try:
152
  cursor.execute('''
153
- INSERT INTO music (title, artist, album, genre, duration, file_name, file_size)
154
- VALUES (?, ?, ?, ?, ?, ?, ?)
155
- ''', (title, artist, album, genre, duration, file_name, file_size))
 
156
  song_id = cursor.lastrowid
157
  conn.commit()
158
  return song_id
@@ -162,52 +159,68 @@ class Database:
162
  conn.close()
163
 
164
  @staticmethod
165
- def scan_music_files() -> Dict:
166
- """扫描所有定义的音乐目录并更新数据库"""
 
 
 
167
  added_count = 0
168
  skipped_count = 0
169
- audio_extensions = ['.mp3', '.flac', '.wav', '.ogg', '.m4a', '.wma']
 
 
 
 
 
 
 
 
 
 
 
170
  conn = Database.get_connection()
171
  cursor = conn.cursor()
172
 
173
- # 遍历所有定义的目录
174
- for base_dir in config.MUSIC_DIRS:
175
- if not base_dir.exists(): continue
176
- print(f"正在扫描目录: {base_dir}")
177
- files = list(base_dir.rglob('*'))
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- for file_path in files:
180
- if not file_path.is_file(): continue
181
- if file_path.suffix.lower() not in audio_extensions: continue
182
-
183
- file_name = file_path.name
184
- meta = extract_audio_metadata(file_path)
185
-
186
- # 如果提取失败,使用默认
187
- if not meta:
188
- meta = {"title": file_path.stem, "artist": "Unknown", "album": "", "genre": "", "duration": 0, "file_size": file_path.stat().st_size}
 
 
 
 
 
 
 
189
 
190
- try:
191
- cursor.execute('''
192
- INSERT OR IGNORE INTO music
193
- (title, artist, album, genre, duration, file_name, file_size)
194
- VALUES (?, ?, ?, ?, ?, ?, ?)
195
- ''', (meta['title'], meta['artist'], meta['album'], meta['genre'], meta['duration'], file_name, meta['file_size']))
196
-
197
- if cursor.rowcount > 0:
198
- added_count += 1
199
- print(f"✅ 新增: {meta['title']} - {meta['artist']} (来源: {base_dir.name})")
200
- else:
201
- skipped_count += 1
202
- except Exception as e:
203
- print(f"❌ 错误 ({file_name}): {e}")
204
-
205
- conn.commit()
206
  conn.close()
207
- return {"status": "success", "added": added_count, "skipped": skipped_count, "message": f"扫描完成,新增 {added_count} 首"}
208
 
209
  @staticmethod
210
- def get_all_songs(limit: int = 1000) -> List[Dict]:
211
  conn = Database.get_connection()
212
  cursor = conn.cursor()
213
  cursor.execute("SELECT * FROM music ORDER BY id DESC LIMIT ?", (limit,))
@@ -216,7 +229,7 @@ class Database:
216
  return [dict(row) for row in rows]
217
 
218
  @staticmethod
219
- def get_song_by_id(song_id: int) -> Optional[Dict]:
220
  conn = Database.get_connection()
221
  cursor = conn.cursor()
222
  cursor.execute("SELECT * FROM music WHERE id = ?", (song_id,))
@@ -226,13 +239,7 @@ class Database:
226
 
227
  # ==================== FastAPI应用部分 ====================
228
 
229
- app = FastAPI(title=config.APP_NAME, version=config.VERSION, docs_url="/docs")
230
-
231
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
232
-
233
- # ==================== 页面 ====================
234
-
235
- API_DOCS_HTML = """
236
  <!DOCTYPE html>
237
  <html lang="zh-CN">
238
  <head>
@@ -347,8 +354,8 @@ API_DOCS_HTML = """
347
  const songs = await res.json();
348
  const html = songs.map(s =>
349
  `<div style="padding:10px; border-bottom:1px solid #333;">
350
- <b>${s.title}</b> - ${s.artist}<br>
351
- <small>File: ${s.file_name} | Size: ${(s.file_size/1024/1024).toFixed(2)}MB</small>
352
  </div>`
353
  ).join('');
354
  document.getElementById('songsList').innerHTML = html || '暂无歌曲,请扫描或上传';
@@ -362,8 +369,12 @@ API_DOCS_HTML = """
362
  </body>
363
  </html>
364
  """
 
 
 
 
 
365
 
366
- # ==================== 路由部分 ====================
367
 
368
  @app.on_event("startup")
369
  def startup_event():
@@ -373,124 +384,49 @@ def startup_event():
373
  async def get_frontend():
374
  return HTMLResponse(API_DOCS_HTML)
375
 
376
- # --- 资源服务 (核心逻辑: 优先本地,其次 Dataset) ---
377
- @app.get("/static/{type}/{filename}")
378
- async def serve_static(type: str, filename: str):
379
- """
380
- 统一的静态资源服务接口
381
- type: 'music' or 'covers'
382
- """
383
- dirs = config.MUSIC_DIRS if type == 'music' else config.COVER_DIRS
384
- filepath = find_file(filename, dirs)
385
-
386
- if not filepath:
387
- raise HTTPException(status_code=404, detail="文件未找到")
388
-
389
- return FileResponse(filepath)
390
-
391
- # --- API: 扫描 ---
392
- @app.get("/api/scan")
393
- async def scan_library():
394
- return JSONResponse(Database.scan_music_files())
395
-
396
- # --- API: 上传 ---
397
- @app.post("/api/upload")
398
- async def upload_song(
399
- file: UploadFile = File(..., description="MP3 文件"),
400
- cover: Optional[UploadFile] = File(None, description="封面图片"),
401
- title: Optional[str] = Form(None, description="标题"),
402
- artist: Optional[str] = Form(None, description="艺术家")
403
- ):
404
- """上传歌曲并更新数据库"""
405
-
406
- # 1. 准备保存路径 (保存到 STORAGE_DIR)
407
- write_music_dir = config.STORAGE_DIR / "static" / "music"
408
- write_cover_dir = config.STORAGE_DIR / "static" / "covers"
409
-
410
- write_music_dir.mkdir(parents=True, exist_ok=True)
411
- write_cover_dir.mkdir(parents=True, exist_ok=True)
412
-
413
- # 2. 保存音乐文件
414
- # 使用安全文件名
415
- safe_filename = urllib.parse.quote(file.filename).replace('/', '-')
416
- music_path = write_music_dir / safe_filename
417
-
418
- try:
419
- with open(music_path, "wb") as buffer:
420
- shutil.copyfileobj(file.file, buffer)
421
- except Exception as e:
422
- raise HTTPException(status_code=500, detail=f"保存音乐文件失败: {e}")
423
-
424
- # 3. 提取元数据 (如果没提供)
425
- if not title or not artist:
426
- meta = extract_audio_metadata(music_path)
427
- if meta:
428
- if not title: title = meta['title']
429
- if not artist: artist = meta['artist']
430
-
431
- # 4. 保存封面 (如果有)
432
- if cover:
433
- # 封面文件名暂时用歌曲ID还不知道,所以先随机名或原始名,这里简单用原始名
434
- safe_cover_name = urllib.parse.quote(cover.filename).replace('/', '-')
435
- cover_path = write_cover_dir / safe_cover_name
436
- try:
437
- with open(cover_path, "wb") as buffer:
438
- shutil.copyfileobj(cover.file, buffer)
439
- # TODO: 这里有个逻辑问题,封面通常对应 ID,上传时还没 ID。
440
- # 解决方案:暂时存为原名,上传后获取 ID 再重命名,或者前端调用单独的更新封面接口。
441
- # 为简化,这里暂不重命名为 ID.jpg,而是依赖用户后续手动管理或者扫描逻辑适配。
442
- # *改进策略*:将封面重命名为 {音乐ID}.jpg (插入数据库后执行)
443
- except Exception as e:
444
- print(f"保存封面失败: {e}")
445
-
446
- # 5. 插入数据库
447
- song_id = Database.add_song(
448
- title=title or "未知标题",
449
- artist=artist or "未知艺术家",
450
- album="", # 上传接口暂未提供专辑字段
451
- genre="",
452
- duration=0, # 如果已提取 meta 可以填入
453
- file_name=safe_filename,
454
- file_size=music_path.stat().st_size
455
- )
456
 
457
- if song_id == 0:
458
- # 插入失败(文件名冲突),删除刚才上传的文件
459
- music_path.unlink()
460
- raise HTTPException(status_code=400, detail="该文件名已存在数据库中")
 
 
461
 
462
- # 6. 如果有封面,重命名为 ID.ext
463
- if cover:
464
  try:
465
- ext = Path(safe_cover_name).suffix
466
- target_cover_path = write_cover_dir / f"{song_id}{ext}"
467
- cover_path.rename(target_cover_path)
468
- except:
469
- pass
470
-
471
- return JSONResponse({
472
- "id": song_id,
473
- "title": title,
474
- "artist": artist,
475
- "file_name": safe_filename,
476
- "message": "上传成功"
477
- })
478
 
479
  # --- 其他 API ---
 
 
 
 
480
  @app.get("/api/music")
481
  async def get_music(limit: int = 100):
482
- songs = Database.get_all_songs(limit)
483
- # 补全 URL
484
- for s in songs:
485
- s['cover_url'] = f"/static/covers/{s['id']}.jpg" # 假设 jpg,实际可以遍历后缀
486
- s['play_url'] = f"/api/play/{s['id']}"
487
- return songs
488
 
489
- @app.get("/api/play/{song_id}")
490
- async def play_music(song_id: int):
491
- song = Database.get_song_by_id(song_id)
492
- if not song: raise HTTPException(404)
493
- # 查找实际文件
494
- filepath = find_file(song['file_name'], config.MUSIC_DIRS)
495
- if not filepath: raise HTTPException(404, detail="音乐文件丢失")
496
- return FileResponse(filepath, media_type="audio/mpeg")
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ 修复版音乐API - 适配 Gradio/Non-Docker Space (直接读取 Dataset)
4
  """
5
 
6
  import os
7
  import sqlite3
8
  import shutil
9
  from pathlib import Path
10
+ from typing import Optional, List, Dict
11
  import urllib.parse
12
 
13
  from fastapi import FastAPI, Query, HTTPException, UploadFile, File, Form
 
16
  from fastapi.middleware.cors import CORSMiddleware
17
  import uvicorn
18
  from mutagen import File as MutagenFile
19
+ from huggingface_hub import hf_hub_download, snapshot_download, HfApi
20
 
21
  # ==================== 配置部分 ====================
22
 
23
  class Config:
24
  """配置类"""
25
  APP_NAME = "音乐API"
26
+ VERSION = "4.6.0"
27
 
28
  BASE_DIR = Path(__file__).resolve().parent
29
 
30
+ # ===== Dataset 配置 =====
31
+ # 请填入您的 Dataset 路径,例如 "your_username/my-music-library"
32
+ DATASET_REPO_ID = "my-music-library"
33
+ # 本地缓存目录 (HuggingFace 会自动缓存下载的文件)
34
+ DATASET_CACHE_DIR = Path("/tmp/hf_cache")
35
 
36
+ # ===== 本地存储 (新上传的文件) =====
37
+ # 非Docker环境下,通常只有 /tmp /home/user这里用 /tmp 缓存新上传
38
+ # 注意:Space 重 /tmp 会清空,这是 Gradio 模式的限制
39
+ UPLOAD_DIR = Path("/tmp/uploads")
40
 
41
+ # 数据库路径
42
+ DATABASE_PATH = Path("/tmp/music.db")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  HOST = "0.0.0.0"
45
  PORT = int(os.environ.get("PORT", 7860))
46
  DEBUG = True
47
 
48
  def __init__(self):
49
+ self.UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
 
 
 
50
  self.DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True)
51
 
52
  config = Config()
53
 
54
  # ==================== 辅助函数 ====================
55
 
56
+ def get_dataset_files():
57
+ """
58
+ 尝试获取 Dataset 的文件列表。
59
+ 由于非Docker模式无法直接挂载,我们尝试从 Hub 获取文件列表。
60
+ 如果失败(例如没有权限),返回空列表。
61
+ """
62
+ try:
63
+ api = HfApi()
64
+ # 获取 repo 所有文件树
65
+ repo_files = api.list_repo_tree(repo_id=config.DATASET_REPO_ID, token=os.environ.get("HF_TOKEN"), repo_type="dataset")
66
+
67
+ # 转换成相对路径列表
68
+ files = []
69
+ for file_info in repo_files:
70
+ if file_info.type == "file":
71
+ # 转换为 Path 对象的相对路径
72
+ files.append(file_info.path)
73
+ return files
74
+ except Exception as e:
75
+ print(f"⚠️ 无法连接到 Dataset Hub: {e}")
76
+ return []
77
 
78
  def extract_audio_metadata(file_path: Path) -> Dict:
79
  """使用 mutagen 提取音频文件的元数据"""
 
107
  if get_tag('genre'): genre = get_tag('genre')
108
  return {"title": title, "artist": artist, "album": album, "genre": genre, "duration": duration, "file_size": file_path.stat().st_size}
109
  except Exception as e:
 
110
  return None
111
 
112
  # ==================== 数据库部分 ====================
113
 
114
  class Database:
 
115
  @staticmethod
116
  def get_connection():
117
  conn = sqlite3.connect(str(config.DATABASE_PATH))
 
132
  duration INTEGER,
133
  file_name TEXT NOT NULL UNIQUE,
134
  file_size INTEGER,
135
+ source_type TEXT DEFAULT 'local', -- 'local' 或 'dataset'
136
+ file_path TEXT,
137
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
138
  )
139
  ''')
 
 
140
  conn.commit()
141
  conn.close()
142
 
143
  @staticmethod
144
+ def add_song(title, artist, album, genre, duration, file_name, file_size, source_type, file_path):
 
145
  conn = Database.get_connection()
146
  cursor = conn.cursor()
147
  try:
148
  cursor.execute('''
149
+ INSERT OR IGNORE INTO music
150
+ (title, artist, album, genre, duration, file_name, file_size, source_type, file_path)
151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
152
+ ''', (title, artist, album, genre, duration, file_name, file_size, source_type, file_path))
153
  song_id = cursor.lastrowid
154
  conn.commit()
155
  return song_id
 
159
  conn.close()
160
 
161
  @staticmethod
162
+ def scan_dataset_files():
163
+ """
164
+ 从 Dataset Hub 扫描文件并注册到数据库
165
+ 注意:这里只是注册元数据,文件会在第一次播放时才下载
166
+ """
167
  added_count = 0
168
  skipped_count = 0
169
+
170
+ # 1. 获取文件列表
171
+ remote_files = get_dataset_files()
172
+ if not remote_files:
173
+ return {"status": "error", "message": "无法获取 Dataset 文件列表,请检查名称或 Token"}
174
+
175
+ # 2. 扫描本地上传目录
176
+ local_files = []
177
+ if config.UPLOAD_DIR.exists():
178
+ for ext in ['.mp3', '.flac', '.wav', '.ogg', '.m4a']:
179
+ local_files.extend(config.UPLOAD_DIR.glob(f"*{ext}"))
180
+
181
  conn = Database.get_connection()
182
  cursor = conn.cursor()
183
 
184
+ # 处理本地文件
185
+ for f_path in local_files:
186
+ meta = extract_audio_metadata(f_path) or {"title": f_path.stem, "artist": "Unknown", "album": "", "genre": "", "duration": 0, "file_size": f_path.stat().st_size}
187
+ song_id = Database.add_song(meta['title'], meta['artist'], meta['album'], meta['genre'], meta['duration'], f_path.name, meta['file_size'], 'local', str(f_path))
188
+ if song_id > 0:
189
+ added_count += 1
190
+ print(f"✅ 本地: {meta['title']}")
191
+ else:
192
+ skipped_count += 1
193
+
194
+ # 处理远程文件
195
+ # 假设 Dataset 结构是 static/music/song.mp3
196
+ target_prefix = "static/music/"
197
+ for file_path in remote_files:
198
+ if not file_path.startswith(target_prefix):
199
+ continue
200
 
201
+ # 提取文件名
202
+ filename = file_path.split("/")[-1]
203
+
204
+ # 简单估算大小 (这里无法获取远程文件真实大小,先填0)
205
+ size = 0
206
+ duration = 0 # 无法扫描远程文件元数据
207
+
208
+ # 默认元数据
209
+ title = Path(filename).stem
210
+ artist = "Unknown Artist"
211
+
212
+ song_id = Database.add_song(title, artist, "", "", duration, filename, size, 'dataset', file_path)
213
+ if song_id > 0:
214
+ added_count += 1
215
+ print(f"✅ 远程: {filename}")
216
+ else:
217
+ skipped_count += 1
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  conn.close()
220
+ return {"status": "success", "added": added_count, "skipped": skipped_count, "message": f"扫描完成"}
221
 
222
  @staticmethod
223
+ def get_all_songs(limit=100):
224
  conn = Database.get_connection()
225
  cursor = conn.cursor()
226
  cursor.execute("SELECT * FROM music ORDER BY id DESC LIMIT ?", (limit,))
 
229
  return [dict(row) for row in rows]
230
 
231
  @staticmethod
232
+ def get_song_by_id(song_id: int):
233
  conn = Database.get_connection()
234
  cursor = conn.cursor()
235
  cursor.execute("SELECT * FROM music WHERE id = ?", (song_id,))
 
239
 
240
  # ==================== FastAPI应用部分 ====================
241
 
242
+ API_DOCS_HTML = API_DOCS_HTML = """
 
 
 
 
 
 
243
  <!DOCTYPE html>
244
  <html lang="zh-CN">
245
  <head>
 
354
  const songs = await res.json();
355
  const html = songs.map(s =>
356
  `<div style="padding:10px; border-bottom:1px solid #333;">
357
+ <b>${s.title}</b> -${s.artist}<br>
358
+ <small>File: ${s.file_name} | Size:${(s.file_size/1024/1024).toFixed(2)}MB</small>
359
  </div>`
360
  ).join('');
361
  document.getElementById('songsList').innerHTML = html || '暂无歌曲,请扫描或上传';
 
369
  </body>
370
  </html>
371
  """
372
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
373
+
374
+ # 页面 (保持之前的 HTML 结构,这里为了简洁略去,请使用上一版完整的 HTML)
375
+ # ... (请确保复制上一版完整的 API_DOCS_HTML 字符串到这里,或者在代码末尾单独提供) ...
376
+ # 为了代码完整性,这里附上一个简单的 HTML 占位
377
 
 
378
 
379
  @app.on_event("startup")
380
  def startup_event():
 
384
  async def get_frontend():
385
  return HTMLResponse(API_DOCS_HTML)
386
 
387
+ # --- 播放接口 (核心逻辑) ---
388
+ @app.get("/api/play/{song_id}")
389
+ async def play_music(song_id: int):
390
+ song = Database.get_song_by_id(song_id)
391
+ if not song: raise HTTPException(404)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
+ # 逻辑分支
394
+ if song['source_type'] == 'local':
395
+ # 本地文件直接返回
396
+ path = Path(song['file_path'])
397
+ if not path.exists(): raise HTTPException(404)
398
+ return FileResponse(path)
399
 
400
+ elif song['source_type'] == 'dataset':
401
+ # 远程文件:下载到本地缓存,然后返回
402
  try:
403
+ # 使用 hf_hub_download 下载单文件
404
+ local_path = hf_hub_download(
405
+ repo_id=config.DATASET_REPO_ID,
406
+ filename=song['file_path'],
407
+ repo_type="dataset",
408
+ cache_dir=str(config.DATASET_CACHE_DIR),
409
+ resume_download=True
410
+ )
411
+ return FileResponse(local_path)
412
+ except Exception as e:
413
+ raise HTTPException(500, detail=f"Dataset 下载失败: {e}")
 
 
414
 
415
  # --- 其他 API ---
416
+ @app.get("/api/scan")
417
+ async def scan():
418
+ return JSONResponse(Database.scan_dataset_files())
419
+
420
  @app.get("/api/music")
421
  async def get_music(limit: int = 100):
422
+ return Database.get_all_songs(limit)
 
 
 
 
 
423
 
424
+ @app.post("/api/upload")
425
+ async def upload(file: UploadFile = File(...)):
426
+ dest = config.UPLOAD_DIR / file.filename
427
+ with open(dest, "wb") as f:
428
+ shutil.copyfileobj(file.file, f)
429
+
430
+ # 简单插入数据库
431
+ Database.scan_dataset_files() # 重新扫描以更新列表
432
+ return JSONResponse({"status": "success", "filename": file.filename})