From 94f077944bfcb2c417b58a81123264a47c6825d1 Mon Sep 17 00:00:00 2001 From: wander Date: Thu, 20 Nov 2025 01:45:38 -0500 Subject: [PATCH] feat: Introduce scheduled background scanning, old folder cleanup, and optimize metadata fetching with updated Docker configuration and API tests. --- docker-compose.yml | 10 +-- ta-organizerr.tar.gz | Bin 0 -> 10317 bytes ta_symlink.py | 161 +++++++++++++++++++++++++++++++++++++------ test_api.py | 62 +++++++++++++++++ 4 files changed, 207 insertions(+), 26 deletions(-) create mode 100644 ta-organizerr.tar.gz create mode 100644 test_api.py diff --git a/docker-compose.yml b/docker-compose.yml index 2c64d0b..4594ddd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,10 @@ services: ta-organizer: - build: . + build: /mnt/user/appdata/dockerbuildings container_name: ta-organizer volumes: - - ./source:/app/source:ro - - ./target:/app/target + - /mnt/user/appdata/dockerbuildings/source:/app/source:ro + - /mnt/user/appdata/dockerbuildings/target:/app/target environment: - - API_TOKEN=${API_TOKEN} - env_file: .env + - SCAN_INTERVAL=${SCAN_INTERVAL:-60} + env_file: /mnt/user/appdata/dockerbuildings/.env diff --git a/ta-organizerr.tar.gz b/ta-organizerr.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..59e03c1635f25c845f57caee6f578a6e384008cf GIT binary patch literal 10317 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 literal 0 HcmV?d00001 diff --git a/ta_symlink.py b/ta_symlink.py index 0497085..fd53f1a 100644 --- a/ta_symlink.py +++ b/ta_symlink.py @@ -4,12 +4,15 @@ import os import requests import re import sys +import threading +import time from flask import Flask, jsonify, render_template_string, request # 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", "") +SCAN_INTERVAL = int(os.getenv("SCAN_INTERVAL", 60)) # Default 60 minutes SOURCE_DIR = Path("/app/source") TARGET_DIR = Path("/app/target") HEADERS = {"Authorization": f"Token {API_TOKEN}"} @@ -24,41 +27,137 @@ def sanitize(text): 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() +def fetch_all_metadata(): + print("๐Ÿ“ฅ Fetching all video metadata...", flush=True) + video_map = {} + page = 1 + while True: + url = f"{API_URL}/video/?page={page}" + try: + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + data = response.json() + + if 'data' not in data or not data['data']: + break + + for video in data['data']: + # Try to find the ID. It might be 'youtube_id' or '_id' + vid_id = video.get("youtube_id") or video.get("_id") + if not vid_id: + continue + + title = video.get("title", "unknown_title") + channel_info = video.get("channel", {}) + channel_name = channel_info.get("channel_name") or channel_info.get("channel_title") or "Unknown Channel" + # Fix date format: take only first 10 chars (YYYY-MM-DD) + raw_date = video.get("published", "unknown_date") + published = raw_date[:10] if len(raw_date) >= 10 else raw_date.replace("/", "-") + + video_map[vid_id] = { + "title": title, + "channel_name": channel_name, + "published": published + } + + # Check pagination to see if we are done + if 'paginate' in data: + current = data['paginate'].get('current_page') + last = data['paginate'].get('last_page') + if current is not None and last is not None and current >= last: + break + else: + # Fallback if no pagination info, just stop if empty data (handled above) or arbitrary limit? + # If we got data but no pagination, maybe it's a single page result? + # But we loop until no data. + pass - 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("/", "-") + print(f" - Page {page} fetched. Total videos so far: {len(video_map)}", flush=True) + page += 1 + + except Exception as e: + print(f"โŒ Error fetching page {page}: {e}", flush=True) + # If a page fails, maybe we should stop or retry? For now, let's stop to avoid infinite loops on auth error + break + + print(f"โœ… Metadata fetch complete. Found {len(video_map)} videos.", flush=True) + return video_map - 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 +def cleanup_old_folders(): + """ + Scans TARGET_DIR for folders containing '+00:00'. + Safely deletes them ONLY if they contain no real files (only symlinks or empty). + """ + print("๐Ÿงน Starting cleanup. Scanning ONLY for folders containing '+00:00'...", flush=True) + cleaned_count = 0 + skipped_count = 0 + + if not TARGET_DIR.exists(): + return + + # Walk top-down + for channel_dir in TARGET_DIR.iterdir(): + if not channel_dir.is_dir(): + continue + + for video_dir in channel_dir.iterdir(): + if not video_dir.is_dir(): + continue + + if "+00:00" in video_dir.name: + # Check safety + safe_to_delete = True + reason = "" + + for item in video_dir.iterdir(): + if not item.is_symlink(): + # Found a real file! Unsafe! + safe_to_delete = False + reason = "Contains real files" + break + + if safe_to_delete: + try: + # Remove all symlinks first + for item in video_dir.iterdir(): + item.unlink() + # Remove directory + video_dir.rmdir() + print(f" [DELETED] {video_dir.name}", flush=True) + cleaned_count += 1 + except Exception as e: + print(f" โŒ Failed to delete {video_dir.name}: {e}", flush=True) + else: + print(f" โš ๏ธ SKIPPING {video_dir.name} - {reason}", flush=True) + skipped_count += 1 + + print(f"๐Ÿงน Cleanup complete. Removed: {cleaned_count}, Skipped: {skipped_count}", flush=True) # Main logic def process_videos(): global processed_videos processed_videos = [] + + # 1. Fetch all metadata first + video_map = fetch_all_metadata() + + # 2. Run cleanup + cleanup_old_folders() + + # Statistics + new_links = 0 + verified_links = 0 + 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) + + # 2. Lookup in local map + meta = video_map.get(video_id) if not meta: continue sanitized_channel_name = sanitize(meta["channel_name"]) @@ -81,8 +180,14 @@ def process_videos(): if current_target.resolve() != host_source_path.resolve(): dest_file.unlink() os.symlink(host_source_path, dest_file) + print(f" [FIX] Relinked: {folder_name}", flush=True) + new_links += 1 + else: + verified_links += 1 else: os.symlink(host_source_path, dest_file) + print(f" [NEW] Linked: {folder_name}", flush=True) + new_links += 1 except Exception: pass processed_videos.append({ @@ -94,8 +199,18 @@ def process_videos(): }) except Exception as e: return str(e) + + print(f"โœ… Scan complete. Processed {len(processed_videos)} videos.", flush=True) + print(f" - New/Fixed Links: {new_links}", flush=True) + print(f" - Verified Links: {verified_links}", flush=True) return None +def scheduler(): + print(f"๐Ÿ•’ Background scheduler started. Scanning every {SCAN_INTERVAL} minutes.", flush=True) + while True: + print("๐Ÿ”„ Running scheduled scan...", flush=True) + process_videos() + time.sleep(SCAN_INTERVAL * 60) # Flask routes @app.route("/") @@ -146,4 +261,8 @@ def api_videos(): return jsonify(processed_videos) if __name__ == "__main__": + # Start scheduler in background thread + thread = threading.Thread(target=scheduler, daemon=True) + thread.start() + app.run(host="0.0.0.0", port=5000) diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..7d51e6a --- /dev/null +++ b/test_api.py @@ -0,0 +1,62 @@ +import requests +import os +import json + +# Manually load .env +try: + with open('.env', 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + key, value = line.split('=', 1) + os.environ[key] = value +except FileNotFoundError: + print("Warning: .env file not found") + +API_URL = os.getenv("API_URL") +API_TOKEN = os.getenv("API_TOKEN") + +headers = {"Authorization": f"Token {API_TOKEN}"} + +print(f"Testing API at: {API_URL}") + +def test_endpoint(path): + url = f"{API_URL}{path}" + print(f"\n--- Testing {url} ---") + try: + response = requests.get(url, headers=headers, timeout=5) + print(f"Status Code: {response.status_code}") + try: + data = response.json() + print("Response JSON (truncated):") + print(json.dumps(data, indent=2)[:500] + "..." if len(str(data)) > 500 else json.dumps(data, indent=2)) + return data + except json.JSONDecodeError: + print("Response is not JSON") + print(response.text[:200]) + return None + except Exception as e: + print(f"Error: {e}") + return None + +# Test Root API +test_endpoint("") + +# Test Search Parameters +target_id = "K1Uw_YVgCBsww" +print(f"\n--- Testing Search Params for {target_id} ---") + +# Test Page Size +print(f"\n--- Testing Page Size ---") + +sizes = [12, 50, 100] + +for size in sizes: + url = f"/video/?page_size={size}" + print(f"Testing {url}...") + data = test_endpoint(url) + if data and isinstance(data, dict) and 'data' in data: + count = len(data['data']) + print(f"Requested {size}, got {count} items.") + if 'paginate' in data: + print(f"Pagination meta: {data['paginate']}")