Files
vps-config/freshrss-fav-archiver.py
T
2026-07-26 21:37:10 +02:00

205 lines
4.7 KiB
Python

import os
import time
import sqlite3
import requests
import subprocess
import random
from urllib.parse import urlparse
random.seed()
def load_env_file(path=".env"):
if not os.path.exists(path):
return
with open(path) as f:
for line in f:
line = line.strip()
# ignore empty lines and comments
if not line or line.startswith("#"):
continue
key, value = line.split("=", 1)
os.environ.setdefault(key, value)
load_env_file()Initialize the random number generator.
If a is omitted or None, the current system time is used. If randomness sources are provided by the operating system, they are used instead of the system time (see the os.urandom() function for details on availability).
If a is an int, its absolute value is used directly.
With version 2 (the default), a str, bytes, or bytearray object gets converted to an int and all of its bits are used.
FRESHRSS_DB = os.environ["FRESHRSS_DB"]
ARCHIVE_INDEX = os.environ["ARCHIVE_INDEX"]
METUBE_URL = os.environ["METUBE_URL"]
METUBE_USER = os.environ["METUBE_USER"]
METUBE_PASS = os.environ["METUBE_PASS"]
NTFY_URL = os.environ["NTFY_URL"]
NTFY_TOKEN = os.environ["NTFY_TOKEN"]
DOCKER_COMPOSE_DIR = os.environ["DOCKER_COMPOSE_DIR"]
VIDEO_HOSTS = {
"youtube.com",
"www.youtube.com",
"youtu.be",
"vimeo.com",
"www.vimeo.com",
"dailymotion.com",
"www.dailymotion.com",
}
print("=== FreshRSS archiver starting ===")
print("Working directory:", os.getcwd())
print("Python:", os.sys.executable)
print("FreshRSS DB:", FRESHRSS_DB)
print("Archive index:", ARCHIVE_INDEX)
def get_favorites():
conn = sqlite3.connect(FRESHRSS_DB)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("""
SELECT title, link
FROM entry
WHERE is_favorite = 1
ORDER BY date DESC
""")
result = [
{
"title": row["title"],
"link": row["link"]
}
for row in cur.fetchall()
]
conn.close()
return result
def load_index():
if not os.path.exists(ARCHIVE_INDEX):
return set()
with open(ARCHIVE_INDEX, "r") as f:
return set(line.strip() for line in f if line.strip())
def save_to_index(url):
with open(ARCHIVE_INDEX, "a") as f:
f.write(url + "\n")
def is_video_url(url):
host = urlparse(url).netloc.lower()
return any(host.endswith(site) for site in VIDEO_HOSTS)
def send_to_metube(url):
"""
MeTube API endpoint:
POST /add
"""
try:
r = requests.post(
f"{METUBE_URL}/add",
json={
"url": url
},
auth=(METUBE_USER, METUBE_PASS),
timeout=30
)
return r.status_code in range(200, 300)
except Exception as e:
print("MeTube error:", e)
return (False, e)
def send_to_archivebox(url):
try:
r = subprocess.run(
[
"docker",
"compose",
"-f",
f"{DOCKER_COMPOSE_DIR}/compose.yml",
"run",
"-T",
"archivebox",
"archivebox",
"add",
url,
],
capture_output=True,
text=True,
timeout=300
)
if r.returncode != 0:
print("ArchiveBox error:")
print(r.stderr)
return (False, r.stderr)
print(r.stdout)
return True
except Exception as e:
print("ArchiveBox error:", e)
return False
def notify_failure(url, text):
try:
requests.post(
NTFY_URL + "/Alerts",
data=f"Failed to archive {url}\n{text}",
auth=("", NTFY_TOKEN),
timeout=10
)
except Exception as e:
print("ntfy error:", e)
def process():
indexed = load_index()
favorites = get_favorites()
for article in favorites:
wasVideo = False
url = article["link"]
if url in indexed:
continue
print("Archiving:", url)
if is_video_url(url):
success, errorMSG = send_to_metube(url)
wasVideo = true
else:
success, errorMSG = send_to_archivebox(url)
if success:
print("Success")
save_to_index(url)
if wasVideo:
time.sleep(random.randrange(1200, 3600)) # YouTube/Google is much more vigilant about scraper. Even if we use a burner google account, we wouldn't want it banned.
else:
print(f"Failed\n{errorMSG}")
notify_failure(url, errorMSG)
# avoid hammering services
time.sleep(60)
if __name__ == "__main__":
process()