radio/web/api/music.py
profit 3d635b742c Initial commit: self-hosted personal radio station
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>
2026-03-13 19:01:33 -07:00

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})