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>
128 lines
3.2 KiB
Python
128 lines
3.2 KiB
Python
"""Feed management API — CRUD on feeds.yaml."""
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import yaml
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from ..config import FEEDS_CONFIG
|
|
from ..db import get_db
|
|
|
|
bp = Blueprint("feeds", __name__)
|
|
|
|
|
|
def _read_feeds():
|
|
if not FEEDS_CONFIG.exists():
|
|
return []
|
|
with open(FEEDS_CONFIG) as f:
|
|
data = yaml.safe_load(f) or {}
|
|
return data.get("feeds", [])
|
|
|
|
|
|
def _write_feeds(feeds):
|
|
data = {"feeds": feeds}
|
|
# Atomic write: temp file then rename
|
|
fd, tmp_path = tempfile.mkstemp(
|
|
dir=str(FEEDS_CONFIG.parent), suffix=".yaml.tmp"
|
|
)
|
|
try:
|
|
with os.fdopen(fd, "w") as f:
|
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
os.replace(tmp_path, str(FEEDS_CONFIG))
|
|
except Exception:
|
|
os.unlink(tmp_path)
|
|
raise
|
|
|
|
|
|
@bp.route("/feeds")
|
|
def list_feeds():
|
|
feeds = _read_feeds()
|
|
db = get_db()
|
|
|
|
result = []
|
|
for feed in feeds:
|
|
name = feed.get("name", "")
|
|
state = db.execute(
|
|
"SELECT last_poll FROM feed_state WHERE feed_name=?", (name,)
|
|
).fetchone()
|
|
ep_count = db.execute(
|
|
"SELECT COUNT(*) as c FROM episodes WHERE feed_name=?", (name,)
|
|
).fetchone()["c"]
|
|
|
|
result.append({
|
|
**feed,
|
|
"last_poll": state["last_poll"] if state else None,
|
|
"episode_count": ep_count,
|
|
})
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
@bp.route("/feeds", methods=["POST"])
|
|
def add_feed():
|
|
data = request.get_json()
|
|
if not data or not data.get("name") or not data.get("url"):
|
|
return jsonify({"error": "name and url are required"}), 400
|
|
|
|
feeds = _read_feeds()
|
|
|
|
# Check for duplicate name
|
|
for f in feeds:
|
|
if f.get("name") == data["name"]:
|
|
return jsonify({"error": "Feed with this name already exists"}), 409
|
|
|
|
new_feed = {
|
|
"name": data["name"],
|
|
"url": data["url"],
|
|
"enabled": data.get("enabled", True),
|
|
"priority": data.get("priority", 10),
|
|
"max_episodes": data.get("max_episodes", 20),
|
|
}
|
|
|
|
feeds.append(new_feed)
|
|
_write_feeds(feeds)
|
|
|
|
return jsonify(new_feed), 201
|
|
|
|
|
|
@bp.route("/feeds/<name>", methods=["PUT"])
|
|
def update_feed(name):
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "JSON body required"}), 400
|
|
|
|
feeds = _read_feeds()
|
|
found = False
|
|
|
|
for feed in feeds:
|
|
if feed.get("name") == name:
|
|
if "enabled" in data:
|
|
feed["enabled"] = data["enabled"]
|
|
if "url" in data:
|
|
feed["url"] = data["url"]
|
|
if "priority" in data:
|
|
feed["priority"] = data["priority"]
|
|
if "max_episodes" in data:
|
|
feed["max_episodes"] = data["max_episodes"]
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
return jsonify({"error": "Feed not found"}), 404
|
|
|
|
_write_feeds(feeds)
|
|
return jsonify({"status": "updated"})
|
|
|
|
|
|
@bp.route("/feeds/<name>", methods=["DELETE"])
|
|
def delete_feed(name):
|
|
feeds = _read_feeds()
|
|
new_feeds = [f for f in feeds if f.get("name") != name]
|
|
|
|
if len(new_feeds) == len(feeds):
|
|
return jsonify({"error": "Feed not found"}), 404
|
|
|
|
_write_feeds(new_feeds)
|
|
return jsonify({"status": "deleted"})
|