clean: remove Hermes cron artifacts, simplify for EasyPanel deploy
Some checks failed
Some checks failed
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,7 +3,3 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
# Local test output
|
|
||||||
feeds/
|
|
||||||
test_*/
|
|
||||||
|
|||||||
45
Dockerfile
45
Dockerfile
@@ -1,47 +1,38 @@
|
|||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# HuggingFace Daily Paper RSS — Dockerfile
|
# HuggingFace Daily Paper RSS — EasyPanel-ready Dockerfile
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# Two modes:
|
# Single container that:
|
||||||
# 1. Run-once (default, for cron):
|
# 1. Generates RSS feeds on startup
|
||||||
# docker run --rm -v ./feeds:/data hf-paper-rss
|
# 2. Serves them via HTTP (port 8080, CORS enabled)
|
||||||
# 2. HTTP server (serve feeds):
|
# 3. Auto-refreshes every 24h (background thread)
|
||||||
# docker run --rm -p 8080:8080 -v ./feeds:/data hf-paper-rss serve
|
#
|
||||||
|
# Deploy on EasyPanel: health check + persistent volume
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
FROM python:3.11-slim AS base
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system deps
|
# System deps
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Python deps
|
# Python deps
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy app code
|
# App code
|
||||||
COPY parser.py rss_generator.py run.py run.sh ./
|
COPY parser.py rss_generator.py run.py server.py ./
|
||||||
|
|
||||||
# Create output directory
|
# Volume for persistent feed output
|
||||||
RUN mkdir -p /data && chmod 777 /data
|
VOLUME ["/data"]
|
||||||
|
|
||||||
# Health check
|
# Health check — verify feed.xml exists (generated on startup)
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \
|
||||||
CMD python -c "import sys; sys.exit(0 if __import__('os').path.exists('/data/feed.xml') else 1)" \
|
CMD python -c "import os; exit(0 if 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 .
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["python", "server.py"]
|
ENTRYPOINT ["python", "server.py"]
|
||||||
CMD ["--dir", "/data", "--port", "8080"]
|
CMD ["--dir", "/data", "--port", "8080", "--refresh", "24"]
|
||||||
|
|||||||
@@ -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."
|
|
||||||
119
server.py
119
server.py
@@ -1,26 +1,80 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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:
|
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
|
||||||
|
python server.py --dir /data --port 8080 --refresh 24
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import http.server
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import datetime
|
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):
|
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):
|
def __init__(self, *args, directory=None, **kwargs):
|
||||||
super().__init__(*args, directory=directory, **kwargs)
|
super().__init__(*args, directory=directory, **kwargs)
|
||||||
|
|
||||||
def end_headers(self):
|
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-Origin", "*")
|
||||||
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||||
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
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.send_response(204)
|
||||||
self.end_headers()
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="RSS Feed HTTP Server")
|
parser = argparse.ArgumentParser(description="HF Daily Paper RSS HTTP Server")
|
||||||
parser.add_argument("--dir", default=os.path.join(os.getcwd(), "feeds"),
|
parser.add_argument("--dir", default="/data",
|
||||||
help="Directory containing RSS feeds")
|
help="Feed output directory (default: /data)")
|
||||||
parser.add_argument("--port", type=int, default=8080,
|
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",
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
feed_dir = os.path.abspath(args.dir)
|
feed_dir = os.path.abspath(args.dir)
|
||||||
if not os.path.exists(feed_dir):
|
os.makedirs(feed_dir, exist_ok=True)
|
||||||
print(f"[server.py] Creating feed directory: {feed_dir}")
|
|
||||||
os.makedirs(feed_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Ensure at least one feed exists
|
# Generate feeds on startup
|
||||||
feeds = sorted(f for f in os.listdir(feed_dir) if f.endswith(".xml"))
|
generate_feeds(feed_dir)
|
||||||
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.")
|
|
||||||
|
|
||||||
|
# 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):
|
class Handler(RSSFeedHandler):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, directory=feed_dir, **kwargs)
|
super().__init__(*args, directory=feed_dir, **kwargs)
|
||||||
|
|
||||||
server = http.server.HTTPServer((args.host, args.port), Handler)
|
server = http.server.HTTPServer((args.host, args.port), Handler)
|
||||||
print(f"[server.py] Serving RSS feeds from {feed_dir}")
|
logger.info(f"Serving RSS feeds from {feed_dir}")
|
||||||
print(f"[server.py] Listening on http://{args.host}:{args.port}")
|
logger.info(f"Listening on http://{args.host}:{args.port}")
|
||||||
print(f"[server.py] Feed URLs:")
|
logger.info(f" GET /feed.xml (daily)")
|
||||||
print(f" http://localhost:{args.port}/feed.xml (daily)")
|
logger.info(f" GET /feed_weekly.xml (weekly, Mondays)")
|
||||||
print(f" http://localhost:{args.port}/feed_weekly.xml (weekly)")
|
logger.info(f" GET /feed_monthly.xml (monthly, 1st)")
|
||||||
print(f" http://localhost:{args.port}/feed_monthly.xml (monthly)")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n[server.py] Shutting down...")
|
logger.info("Shutting down...")
|
||||||
server.server_close()
|
server.server_close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user