#!/usr/bin/env python3 import os import sys import time import re from urllib.parse import urlparse from bs4 import BeautifulSoup from curl_cffi import requests TARGET_URL = "https://cagle.com/cartoons/" DOWNLOAD_DIR = "/mnt/archie/Pictures/Editorial Cartoons/Cagle" 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 cartoon 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_latest_cartoon(): print("=== STARTING PURE PYTHON MULTI-IMAGE 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,image/apng,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Sec-Ch-Ua": '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"', "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "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 target index page...", 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 target index page.", 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 target index page.", file=sys.stderr, flush=True) return try: soup = BeautifulSoup(response.text, 'html.parser') all_cards = soup.find_all('div', class_='cartoon-card') print(f"Detected {len(all_cards)} cartoon items on the target index page.", flush=True) new_downloads_count = 0 for index, card in enumerate(all_cards): print(f"\nProcessing entry [{index + 1}/{len(all_cards)}]...", flush=True) img_element = card.find('img', class_='card-img-top') if not img_element: print(" -> [Parse Warning]: Target entry missing standard img node structure. Skipping.", flush=True) continue img_url = img_element.get('src') source_element = card.find('source', media="(min-width: 992px)") if source_element and source_element.get('srcset'): srcset_line = source_element.get('srcset') srcset_parts = srcset_line.split(',') first_entry = srcset_parts[0].strip() url_parts = first_entry.split(' ') img_url = url_parts[0].strip() if not img_url: print(" -> [Parse Warning]: Could not parse a valid URL from this asset node. Skipping.", flush=True) continue 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_latest_cartoon()