#!/usr/bin/env python3 import os import sys import time import re import xml.etree.ElementTree as ET from urllib.parse import urlparse from bs4 import BeautifulSoup from curl_cffi import requests TARGET_URL = "https://xkcd.com/rss.xml" DOWNLOAD_DIR = "/mnt/archie/Pictures/Editorial Cartoons/xkcd" MAX_STORAGE_DAYS = 30 # ========================================== # PROXY CONFIGURATION (SINGLE PROXY) # ========================================== # Set to None to disable proxy and connect directly # Format: {"http": "http://HOSTNAME:PORT", "https": "http://HOSTNAME:PORT"} # Example: {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} PROXY = {"http": "http://proxydrum:8080"} def enforce_retention_policy(): print(f"Running automated retention validation sweep inside {DOWNLOAD_DIR}...", flush=True) now = time.time() cutoff = now - (MAX_STORAGE_DAYS * 86400) try: purged_count = 0 for entry in os.scandir(DOWNLOAD_DIR): if entry.is_file(): file_modification_time = entry.stat().st_mtime if file_modification_time < cutoff: os.remove(entry.path) purged_count += 1 if purged_count > 0: print(f"[Cleanup Success]: Purged {purged_count} legacy assets older than {MAX_STORAGE_DAYS} days.", flush=True) except Exception as e: print(f"[Warning]: Automated file retention sweep encountered an issue: {e}", file=sys.stderr, flush=True) def scrape_xkcd_rss(): print("=== STARTING PURE PYTHON RSS EXTRACTION RUN ===", flush=True) print(f"[TIME] {time.strftime('%Y-%m-%d %H:%M:%S')}", flush=True) if PROXY: print(f"[PROXY]: {PROXY.get('http', 'N/A')}", flush=True) else: print(f"[PROXY]: None (direct connection)", flush=True) if not os.path.exists(DOWNLOAD_DIR): print(f"Creating target storage directory tree structure at: {DOWNLOAD_DIR}", flush=True) os.makedirs(DOWNLOAD_DIR, exist_ok=True) browser_headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } response = None print("Fetching RSS feed...", flush=True) try: if PROXY: res = requests.get(TARGET_URL, impersonate="chrome120", headers=browser_headers, proxies=PROXY, timeout=15) else: res = requests.get(TARGET_URL, impersonate="chrome120", headers=browser_headers, timeout=15) if res.status_code == 200: response = res print("Successfully fetched RSS raw file payload.", flush=True) else: print(f" -> Status: {res.status_code} (Rejected)", flush=True) except Exception as e: print(f" -> Connection dropped / Timeout: {e}", flush=True) if response is None: print("[CRITICAL FATAL ERROR]: Could not fetch RSS feed.", file=sys.stderr, flush=True) return try: root = ET.fromstring(response.content) items = root.findall('.//item') print(f"Detected {len(items)} items inside the RSS feed description structure.", flush=True) new_downloads_count = 0 for index, item in enumerate(items): print(f"\nProcessing entry [{index + 1}/{len(items)}]...", flush=True) description_node = item.find('description') if description_node is None or not description_node.text: print(" -> [Parse Warning]: Target entry missing clean description markup string content. Skipping.", flush=True) continue soup = BeautifulSoup(description_node.text, 'html.parser') img_element = soup.find('img') if not img_element: print(" -> [Parse Warning]: Description block found but missing raw HTML img tag context element. Skipping.", flush=True) continue img_url = img_element.get('src') if not img_url: print(" -> [Parse Warning]: Could not parse out a clean target image source web link string. Skipping.", flush=True) continue if img_url.startswith('//'): img_url = 'https:' + img_url parsed_url = urlparse(img_url) filename = os.path.basename(parsed_url.path) if not filename: continue save_path = os.path.join(DOWNLOAD_DIR, filename) if os.path.exists(save_path): print(f" -> [SKIPPED]: File locally present: \"{filename}\".", flush=True) continue print(f" -> [DISCOVERY]: Fetching binary asset: {img_url}", flush=True) download_success = False try: if PROXY: img_response = requests.get(img_url, impersonate="chrome120", headers=browser_headers, proxies=PROXY, timeout=20) else: img_response = requests.get(img_url, impersonate="chrome120", headers=browser_headers, timeout=20) if img_response.status_code == 200: with open(save_path, "wb") as file_writer: file_writer.write(img_response.content) download_success = True else: print(f" -> Download failed (Status: {img_response.status_code}).", flush=True) except Exception as e: print(f" -> Download timed out/dropped: {e}", flush=True) if download_success: print(f" -> [SUCCESS]: Archived target locally -> {save_path}", flush=True) new_downloads_count += 1 else: print(f" -> [DOWNLOAD ERROR]: Skipping this image.", file=sys.stderr, flush=True) print(f"\nProcessing queue empty. Total new assets written to storage disk: {new_downloads_count}", flush=True) enforce_retention_policy() except Exception as error: print(f"\n[UNHANDLED RUNTIME TRACKER EXCEPTION]: {str(error)}", file=sys.stderr, flush=True) if __name__ == "__main__": scrape_xkcd_rss()