Spaces:
Sleeping
Sleeping
| #!/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: | |
| 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() | |
| 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} | |
| 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 | |
| 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=["*"]) | |
| def startup(): Database.scan() | |
| 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> | |
| """ | |
| async def scan(): | |
| return Database.scan() | |
| async def get_music(limit: int = 100): | |
| return Database.get_all(limit) | |
| 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']) | |
| 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) | |