#!/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 """