#!/usr/bin/env python3 """ Download slides pages as PNG files and voice narrations as WAV files. Exports everything as a ZIP archive (completely free). """ import os import sys import json import argparse import ipaddress import re import socket import requests from urllib.parse import urlparse from typing import Optional, Dict, Any API_BASE_URL = "https://2slides.com/api/v1" JOB_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$") def validate_job_id(job_id: str) -> str: if not JOB_ID_RE.match(job_id or ""): raise ValueError("Job ID contains unsupported characters") return job_id def validate_public_https_url(url: str) -> str: parsed = urlparse(url) if parsed.scheme != "https" or not parsed.hostname: raise ValueError("Download URL must be HTTPS") for info in socket.getaddrinfo(parsed.hostname, None): ip = ipaddress.ip_address(info[4][0]) if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: raise ValueError("Download URL resolves to a non-public address") return url def get_api_key() -> str: """Get API key from environment variable.""" api_key = os.environ.get("SLIDES_2SLIDES_API_KEY") if not api_key: raise ValueError( "API key not found. Set SLIDES_2SLIDES_API_KEY environment variable.\n" "Get your API key from: https://2slides.com/api" ) return api_key def download_slides_pages_voices( job_id: str, output_path: Optional[str] = None, api_key: Optional[str] = None ) -> str: """ Download slides pages and voice narrations as a ZIP archive. Args: job_id: Job ID from slide generation output_path: Optional path to save the ZIP file (default: .zip) api_key: API key (uses env var if not provided) Returns: Path to the downloaded ZIP file Notes: - Exports pages as PNG files - Exports voices as WAV files - Includes transcripts - Completely free (no credit cost) - Download URL valid for 1 hour """ if api_key is None: api_key = get_api_key() job_id = validate_job_id(job_id) headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "jobId": job_id } url = f"{API_BASE_URL}/slides/download-slides-pages-voices" print(f"Requesting download for job: {job_id}...", file=sys.stderr) response = requests.post(url, headers=headers, json=payload, timeout=30) response.raise_for_status() result = response.json() # Check API response structure if not result.get("success"): error_msg = result.get("error", "Unknown error") raise ValueError(f"API error: {error_msg}") # Get download URL from data field data = result.get("data") if not data: raise ValueError("No data in API response") download_url = data.get("downloadUrl") if not download_url: raise ValueError("No download URL in response") download_url = validate_public_https_url(download_url) # Optional: log additional info file_name = data.get("fileName", "unknown.zip") expires_in = data.get("expiresIn", 3600) print(f" Filename: {file_name}", file=sys.stderr) print(f" Expires in: {expires_in} seconds", file=sys.stderr) # Download the ZIP file if output_path is None: output_path = f"{job_id}.zip" print(f"Downloading ZIP archive to: {output_path}...", file=sys.stderr) zip_response = requests.get(download_url, stream=True, timeout=120) zip_response.raise_for_status() # Save to file with open(output_path, 'wb') as f: for chunk in zip_response.iter_content(chunk_size=8192): f.write(chunk) file_size = os.path.getsize(output_path) print(f"✓ Downloaded successfully!", file=sys.stderr) print(f" File: {output_path}", file=sys.stderr) print(f" Size: {file_size:,} bytes", file=sys.stderr) return output_path def main(): parser = argparse.ArgumentParser( description="Download 2slides pages and voices as ZIP archive (FREE)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Download with default filename %(prog)s --job-id "abc-123-def-456" # Download to specific path %(prog)s --job-id "abc-123-def-456" --output slides.zip Archive Contents: - Pages as PNG files - Voice files as WAV - Transcripts Note: Download URLs are valid for 1 hour only Cost: Completely FREE (no credits used) """ ) parser.add_argument("--job-id", required=True, help="Job ID from slide generation") parser.add_argument("--output", help="Output ZIP file path (default: .zip)") args = parser.parse_args() try: output_path = download_slides_pages_voices( job_id=args.job_id, output_path=args.output ) # Output path for easy parsing print(json.dumps({"success": True, "output": output_path}, indent=2)) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()