commit 9bc7a648afdb44dbd7e0a7d85cf5ab1863fd438d Author: Space-Banane Date: Wed Apr 1 17:12:28 2026 +0200 Add initial implementation of StreamShot with snapshot capture feature and UI diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bc4044 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.jpg \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a65edde --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# StreamShot 📸 + +Capture high-quality JPEG snapshots from any YouTube livestream via a simple web UI or API. +(yes it's a yt-dlp wrapper) + +## Features + +- **Stream Extraction**: Uses `yt-dlp` to find the best available stream URL. +- **Frame Capture**: Uses OpenCV to grab a single frame directly from the video stream. +- **Web UI**: A clean, Tailwind CSS-powered interface for quick captures. +- **API Access**: Returns snapshots as base64-encoded buffers in a JSON object. + +## API Usage + +### Endpoint +`POST /snapshot` + +### Request Body +```json +{ + "url": "https://www.youtube.com/watch?v=..." +} +``` + +### Response +```json +{ + "message": "done", + "buffer": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAg..." +} +``` + +### CLI Example (Save to File) +To capture a snapshot and save it directly to a file using `curl`, `jq`, and `base64`: + +```bash +curl -X POST \ + https://shsf-api.reversed.dev/api/exec/16/cca815a2-216d-4362-b6f5-1d57ac9f088d/snapshot \ + -H "Content-Type: application/json" \ + -d '{"url": "YOUR_YOUTUBE_URL_HERE"}' \ + | jq -r .buffer | base64 -d > snapshot.jpg +``` + +## Local Development + +### Prerequisites +- Python 3.14 (tested) +- `opencv-python` +- `yt-dlp` +- `numpy` + +### Installation +```bash +pip install -r requirements.txt +``` + +### Running Locally +You can test the capture logic directly: +```bash +python main.py +``` + +## Tech Stack +- **Backend**: Python (yt-dlp, OpenCV) +- **Frontend**: HTML5, Tailwind CSS, JavaScript (Fetch API) +- **Deployment**: Shsf (Serverless) diff --git a/main.py b/main.py new file mode 100644 index 0000000..0317829 --- /dev/null +++ b/main.py @@ -0,0 +1,176 @@ +import cv2 +import yt_dlp +import numpy as np +import json + + +def capture_stream_snapshot(youtube_url, output_file="snapshot.jpg", toBuffer=False): + # 1. Configure yt-dlp to get the direct stream URL + ydl_opts = { + "format": "best", + "quiet": True, + "no_warnings": True, + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + try: + info = ydl.extract_info(youtube_url, download=False) + stream_url = info["url"] + except Exception as e: + print(f"Error extracting info: {e}") + return None + + # 2. Use OpenCV to capture a frame from the stream URL + try: + cap = cv2.VideoCapture(stream_url) + except Exception as e: + print(f"Error opening VideoCapture: {e}") + return None + + if not cap.isOpened(): + print("Error: Could not open video stream.") + return None if toBuffer else None + + # Read a single frame + success, frame = cap.read() + + if success: + if toBuffer: + # Encode frame as JPEG to memory buffer + ret, buf = cv2.imencode(".jpg", frame) + if ret: + print("Snapshot captured to buffer.") + cap.release() + return buf.tobytes() + else: + print("Error: Could not encode frame to buffer.") + cap.release() + return None + else: + # 3. Save the frame as an image + cv2.imwrite(output_file, frame) + print(f"Snapshot saved to {output_file}") + else: + print("Error: Could not read frame from stream.") + + # Cleanup + cap.release() + + +# Shsf Handler +def main(args): + route = args.get("route") + if route == "snapshot": + url = args.get("body", "") + try: + url = json.loads(url) + except json.JSONDecodeError: + print("Error: Invalid JSON input.") + return { + "_shsf": "v2", + "_code": 400, + "_res": {"message": "Invalid JSON input."}, + "_headers": { + "Content-Type": "application/json", + }, + } + url = url.get("url", "") + if not url: + print("Error: URL not provided.") + return { + "_shsf": "v2", + "_code": 400, + "_res": {"message": "URL not provided."}, + "_headers": { + "Content-Type": "application/json", + }, + } + + try: + capture_stream_snapshot(url, output_file="/tmp/snapshot.jpg") + content = None + with open("/tmp/snapshot.jpg", "rb") as f: + content = f.read() + except Exception as e: + print(f"Error: {e}") + return { + "_shsf": "v2", + "_code": 500, + "_res": {"message": f"Could not capture snapshot: {str(e)}"}, + "_headers": { + "Content-Type": "application/json", + }, + } + + if content is None: + print("Error: Could not capture snapshot.") + return { + "_shsf": "v2", + "_code": 500, + "_res": {"message": "Could not capture snapshot."}, + "_headers": { + "Content-Type": "application/json", + }, + } + + # Convert content to base64 for JSON response + import base64 + encoded_content = base64.b64encode(content).decode('utf-8') + + return { + "_shsf": "v2", + "_code": 200, + "_res": { + "message": "done", + "buffer": encoded_content + }, + "_headers": { + "Content-Type": "application/json", + }, + } + elif route == "health": + return { + "_shsf": "v2", + "_code": 200, + "_res": {"message": "Service is healthy."}, + "_headers": { + "Content-Type": "application/json", + }, + } + elif route == "default": # UI Route + try: + content = "" + with open("/app/ui.html", "r") as f: + content = f.read() + except FileNotFoundError: + print("Error: /app/ui.html not found.") + return { + "_shsf": "v2", + "_code": 404, + "_res": {"message": "UI file not found."}, + "_headers": { + "Content-Type": "application/json", + }, + } + except Exception as e: + print(f"Error loading UI: {e}") + return { + "_shsf": "v2", + "_code": 500, + "_res": {"message": f"Server error: {str(e)}"}, + "_headers": { + "Content-Type": "application/json", + }, + } + return { + "_shsf": "v2", + "_code": 200, + "_res": content, + "_headers": { + "Content-Type": "text/html", + }, + } + +if __name__ == "__main__": + print("omg im on main") + capture_stream_snapshot("https://www.youtube.com/watch?v=n15V_fCsl_c", output_file="snapshot.jpg") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..26914e5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +yt-dlp +opencv-python \ No newline at end of file diff --git a/ui.html b/ui.html new file mode 100644 index 0000000..87e1311 --- /dev/null +++ b/ui.html @@ -0,0 +1,157 @@ + + + + + + StreamShot + + + + +
+
+

StreamShot

+

Capture a high-quality frame from any YouTube livestream

+
+ +
+
+ + +
+ + +
+ + + +
+

API Documentation (for AI/CLI)

+
+
+

To capture a snapshot and save the binary image directly using curl and jq:

+
curl -X POST \
+  https://shsf-api.reversed.dev/api/exec/16/cca815a2-216d-4362-b6f5-1d57ac9f088d/snapshot \
+  -H "Content-Type: application/json" \
+  -d '{"url": "https://www.youtube.com/watch?v=STREAMID"}' \
+  | jq -r .buffer | base64 -d > snapshot.jpg
+
+
+ Note: The response is a JSON object with a base64 encoded string in the buffer field. +
+
+
+
+ + + +