#!/usr/bin/env python3 import argparse import sys from pathlib import Path from services.iview import iViewService from services.ten import TenService # ------------------------- # Service registry # ------------------------- SERVICES = { "IVIEW": iViewService(), "TEN": TenService(), } class AutoGrabber: def __init__(self, download_dir, config_dir, dry_run=False, mark_existing=False): # The main media output directory (Required) self.download_dir = Path(download_dir) # The config directory (Defaults to PWD/.tvgrabber or CLI override) self.config_dir = Path(config_dir) self.history_file = self.config_dir / "history" self.series_file = self.config_dir / "series" self.dry_run = dry_run self.mark_existing = mark_existing # Initialize self.history = self.load_history() self.series_list = self.load_series() def load_history(self): history = {} # Ensure .tvgrabber exists before we try to handle the history file if not self.config_dir.exists(): self.config_dir.mkdir(parents=True, exist_ok=True) if not self.history_file.exists(): self.history_file.touch() with open(self.history_file, "r") as f: for line in f: line = line.strip() if not line or "|" not in line: if line: history[line] = None continue ep_id, filename = line.split("|", 1) history[ep_id.strip()] = filename.strip() return history def save_history(self): with open(self.history_file, "w") as f: for ep_id in sorted(self.history.keys()): filename = self.history[ep_id] f.write(f"{ep_id} | {filename}\n" if filename else f"{ep_id}\n") def load_series(self): self.series_file = self.config_dir / "series.json" if not self.series_file.exists(): print(f"โŒ Error: Series file not found at {self.series_file}") print(f"Please ensure your series list is in {self.config_dir}/series.json") sys.exit(1) try: with open(self.series_file, "r") as f: return json.load(f) except json.JSONDecodeError as e: print(f"โŒ Failed to parse series.json: {e}") sys.exit(1) def run(self): for show_config in self.series_list: self.process_show(show_config) def process_show(self, config): service_name = config.get("service") source_title = config.get("source_title") if not service_name or not source_title: print(f"โŒ Skipping invalid entry: {config} (Missing service or source_title)") return service = SERVICES.get(service_name.upper()) if not service: print(f"โŒ Unknown service: {service_name}") return # 2. Setup Defaults for Output Fields output_title = config.get("output_title") or source_title source_season = config.get("source_season") # If output_season isn't provided, use source_season (if it exists) output_season = config.get("output_season") or source_season print(f"\n==============================") print(f"๐Ÿ“บ Show: {output_title}") print(f"๐Ÿ“ก Service: {service_name}") print("==============================") # 3. Discovery (Ten will handle its own requirement for source_season) seasons = service.discover_seasons(source_title, source_season=source_season) if not seasons: # Service will have already printed its specific error message return for season_data in seasons: # We use the resolved output_season (either from JSON or fallback) # If discovery found a season number and we have no override, we use that. final_season = output_season or season_data.get("season") print(f"\n๐Ÿ“ฆ Processing Season {final_season}") for entry in season_data["data"]["entries"]: episode = service.normalize_episode( output_title, entry, season_num=final_season ) ep_id = episode["episode_id"] filename = episode["filename"] if ep_id in self.history: print(f" โฉ {filename} (Skipped)") continue if self.dry_run: print(f" ๐Ÿงช {filename} (Dry Run)") continue if self.mark_existing: print(f" ๐Ÿ“ {filename} (Marked)") self.history[ep_id] = filename self.save_history() continue print(f" โœ… {filename} (Downloading...)") if service.download_episode(episode, entry, self.download_dir): self.history[ep_id] = filename self.save_history() def run(self): for service, source, output in self.series_list: self.process_show(service, source, output) print("\nโœ… Done.") # ------------------------- # CLI # ------------------------- def parse_args(): parser = argparse.ArgumentParser(description="Australian FTA TV AutoGrabber") # Required positional argument for the download directory parser.add_argument( "download_dir", help="REQUIRED: Path to the directory where media will be saved" ) # Optional config directory override parser.add_argument( "-c", "--config", default="~/.tvgrabber", help="Path to the config folder containing 'series' and 'history' (default: ~/.tvgrabber)" ) parser.add_argument("--dry-run", action="store_true") parser.add_argument("--mark-existing", action="store_true") return parser.parse_args() if __name__ == "__main__": args = parse_args() # Ensure the download directory exists out_path = Path(args.download_dir).resolve() if not out_path.exists() and not args.dry_run: out_path.mkdir(parents=True, exist_ok=True) AutoGrabber( download_dir=out_path, config_dir=Path(args.config).resolve(), dry_run=args.dry_run, mark_existing=args.mark_existing ).run()