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,127 @@
|
||||
"""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"})
|
||||
Reference in New Issue
Block a user