Flask + React web UI with audio player, podcast queue, feed management, episode browser, music library, schedule viewer, and log tail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
3.0 KiB
Python
109 lines
3.0 KiB
Python
"""Music library API — list dirs, list files, move files between folders."""
|
|
|
|
import os
|
|
import shutil
|
|
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from ..config import MUSIC_BASE, FALLBACK_DIR
|
|
|
|
bp = Blueprint("music", __name__)
|
|
|
|
AUDIO_EXTENSIONS = {".mp3", ".flac", ".ogg", ".opus", ".m4a", ".wav"}
|
|
|
|
|
|
def _get_folders():
|
|
"""Return dict mapping folder name -> absolute path."""
|
|
folders = {}
|
|
if MUSIC_BASE.exists():
|
|
for d in sorted(MUSIC_BASE.iterdir()):
|
|
if d.is_dir():
|
|
folders[d.name] = d
|
|
folders["fallback"] = FALLBACK_DIR
|
|
return folders
|
|
|
|
|
|
def _count_audio_files(directory):
|
|
if not directory.exists():
|
|
return 0
|
|
return sum(
|
|
1 for f in directory.rglob("*")
|
|
if f.is_file() and f.suffix.lower() in AUDIO_EXTENSIONS
|
|
)
|
|
|
|
|
|
def _list_audio_files(directory):
|
|
if not directory.exists():
|
|
return []
|
|
files = []
|
|
for f in sorted(directory.iterdir()):
|
|
if f.is_file() and f.suffix.lower() in AUDIO_EXTENSIONS:
|
|
files.append({
|
|
"name": f.name,
|
|
"size": f.stat().st_size,
|
|
})
|
|
return files
|
|
|
|
|
|
@bp.route("/music")
|
|
def list_music():
|
|
dirs = []
|
|
if MUSIC_BASE.exists():
|
|
for d in sorted(MUSIC_BASE.iterdir()):
|
|
if d.is_dir():
|
|
dirs.append({
|
|
"folder": d.name,
|
|
"path": str(d),
|
|
"file_count": _count_audio_files(d),
|
|
})
|
|
|
|
fallback_count = _count_audio_files(FALLBACK_DIR)
|
|
|
|
return jsonify({
|
|
"directories": dirs,
|
|
"fallback": {
|
|
"path": str(FALLBACK_DIR),
|
|
"file_count": fallback_count,
|
|
},
|
|
})
|
|
|
|
|
|
@bp.route("/music/<folder>/files")
|
|
def list_files(folder):
|
|
folders = _get_folders()
|
|
if folder not in folders:
|
|
return jsonify({"error": "Unknown folder"}), 404
|
|
files = _list_audio_files(folders[folder])
|
|
return jsonify({"folder": folder, "files": files})
|
|
|
|
|
|
@bp.route("/music/move", methods=["POST"])
|
|
def move_file():
|
|
data = request.get_json()
|
|
src_folder = data.get("from")
|
|
dst_folder = data.get("to")
|
|
filename = data.get("file")
|
|
|
|
if not all([src_folder, dst_folder, filename]):
|
|
return jsonify({"error": "Missing from, to, or file"}), 400
|
|
|
|
folders = _get_folders()
|
|
if src_folder not in folders or dst_folder not in folders:
|
|
return jsonify({"error": "Unknown folder"}), 404
|
|
|
|
if os.sep in filename or "/" in filename or ".." in filename:
|
|
return jsonify({"error": "Invalid filename"}), 400
|
|
|
|
src_path = folders[src_folder] / filename
|
|
dst_path = folders[dst_folder] / filename
|
|
|
|
if not src_path.exists():
|
|
return jsonify({"error": "File not found"}), 404
|
|
|
|
if dst_path.exists():
|
|
return jsonify({"error": "File already exists in destination"}), 409
|
|
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.move(str(src_path), str(dst_path))
|
|
return jsonify({"ok": True, "moved": filename, "from": src_folder, "to": dst_folder})
|