diff --git a/.gitignore b/.gitignore index 273c8df..806ef83 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,3 @@ __pycache__/ *.py[cod] .venv/ venv/ - -# Local test output -feeds/ -test_*/ diff --git a/Dockerfile b/Dockerfile index f5bff4f..5537b93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,38 @@ # ───────────────────────────────────────────── -# HuggingFace Daily Paper RSS — Dockerfile +# HuggingFace Daily Paper RSS — EasyPanel-ready Dockerfile # ───────────────────────────────────────────── -# Two modes: -# 1. Run-once (default, for cron): -# docker run --rm -v ./feeds:/data hf-paper-rss -# 2. HTTP server (serve feeds): -# docker run --rm -p 8080:8080 -v ./feeds:/data hf-paper-rss serve +# Single container that: +# 1. Generates RSS feeds on startup +# 2. Serves them via HTTP (port 8080, CORS enabled) +# 3. Auto-refreshes every 24h (background thread) +# +# Deploy on EasyPanel: health check + persistent volume # ───────────────────────────────────────────── -FROM python:3.11-slim AS base +FROM python:3.11-slim WORKDIR /app -# Install system deps +# System deps RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Install Python deps +# Python deps COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy app code -COPY parser.py rss_generator.py run.py run.sh ./ +# App code +COPY parser.py rss_generator.py run.py server.py ./ -# Create output directory -RUN mkdir -p /data && chmod 777 /data +# Volume for persistent feed output +VOLUME ["/data"] -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD python -c "import sys; sys.exit(0 if __import__('os').path.exists('/data/feed.xml') else 1)" \ - || true # Allow healthcheck to be optional - -ENTRYPOINT ["python", "run.py"] -CMD ["--output-dir", "/data"] - - -# ─── Serve mode ─── -FROM base AS serve - -COPY server.py . +# Health check — verify feed.xml exists (generated on startup) +HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \ + CMD python -c "import os; exit(0 if os.path.exists('/data/feed.xml') else 1)" EXPOSE 8080 ENTRYPOINT ["python", "server.py"] -CMD ["--dir", "/data", "--port", "8080"] +CMD ["--dir", "/data", "--port", "8080", "--refresh", "24"] diff --git a/cron_update.sh b/cron_update.sh deleted file mode 100644 index a0b5067..0000000 --- a/cron_update.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ───────────────────────────────────────────── -# cron_update.sh — run daily via cron -# ───────────────────────────────────────────── -# 1. Generate RSS feeds -# 2. Commit & push updated feeds to Gitea -# -# Designed for Hermes cron or system cron. -# ───────────────────────────────────────────── - -REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$REPO_DIR" - -# Setup Python -PYTHON=$(command -v python3 || command -v python) - -# Try venv first, fallback to system python -if [[ -f .venv/bin/python ]]; then - PYTHON=".venv/bin/python" -fi - -# Verify deps installed -echo "[cron] Using: $PYTHON ($($PYTHON --version))" - -# Generate feeds (save in repo root — Gitea raw URL serves directly) -echo "[cron] Generating RSS feeds..." -$PYTHON run.py --output-dir "$REPO_DIR" - -# Git commit & push if feed files changed -cd "$REPO_DIR" -CHANGED=false -for f in feed.xml feed_weekly.xml feed_monthly.xml; do - if [[ -f "$f" ]]; then - git add "$f" && CHANGED=true - fi -done - -if $CHANGED && ! git diff --cached --quiet; then - echo "[cron] Changes detected — committing and pushing..." - git commit -m "bot: update RSS feeds $(date '+%Y-%m-%d')" - git push origin main - echo "[cron] ✅ Pushed updated feeds to Gitea." -else - echo "[cron] No changes to feeds." -fi - -echo "[cron] Done." diff --git a/server.py b/server.py index 412c61b..38f1d45 100644 --- a/server.py +++ b/server.py @@ -1,26 +1,80 @@ #!/usr/bin/env python3 """ -Minimal HTTP server to serve generated RSS feed files. +RSS Feed HTTP Server — generates feeds on startup + serves them via HTTP. +For EasyPanel deployment: single entrypoint that handles everything. Usage: - python server.py # port 8080, dir ./feeds + python server.py # port 8080, dir /data python server.py --dir /data --port 8080 + python server.py --dir /data --port 8080 --refresh 24 """ import argparse -import http.server -import os -import sys import datetime +import http.server +import logging +import os +import subprocess +import sys +import threading +import time +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger("hf-rss") + + +# ── Feed generation ── + +def generate_feeds(output_dir: str): + """Run run.py to generate RSS feeds.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + run_py = os.path.join(script_dir, "run.py") + python = sys.executable + + logger.info("Generating RSS feeds...") + try: + result = subprocess.run( + [python, run_py, "--output-dir", output_dir], + capture_output=True, text=True, timeout=60, + cwd=script_dir, + ) + if result.returncode == 0: + logger.info(f"Feeds generated successfully in {output_dir}") + for f in sorted(os.listdir(output_dir)): + if f.endswith(".xml"): + fpath = os.path.join(output_dir, f) + size = os.path.getsize(fpath) + logger.info(f" {f} ({size:,} bytes)") + else: + logger.error(f"Feed generation failed (exit {result.returncode})") + logger.error(result.stderr[:500] if result.stderr else result.stdout[:500]) + except Exception as e: + logger.error(f"Feed generation error: {e}") + + +def background_refresh(output_dir: str, interval_hours: int): + """Regenerate feeds periodically.""" + if interval_hours <= 0: + return + interval_seconds = interval_hours * 3600 + logger.info(f"Background refresh every {interval_hours}h (next in {interval_hours}h)") + while True: + time.sleep(interval_seconds) + generate_feeds(output_dir) + + +# ── HTTP Server ── class RSSFeedHandler(http.server.SimpleHTTPRequestHandler): - """Custom handler with CORS headers and auto-refresh.""" + """Custom handler with CORS headers.""" def __init__(self, *args, directory=None, **kwargs): super().__init__(*args, directory=directory, **kwargs) def end_headers(self): - # CORS — allow RSS readers to fetch from any origin self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") @@ -32,49 +86,52 @@ class RSSFeedHandler(http.server.SimpleHTTPRequestHandler): self.send_response(204) self.end_headers() - def log_message(self, format, *args): - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - sys.stderr.write(f"[{timestamp}] {args[0]} {args[1]} {args[2]}\n") +# ── Main ── def main(): - parser = argparse.ArgumentParser(description="RSS Feed HTTP Server") - parser.add_argument("--dir", default=os.path.join(os.getcwd(), "feeds"), - help="Directory containing RSS feeds") + parser = argparse.ArgumentParser(description="HF Daily Paper RSS HTTP Server") + parser.add_argument("--dir", default="/data", + help="Feed output directory (default: /data)") parser.add_argument("--port", type=int, default=8080, - help="Port to serve on") + help="HTTP server port (default: 8080)") parser.add_argument("--host", default="0.0.0.0", - help="Host to bind to") + help="Bind address (default: 0.0.0.0)") + parser.add_argument("--refresh", type=int, default=24, + help="Auto-refresh interval in hours (0=disabled, default: 24)") args = parser.parse_args() feed_dir = os.path.abspath(args.dir) - if not os.path.exists(feed_dir): - print(f"[server.py] Creating feed directory: {feed_dir}") - os.makedirs(feed_dir, exist_ok=True) + os.makedirs(feed_dir, exist_ok=True) - # Ensure at least one feed exists - feeds = sorted(f for f in os.listdir(feed_dir) if f.endswith(".xml")) - if feeds: - print(f"[server.py] Found {len(feeds)} feed(s): {', '.join(feeds)}") - else: - print(f"[server.py] No feeds yet in {feed_dir}. Run 'run.py' first or wait for cron.") + # Generate feeds on startup + generate_feeds(feed_dir) + # Start background refresh thread + if args.refresh > 0: + t = threading.Thread( + target=background_refresh, + args=(feed_dir, args.refresh), + daemon=True, + ) + t.start() + + # Start HTTP server class Handler(RSSFeedHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=feed_dir, **kwargs) server = http.server.HTTPServer((args.host, args.port), Handler) - print(f"[server.py] Serving RSS feeds from {feed_dir}") - print(f"[server.py] Listening on http://{args.host}:{args.port}") - print(f"[server.py] Feed URLs:") - print(f" http://localhost:{args.port}/feed.xml (daily)") - print(f" http://localhost:{args.port}/feed_weekly.xml (weekly)") - print(f" http://localhost:{args.port}/feed_monthly.xml (monthly)") + logger.info(f"Serving RSS feeds from {feed_dir}") + logger.info(f"Listening on http://{args.host}:{args.port}") + logger.info(f" GET /feed.xml (daily)") + logger.info(f" GET /feed_weekly.xml (weekly, Mondays)") + logger.info(f" GET /feed_monthly.xml (monthly, 1st)") try: server.serve_forever() except KeyboardInterrupt: - print("\n[server.py] Shutting down...") + logger.info("Shutting down...") server.server_close()