#!/usr/bin/env python3 import os import sys from functools import lru_cache from typing import Literal import psycopg2 from pydantic import BaseModel from alfred_utils import AlfredOutput, print_debug, print_items, print_out from extract_uuid import extract_uuid from hook_cache import HookCache from rate_limiter import RateLimiter # Initialize cache with permanent storage cache = HookCache() # Initialize rate limiter (1 second minimum between DB queries) rate_limiter = RateLimiter(min_interval=1.0) class HookLookup(BaseModel): hook_id: str original_clip_id: str video_upload_id: str file_ext: str env: Literal["prod", "staging"] = "staging" def get_base_url(self) -> str: if self.env == "prod": return "https://suno.com/" else: return "https://b.suno.fm/" def make_raw_upload_result(upload_id: str, file_ext: str | None = None) -> AlfredOutput: subtitle = "View raw video upload in S3" if not file_ext: subtitle = ( "View raw video upload in S3 (unknown file extension, defaulting to mp4)" ) file_ext = "mp4" url = f"https://us-east-1.console.aws.amazon.com/s3/object/suno-uploads?region=us-east-1&bucketType=general&prefix=raw_uploads/{upload_id}.{file_ext}" return AlfredOutput( title="Open Raw Video Upload", subtitle=subtitle, arg=url, valid=True, ) def make_processed_video_result(upload_id: str) -> AlfredOutput: url = f"https://cdn1.suno.ai/video_upload_{upload_id}.mp4" return AlfredOutput( title="Open Processed Video", subtitle="View processed video in CDN", arg=url, valid=True, ) def make_rendered_hook_result(hook_id: str) -> AlfredOutput: url = f"https://cdn1.suno.ai/hook_{hook_id}.mp4" return AlfredOutput( title="Open Rendered Hook in CDN", subtitle="View rendered hook in CDN", arg=url, valid=True, ) def make_hook_app_result(hook_lookup: HookLookup) -> AlfredOutput: url = f"{hook_lookup.get_base_url()}/hook/{hook_lookup.hook_id}" return AlfredOutput( title="Open Hook on Web", subtitle="View the hook live in the app", arg=url, valid=True, ) def make_original_clip_result(hook_lookup: HookLookup) -> AlfredOutput: return AlfredOutput( title="Open Original Clip", subtitle="View the original clip", arg=f"{hook_lookup.get_base_url()}/song/{hook_lookup.original_clip_id}", valid=True, ) def make_original_clip_cdn_result(hook_lookup: HookLookup) -> AlfredOutput: return AlfredOutput( title="Download Original Clip from CDN", subtitle="View the original clip", arg=f"https://cdn1.suno.ai/{hook_lookup.original_clip_id}.mp4", valid=True, ) def make_copy_upload_id_result(hook_lookup: HookLookup) -> AlfredOutput: return AlfredOutput( title="Copy Upload ID", subtitle="Copy the upload ID to the clipboard", arg=f"{hook_lookup.video_upload_id}", valid=True, variables={"action": "copy"}, ) def make_remove_cache_result(hook_id: str) -> AlfredOutput: return AlfredOutput( title="Remove from Cache", subtitle="Clear this hook ID from the cache", arg=f"{hook_id}", valid=True, variables={"action": "clear-cache"}, ) class ReturnException(Exception): def __init__(self, title: str, subtitle: str, valid: bool, error: str): self.title = title self.subtitle = subtitle self.valid = valid self.error = error super().__init__(title) def print_out(self) -> None: print_out(self.title, self.subtitle, self.valid, self.error) @lru_cache(maxsize=128) def lookup_hook_video_upload(hook_id: str, database_url: str) -> HookLookup | None: conn = None cur = None try: # Connect to database conn = psycopg2.connect(database_url) cur = conn.cursor() # Query for hook data cur.execute( """ SELECT vh.id, vh.original_clip, vu.id, vu.original_file_ext FROM video_videohook vh JOIN bots_videoupload vu ON vh.raw_video_upload_id = vu.id WHERE vh.id = %s """, (hook_id,), ) result = cur.fetchone() if result: hook_id, original_clip_id, upload_id, file_ext = result return HookLookup( hook_id=hook_id, original_clip_id=original_clip_id, video_upload_id=upload_id, file_ext=file_ext, ) except Exception as e: # Log error for debugging but still return None to try next database print_debug(f"Database error: {e}") return None finally: if cur: cur.close() if conn: conn.close() return None def print_results(hook_lookup: HookLookup) -> None: raw_upload_result = make_raw_upload_result( hook_lookup.video_upload_id, hook_lookup.file_ext ) processed_video_result = make_processed_video_result(hook_lookup.video_upload_id) rendered_hook_result = make_rendered_hook_result(hook_lookup.hook_id) hook_app_result = make_hook_app_result(hook_lookup) original_clip_result = make_original_clip_result(hook_lookup) original_clip_cdn_result = make_original_clip_cdn_result(hook_lookup) copy_upload_id_result = make_copy_upload_id_result(hook_lookup) remove_cache_result = make_remove_cache_result(hook_lookup.hook_id) print_items( [ raw_upload_result, processed_video_result, rendered_hook_result, hook_app_result, original_clip_result, original_clip_cdn_result, copy_upload_id_result, remove_cache_result, ] ) def print_not_found_with_cache_option(hook_id: str, is_cached: bool = False) -> None: """Print not found message with option to remove from cache.""" title = "No video upload found for this hook ID" if is_cached: title += " (cached)" print_items( [ AlfredOutput( title=title, subtitle="Check the hook ID and try again", valid=False, arg=None, ), make_remove_cache_result(hook_id), ] ) def handle_hook_lookup(hook_id: str) -> None: uuids = extract_uuid(hook_id) if not uuids: print_out( title="No UUID found", subtitle="No valid UUID found in the input text", valid=False, ) return hook_id = uuids[0] # Check cache first cache_key = f"hook_{hook_id}" cached_result = cache.get(cache_key) if cached_result is not None: try: # Cache hit - cached_result is a dict with 'found' key if cached_result.get("found"): # Reconstruct HookLookup from dict hook_data = cached_result["hook_lookup"] if hook_data: hook_lookup = HookLookup(**hook_data) print_results(hook_lookup) else: print_not_found_with_cache_option(hook_id, is_cached=True) return except Exception as e: # If deserialization fails, clear the cache entry and continue with fresh lookup print_debug(f"Cache deserialization failed: {e}, clearing cache entry") cache.remove(cache_key) # Not in cache, do database lookup # Apply rate limiting before database queries if rate_limiter.wait_if_needed(): print_debug("Rate limit applied") hook_lookup = lookup_hook_video_upload(hook_id, os.getenv("DATABASE_URL_PROD")) print_debug(f"hook_lookup: {hook_lookup}") if not hook_lookup: print_debug("not found in prod, looking in staging") hook_lookup = lookup_hook_video_upload( hook_id, os.getenv("DATABASE_URL_STAGING") ) if not hook_lookup: print_debug("not found in staging") print_not_found_with_cache_option(hook_id, is_cached=False) return hook_lookup.env = "staging" else: hook_lookup.env = "prod" # Record that we made database queries rate_limiter.record_operation() # Save to cache with metadata about whether it was found # Convert Pydantic model to dict for serialization cache_value = { "found": bool(hook_lookup), "hook_lookup": hook_lookup.model_dump() if hook_lookup else None, } cache.set(cache_key, cache_value) if hook_lookup: print_results(hook_lookup) else: print_not_found_with_cache_option(hook_id, is_cached=False) def main(): if len(sys.argv) < 2: print_out( title="Enter a hook ID", subtitle="Type a hook ID to lookup", valid=False, ) return hook_id = sys.argv[1].strip() if not hook_id: print_out( title="Enter a hook ID", subtitle="Type a hook ID to lookup", valid=False, ) return try: handle_hook_lookup(hook_id) except ReturnException as e: e.print_out() if __name__ == "__main__": main()