Spaces:
Sleeping
Sleeping
Update music_api.py
Browse files- music_api.py +143 -207
music_api.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
修复版音乐API -
|
| 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,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.
|
| 26 |
|
| 27 |
BASE_DIR = Path(__file__).resolve().parent
|
| 28 |
|
| 29 |
-
# =====
|
| 30 |
-
#
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
# =====
|
| 34 |
-
#
|
| 35 |
-
#
|
| 36 |
-
|
| 37 |
|
| 38 |
-
#
|
| 39 |
-
|
| 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
|
| 72 |
-
"""
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 148 |
-
"""添加一首歌到数据库"""
|
| 149 |
conn = Database.get_connection()
|
| 150 |
cursor = conn.cursor()
|
| 151 |
try:
|
| 152 |
cursor.execute('''
|
| 153 |
-
INSERT INTO music
|
| 154 |
-
|
| 155 |
-
|
|
|
|
| 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
|
| 166 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 167 |
added_count = 0
|
| 168 |
skipped_count = 0
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
conn = Database.get_connection()
|
| 171 |
cursor = conn.cursor()
|
| 172 |
|
| 173 |
-
#
|
| 174 |
-
for
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"扫描完成
|
| 208 |
|
| 209 |
@staticmethod
|
| 210 |
-
def get_all_songs(limit
|
| 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)
|
| 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 |
-
|
| 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> -
|
| 351 |
-
<small>File: ${s.file_name} | Size:
|
| 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 |
-
# ---
|
| 377 |
-
@app.get("/
|
| 378 |
-
async def
|
| 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 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
try:
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
"message": "上传成功"
|
| 477 |
-
})
|
| 478 |
|
| 479 |
# --- 其他 API ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
@app.get("/api/music")
|
| 481 |
async def get_music(limit: int = 100):
|
| 482 |
-
|
| 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.
|
| 490 |
-
async def
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
|
|
|
|
|
| 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})
|