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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Queue management API — GET/POST/DELETE /api/queue."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from ..config import QUEUE_DIR, PODCAST_DIR
|
||||
from ..db import get_db
|
||||
|
||||
bp = Blueprint("queue", __name__)
|
||||
|
||||
|
||||
@bp.route("/queue")
|
||||
def list_queue():
|
||||
db = get_db()
|
||||
rows = db.execute("""
|
||||
SELECT q.id, q.position, q.enqueued, q.played,
|
||||
e.id as episode_id, e.feed_name, e.title, e.pub_date, e.file_path
|
||||
FROM queue q
|
||||
JOIN episodes e ON e.id = q.episode_id
|
||||
WHERE q.played = 0
|
||||
ORDER BY q.position
|
||||
""").fetchall()
|
||||
|
||||
return jsonify([dict(r) for r in rows])
|
||||
|
||||
|
||||
@bp.route("/queue", methods=["POST"])
|
||||
def enqueue():
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "JSON body required"}), 400
|
||||
|
||||
episode_id = data.get("episode_id")
|
||||
file_path = data.get("file_path")
|
||||
|
||||
db = get_db()
|
||||
|
||||
if episode_id:
|
||||
row = db.execute(
|
||||
"SELECT * FROM episodes WHERE id=? AND downloaded=1", (episode_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "Episode not found or not downloaded"}), 404
|
||||
file_path = row["file_path"]
|
||||
elif file_path:
|
||||
if not os.path.exists(file_path):
|
||||
return jsonify({"error": "File not found"}), 404
|
||||
else:
|
||||
return jsonify({"error": "Provide episode_id or file_path"}), 400
|
||||
|
||||
# Create queue entry if episode_id is available
|
||||
if episode_id:
|
||||
existing = db.execute(
|
||||
"SELECT 1 FROM queue WHERE episode_id=? AND played=0", (episode_id,)
|
||||
).fetchone()
|
||||
if existing:
|
||||
return jsonify({"error": "Episode already in queue"}), 409
|
||||
|
||||
next_pos = db.execute(
|
||||
"SELECT COALESCE(MAX(position), 0) + 1 as p FROM queue"
|
||||
).fetchone()["p"]
|
||||
db.execute(
|
||||
"INSERT INTO queue (episode_id, position) VALUES (?, ?)",
|
||||
(episode_id, next_pos),
|
||||
)
|
||||
db.execute(
|
||||
"UPDATE episodes SET queued=1, played=0 WHERE id=?", (episode_id,)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# Create symlink in queue directory
|
||||
QUEUE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
src = Path(file_path)
|
||||
eid = episode_id or 0
|
||||
link_name = f"{timestamp}_{eid:06d}_{src.name}"
|
||||
link_path = QUEUE_DIR / link_name
|
||||
|
||||
try:
|
||||
link_path.symlink_to(src.resolve())
|
||||
except (OSError, FileExistsError):
|
||||
try:
|
||||
link_path.hardlink_to(src.resolve())
|
||||
except (OSError, FileExistsError):
|
||||
shutil.copy2(str(src), str(link_path))
|
||||
|
||||
return jsonify({"status": "enqueued", "link": link_name}), 201
|
||||
|
||||
|
||||
@bp.route("/queue/<int:queue_id>", methods=["DELETE"])
|
||||
def dequeue(queue_id):
|
||||
db = get_db()
|
||||
|
||||
row = db.execute(
|
||||
"SELECT q.*, e.id as eid FROM queue q JOIN episodes e ON e.id=q.episode_id WHERE q.id=?",
|
||||
(queue_id,),
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({"error": "Queue item not found"}), 404
|
||||
|
||||
# Remove symlink from queue directory
|
||||
eid = row["eid"]
|
||||
if QUEUE_DIR.exists():
|
||||
pattern = re.compile(rf"_0*{eid}_")
|
||||
for item in QUEUE_DIR.iterdir():
|
||||
if pattern.search(item.name):
|
||||
item.unlink(missing_ok=True)
|
||||
break
|
||||
|
||||
db.execute("DELETE FROM queue WHERE id=?", (queue_id,))
|
||||
db.execute("UPDATE episodes SET queued=0 WHERE id=?", (eid,))
|
||||
db.commit()
|
||||
|
||||
return jsonify({"status": "removed"})
|
||||
Reference in New Issue
Block a user