python -m venv peertube source peertube/bin/activate pip install aiohttp python peertube_search.py “you are the dead internet”

#!/usr/bin/env python3
"""
PeerTube Universal Search
Searches ALL known PeerTube instances in parallel, not just SepiaSearch-indexed ones.
Usage: python peertube_search.py "your search query" [--max-instances N] [--timeout N]
"""

import asyncio
import aiohttp
import argparse
import json
import sys
from datetime import datetime

INSTANCES_API = "https://instances.joinpeertube.org/api/v1/instances?count=1000&start=0"

async def fetch_instances(session):
    """Fetch all known PeerTube instances."""
    print("Fetching instance list from instances.joinpeertube.org ...", flush=True)
    try:
        async with session.get(INSTANCES_API, timeout=aiohttp.ClientTimeout(total=15)) as resp:
            data = await resp.json()
            instances = [i["host"] for i in data.get("data", [])]
            print(f"Found {len(instances)} instances.\n", flush=True)
            return instances
    except Exception as e:
        print(f"Failed to fetch instance list: {e}", file=sys.stderr)
        return []

async def search_instance(session, host, query, timeout):
    """Search a single PeerTube instance."""
    url = f"https://{host}/api/v1/search/videos"
    params = {"search": query, "count": 10, "sort": "-match"}
    try:
        async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=timeout)) as resp:
            if resp.status == 200:
                data = await resp.json()
                videos = data.get("data", [])
                if videos:
                    return host, videos
    except Exception:
        pass
    return host, []

def format_video(video, host):
    name = video.get("name", "Untitled")
    uuid = video.get("uuid", "")
    duration = video.get("duration", 0)
    mins, secs = divmod(duration, 60)
    channel = video.get("channel", {}).get("displayName", "Unknown")
    published = video.get("publishedAt", "")[:10] if video.get("publishedAt") else ""
    views = video.get("views", 0)
    url = f"https://{host}/videos/watch/{uuid}"
    return (
        f"  Title   : {name}\n"
        f"  Channel : {channel}  |  Duration: {mins}m{secs:02d}s"
        f"  |  Views: {views}  |  Date: {published}\n"
        f"  URL     : {url}\n"
    )

async def main():
    parser = argparse.ArgumentParser(description="Search all PeerTube instances")
    parser.add_argument("query", help="Search query")
    parser.add_argument("--max-instances", type=int, default=500,
                        help="Max instances to query (default: 500)")
    parser.add_argument("--timeout", type=int, default=8,
                        help="Per-instance timeout in seconds (default: 8)")
    parser.add_argument("--json", action="store_true",
                        help="Output raw JSON instead of formatted text")
    args = parser.parse_args()

    connector = aiohttp.TCPConnector(limit=100, ssl=False)
    headers = {"User-Agent": "PeerTubeUniversalSearch/1.0"}

    async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
        instances = await fetch_instances(session)
        if not instances:
            print("No instances found. Exiting.")
            return

        instances = instances[:args.max_instances]
        print(f"Searching {len(instances)} instances for: \"{args.query}\"")
        print("(This may take a few seconds...)\n", flush=True)

        tasks = [search_instance(session, host, args.query, args.timeout) for host in instances]

        results = []
        done = 0
        for coro in asyncio.as_completed(tasks):
            host, videos = await coro
            done += 1
            if videos:
                results.append((host, videos))
                print(f"[{done}/{len(instances)}] ✓ {host}{len(videos)} result(s)", flush=True)

        print(f"\n{'='*60}")
        print(f"Search complete. {len(results)} instance(s) returned results.")
        print(f"{'='*60}\n")

        if args.json:
            all_videos = []
            for host, videos in results:
                for v in videos:
                    v["_instance"] = host
                    all_videos.append(v)
            print(json.dumps(all_videos, indent=2))
        else:
            total = 0
            for host, videos in sorted(results, key=lambda x: -len(x[1])):
                print(f"── {host} ({len(videos)} result(s)) ──")
                for v in videos:
                    print(format_video(v, host))
                    total += 1
            print(f"Total videos found: {total}")

if __name__ == "__main__":
    asyncio.run(main())