commit 1230ae7adf3001152da2e348f9e73c1028302e46 Author: wander Date: Sat Apr 12 15:25:05 2025 -0400 Initial commit: TA Organizer script with Docker support diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6cf5f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12d2b13 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.11-slim +WORKDIR /app +COPY ta_symlink.py . +RUN pip install --no-cache-dir requests +CMD ["python", "ta_symlink.py"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2c64d0b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + ta-organizer: + build: . + container_name: ta-organizer + volumes: + - ./source:/app/source:ro + - ./target:/app/target + environment: + - API_TOKEN=${API_TOKEN} + env_file: .env diff --git a/ta_symlink.py b/ta_symlink.py new file mode 100644 index 0000000..9cfc88e --- /dev/null +++ b/ta_symlink.py @@ -0,0 +1,94 @@ +from pathlib import Path +import os +import requests +import re +import sys + +# Load config from environment variables +API_URL = os.getenv("API_URL", "http://localhost:8457/api") +VIDEO_URL = os.getenv("VIDEO_URL", "http://localhost:8457/video/") +API_TOKEN = os.getenv("API_TOKEN", "") +SOURCE_DIR = Path("/app/source") +TARGET_DIR = Path("/app/target") +HEADERS = {"Authorization": f"Token {API_TOKEN}"} + +# Utility functions +def sanitize(text): + text = text.encode("ascii", "ignore").decode() + text = re.sub(r'[\/:*?"<>|]', "_", text) + return text.strip() + +def fetch_video_metadata(video_id): + url = f"{API_URL}/video/{video_id}/" + try: + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + data = response.json() + + title = data.get("title", "unknown_title") + channel_info = data.get("channel", {}) + channel_id = channel_info.get("channel_id", "unknown_channel") + channel_name = channel_info.get("channel_name") or channel_info.get("channel_title") or "Unknown Channel" + published = data.get("published", "unknown_date").replace("/", "-") + + return { + "title": title, + "channel_id": channel_id, + "channel_name": channel_name, + "published": published + } + except Exception as e: + print(f"❌ Error fetching metadata for {video_id}: {e}", flush=True) + return None + +# Main logic +def process_videos(): + print("📁 Starting video processing...", flush=True) + + try: + for channel_path in SOURCE_DIR.iterdir(): + if not channel_path.is_dir(): + continue + + for video_file in channel_path.glob("*.*"): + video_id = video_file.stem + meta = fetch_video_metadata(video_id) + if not meta: + print(f"⚠️ Skipped {video_id}: could not fetch metadata", flush=True) + continue + + sanitized_channel_name = sanitize(meta["channel_name"]) + channel_dir = TARGET_DIR / sanitized_channel_name + channel_dir.mkdir(parents=True, exist_ok=True) + + sanitized_title = sanitize(meta["title"]) + folder_name = f"{meta['published']} - {sanitized_title}" + video_dir = channel_dir / folder_name + video_dir.mkdir(parents=True, exist_ok=True) + + actual_file = next(channel_path.glob(f"{video_id}.*"), None) + if not actual_file: + print(f"⚠️ Video file not found for {video_id} in {channel_path}", flush=True) + continue + + host_path_root = Path("/mnt/user/tubearchives/bp") + host_source_path = host_path_root / actual_file.relative_to(SOURCE_DIR) + + dest_file = video_dir / f"video{actual_file.suffix}" + try: + if dest_file.exists(): + if dest_file.is_symlink(): + current_target = Path(os.readlink(dest_file)) + if current_target.resolve() != host_source_path.resolve(): + dest_file.unlink() + os.symlink(host_source_path, dest_file) + else: + os.symlink(host_source_path, dest_file) + print(f"✅ Linked: {dest_file}", flush=True) + except Exception: + pass + except Exception as e: + print(f"💥 Unhandled error: {e}", file=sys.stderr, flush=True) + +if __name__ == "__main__": + process_videos()