zero / music_api.py
milk-git55's picture
Update music_api.py
c3beb5e verified
Raw
History Blame Contribute Delete
10.2 kB
#!/usr/bin/env python3
import os
import sqlite3
import shutil
from pathlib import Path
from typing import Optional, List
from fastapi import FastAPI, UploadFile, File, Query, HTTPException
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from mutagen import File as MutagenFile
from huggingface_hub import HfApi, hf_hub_download
# ==================== 配置 ====================
class Config:
APP_NAME = "Music API"
VERSION = "5.1.0"
# Dataset ID (可在 Settings -> Variables 设置 HF_DATASET_REPO_ID)
DATASET_REPO_ID = os.environ.get("HF_DATASET_REPO_ID", "my-music-library")
# 本地缓存和上传目录
CACHE_DIR = Path("/tmp/hf_cache")
UPLOAD_DIR = Path("/tmp/uploads")
DATABASE_PATH = Path("/tmp/music.db")
# 服务器配置
HOST = "0.0.0.0"
PORT = int(os.environ.get("PORT", 7860))
config = Config()
config.CACHE_DIR.mkdir(parents=True, exist_ok=True)
config.UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# ==================== 辅助函数 ====================
def get_remote_files():
try:
api = HfApi()
tree = api.list_repo_tree(repo_id=config.DATASET_REPO_ID, repo_type="dataset")
return [item.path for item in tree if item.type == "file"]
except Exception as e:
print(f"Error fetching dataset: {e}")
return []
def get_metadata(file_path: Path):
try:
audio = MutagenFile(file_path)
if not audio: return None
title = artist = album = genre = ""
duration = 0
if hasattr(audio, 'info') and audio.info.length:
duration = int(audio.info.length)
tags = {
'title': ['TIT2', '\xa9nam', 'TITLE', 'Title'],
'artist': ['TPE1', '\xa9ART', 'ARTIST', 'Artist'],
'album': ['TALB', '\xa9alb', 'ALBUM', 'Album'],
'genre': ['TCON', '\xa9gen', 'GENRE', 'Genre']
}
for k, v in tags.items():
val = None
if audio:
for key in v:
if key in audio:
val = str(audio[key][0] if isinstance(audio[key], list) else audio[key])
break
if val: locals()[k] = val
if not title: title = file_path.stem
if not artist: artist = "Unknown"
return {"title": title, "artist": artist, "album": album, "genre": genre, "duration": duration, "size": file_path.stat().st_size}
except: return None
# ==================== 数据库 ====================
class Database:
@staticmethod
def init():
conn = sqlite3.connect(config.DATABASE_PATH)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS music (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT, artist TEXT, album TEXT, genre TEXT,
duration INTEGER, file_name TEXT, source TEXT, path TEXT,
UNIQUE(file_name)
)''')
conn.commit()
conn.close()
@staticmethod
def scan():
Database.init()
remote_files = get_remote_files()
local_files = list(config.UPLOAD_DIR.glob("*.mp3")) + list(config.UPLOAD_DIR.glob("*.wav"))
conn = sqlite3.connect(config.DATABASE_PATH)
c = conn.cursor()
count = 0
# 处理远程文件
for path in remote_files:
if not path.lower().endswith(('.mp3', '.flac', '.wav', '.ogg')): continue
filename = path.split('/')[-1]
try:
c.execute("INSERT OR IGNORE INTO music (title, artist, file_name, source, path) VALUES (?,?,?,?,?)",
(Path(filename).stem, "Dataset Artist", filename, "dataset", path))
if c.rowcount > 0: count += 1
except: pass
# 处理本地文件
for path in local_files:
meta = get_metadata(path) or {}
c.execute("INSERT OR IGNORE INTO music (title, artist, file_name, source, path, duration) VALUES (?,?,?,?,?,?)",
(meta.get('title'), meta.get('artist'), path.name, "local", str(path), meta.get('duration', 0)))
if c.rowcount > 0: count += 1
conn.commit()
conn.close()
return {"added": count}
@staticmethod
def get_all(limit=100):
conn = sqlite3.connect(config.DATABASE_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM music ORDER BY id DESC LIMIT ?", (limit,))
songs = [dict(row) for row in c.fetchall()]
conn.close()
return songs
@staticmethod
def get_by_id(song_id):
conn = sqlite3.connect(config.DATABASE_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM music WHERE id = ?", (song_id,))
song = c.fetchone()
conn.close()
return dict(song) if song else None
# ==================== FastAPI ====================
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
@app.on_event("startup")
def startup(): Database.scan()
@app.get("/", response_class=HTMLResponse)
async def index():
return """
<!DOCTYPE html>
<html>
<head><title>Music Player</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 20px auto; background: #f4f4f9; padding: 20px; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
button { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
.song-list { list-style: none; padding: 0; }
.song-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.song-item:hover { background: #f9f9f9; cursor: pointer; }
.playing { color: #007bff; font-weight: bold; }
input[type="file"] { margin-bottom: 10px; }
#player { position: fixed; bottom: 0; left: 0; right: 0; background: #333; color: white; padding: 10px; display: flex; align-items: center; gap: 10px; }
</style>
</head>
<body>
<div class="card">
<h2>📚 Library Management</h2>
<button onclick="scan()">Scan Library</button>
<div id="scan-status"></div>
</div>
<div class="card">
<h2>⬆️ Upload</h2>
<input type="file" id="fileInput" accept="audio/*">
<button onclick="upload()">Upload</button>
<div id="upload-status"></div>
</div>
<div class="card">
<h2>🎵 Songs (<span id="count">0</span>)</h2>
<ul class="song-list" id="songList"></ul>
</div>
<audio id="audio" controls style="width:100%; display:none;"></audio>
<div id="player"></div>
<script>
const API = '';
async function scan() {
document.getElementById('scan-status').innerText = 'Scanning...';
await fetch(API + '/api/scan');
loadSongs();
document.getElementById('scan-status').innerText = 'Done!';
}
async function loadSongs() {
const res = await fetch(API + '/api/music?limit=100');
const songs = await res.json();
document.getElementById('count').innerText = songs.length;
const list = document.getElementById('songList');
list.innerHTML = songs.map(s => `
<li class="song-item" onclick="play(${s.id})">
<span>${s.title} - ${s.artist}</span>
<small>${s.source === 'dataset' ? '☁️' : '💾'}</small>
</li>`).join('');
}
async function play(id) {
const audio = document.getElementById('audio');
audio.src = API + '/api/play/' + id;
audio.style.display = 'block';
audio.play();
}
async function upload() {
const fileInput = document.getElementById('fileInput');
if(!fileInput.files[0]) return alert('Select file');
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const res = await fetch(API + '/api/upload', { method: 'POST', body: formData });
if(res.ok) { alert('Uploaded!'); scan(); }
else alert('Failed');
}
loadSongs();
</script>
</body>
</html>
"""
@app.get("/api/scan")
async def scan():
return Database.scan()
@app.get("/api/music")
async def get_music(limit: int = 100):
return Database.get_all(limit)
@app.get("/api/play/{song_id}")
async def play(song_id: int):
song = Database.get_by_id(song_id)
if not song: raise HTTPException(404)
if song['source'] == 'dataset':
# 下载远程文件到缓存
try:
local_path = hf_hub_download(
repo_id=config.DATASET_REPO_ID,
filename=song['path'],
repo_type="dataset",
cache_dir=str(config.CACHE_DIR),
resume_download=True
)
return FileResponse(local_path)
except Exception as e:
raise HTTPException(500, detail=str(e))
else:
# 本地文件
return FileResponse(song['path'])
@app.post("/api/upload")
async def upload(file: UploadFile = File(...)):
dest = config.UPLOAD_DIR / file.filename
with open(dest, "wb") as f: shutil.copyfileobj(file.file, f)
# 简单触发扫描
Database.scan()
return {"status": "ok"}
if __name__ == "__main__":
uvicorn.run(app, host=config.HOST, port=config.PORT)