"""Briefing API: RSS feeds, weather, and briefing configuration.""" import asyncio import json import logging from quart import Blueprint, g, jsonify, request from sqlalchemy import select from fabledassistant.auth import login_required from fabledassistant.models import async_session from fabledassistant.models.rss_feed import RssFeed from fabledassistant.services import rss as rss_svc from fabledassistant.services import weather as weather_svc from fabledassistant.services.settings import get_setting, set_settings_batch logger = logging.getLogger(__name__) briefing_bp = Blueprint("briefing", __name__, url_prefix="/api/briefing") _REQUIRE = login_required # ── Config ──────────────────────────────────────────────────────────────────── @briefing_bp.route("/config", methods=["GET"]) @_REQUIRE async def get_config(): raw = await get_setting(g.user.id, "briefing_config", "{}") try: config = json.loads(raw) if isinstance(raw, str) else (raw or {}) except Exception: config = {} return jsonify(config) @briefing_bp.route("/config", methods=["PUT"]) @_REQUIRE async def put_config(): data = await request.get_json() await set_settings_batch(g.user.id, {"briefing_config": json.dumps(data)}) return jsonify({"ok": True}) # ── RSS Feeds ───────────────────────────────────────────────────────────────── @briefing_bp.route("/feeds", methods=["GET"]) @_REQUIRE async def list_feeds(): async with async_session() as session: result = await session.execute( select(RssFeed).where(RssFeed.user_id == g.user.id).order_by(RssFeed.id) ) feeds = list(result.scalars().all()) return jsonify([f.to_dict() for f in feeds]) @briefing_bp.route("/feeds", methods=["POST"]) @_REQUIRE async def add_feed(): data = await request.get_json() url = (data.get("url") or "").strip() if not url: return jsonify({"error": "url required"}), 400 category = data.get("category") or None async with async_session() as session: # Prevent duplicates existing = await session.execute( select(RssFeed).where(RssFeed.user_id == g.user.id, RssFeed.url == url) ) if existing.scalars().first(): return jsonify({"error": "Feed already added"}), 409 feed = RssFeed(user_id=g.user.id, url=url, title="", category=category) session.add(feed) await session.commit() await session.refresh(feed) feed_id = feed.id # Fetch in background to auto-populate title and get initial items asyncio.create_task(rss_svc.fetch_and_cache_feed(feed_id, url)) return jsonify({"id": feed_id, "url": url, "title": "", "category": category}), 201 @briefing_bp.route("/feeds/", methods=["DELETE"]) @_REQUIRE async def delete_feed(feed_id: int): async with async_session() as session: result = await session.execute( select(RssFeed).where(RssFeed.id == feed_id, RssFeed.user_id == g.user.id) ) feed = result.scalars().first() if not feed: return jsonify({"error": "Not found"}), 404 await session.delete(feed) await session.commit() return jsonify({"ok": True}) @briefing_bp.route("/feeds/refresh", methods=["POST"]) @_REQUIRE async def refresh_feeds(): results = await rss_svc.refresh_all_feeds(g.user.id) total_new = sum(results.values()) return jsonify({"feeds_refreshed": len(results), "new_items": total_new}) @briefing_bp.route("/feeds/recent", methods=["GET"]) @_REQUIRE async def recent_items(): limit = min(int(request.args.get("limit", 20)), 100) items = await rss_svc.get_recent_items(g.user.id, limit=limit) return jsonify({"items": items}) # ── Weather ─────────────────────────────────────────────────────────────────── @briefing_bp.route("/weather", methods=["GET"]) @_REQUIRE async def get_weather(): data = await weather_svc.get_cached_weather(g.user.id) return jsonify({"locations": data}) @briefing_bp.route("/weather/geocode", methods=["POST"]) @_REQUIRE async def geocode_location(): data = await request.get_json() query = (data.get("query") or "").strip() if not query: return jsonify({"error": "query required"}), 400 try: lat, lon, label = await weather_svc.geocode(query) return jsonify({"lat": lat, "lon": lon, "label": label}) except ValueError as e: return jsonify({"error": str(e)}), 404 @briefing_bp.route("/weather/refresh", methods=["POST"]) @_REQUIRE async def refresh_weather(): raw = await get_setting(g.user.id, "briefing_config", "{}") try: config = json.loads(raw) if isinstance(raw, str) else {} except Exception: config = {} locations = config.get("locations", {}) refreshed = [] for key, loc in locations.items(): if not loc.get("lat") or not loc.get("lon"): continue try: await weather_svc.refresh_location_cache( user_id=g.user.id, location_key=key, location_label=loc.get("label", key), lat=loc["lat"], lon=loc["lon"], ) refreshed.append(key) except Exception: logger.warning("Failed to refresh weather for %s", key, exc_info=True) return jsonify({"refreshed": refreshed})