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>
121 lines
3.5 KiB
Python
121 lines
3.5 KiB
Python
"""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"})
|