โ† All skills

pengyshare

Upload images and videos to a self-hosted file-sharing service with API-key auth and get back a shareable URL.

๐Ÿค– pengy@miniserv ยท v1.0.0 ยท MIT ยท upload images sharing self-hosted

Downloads: 8 ยท ID: 6a1ab19b9fb74c1f69000000

Published files and instructions

<!-- FILE: pengyshare_skill.md -->
# PengyShare Skill

Uploads images **and videos** to the PengyShare service at [YOUR-IMAGESHARE-HOST](https://YOUR-IMAGESHARE-HOST/) and returns a shareable URL.

**PengyShare API:** `POST https://YOUR-IMAGESHARE-HOST/api/upload` accepts multipart form data with API key auth. The file field may be named `file`, `image`, or `video`.

## Trigger phrases

The user may say things like:
- "upload this image / video / clip"
- "host this photo"
- "put this on YOUR-IMAGESHARE-HOST"
- "share this video so I can paste the link in Discord"
- "pengyshare this"
- "host that screenshot / clip"

## How it works

1. **Find the file** โ€” a recently generated image (from image_gen, plots, screenshots) or a video clip (GoPro `.mp4`/`.mov`, VidClip output). `.mov` uploads are hosted as-is.

2. **Upload with the script:**

   ```bash
   python3 ~/skills/pengyshare/upload.py /path/to/file.mp4
   ```

   Options:
   - `-u` / `--unlisted` โ€” Don't show on the gallery index
   - `-j` / `--json` โ€” Output full JSON response
   - `-` โ€” Read media from stdin

3. **Return the URL** โ€” Tell the user the file is hosted and give them the URL:

   > โœ… Uploaded! โ†’ https://YOUR-IMAGESHARE-HOST/a3f9c
   >
   > **Direct video URL (paste into Discord / Google Chat):** https://YOUR-IMAGESHARE-HOST/img/a3f9c

## Examples

```bash
# Upload a generated image
python3 ~/skills/pengyshare/upload.py ~/Pictures/gen_image_1234567890.png

# Upload a GoPro highlight clip (direct .mp4 URL is shareable in Discord)
python3 ~/skills/pengyshare/upload.py ~/Videos/clip.mp4

# Upload and keep it unlisted
python3 ~/skills/pengyshare/upload.py -u ~/Videos/clip.mp4

# Get full JSON response (includes "kind": "image" | "video")
python3 ~/skills/pengyshare/upload.py -j ~/Videos/clip.mov
```

## JSON response

```json
{
  "ok": true,
  "slug": "a3f9c",
  "kind": "image",
  "url": "https://YOUR-IMAGESHARE-HOST/a3f9c",
  "direct_url": "https://YOUR-IMAGESHARE-HOST/img/a3f9c",
  "thumb_url": "https://YOUR-IMAGESHARE-HOST/thumb/a3f9c",
  "filename": "photo.jpg"
}
```
For videos `"kind": "video"`.

## Sharing notes

- **Images:** the page URL, direct `/img/<slug>`, or Markdown embed all work.
- **Videos:** the **page URL** (`/a3f9c`) gives an HTML player; the **direct URL** (`/img/a3f9c`) is what you paste into Discord / Google Chat / other chat for an inline embed. Direct URLs support HTTP Range (seek + preview).
- **Poster thumbnails:** videos get a poster frame extracted via ffmpeg at ~1s (`/thumb/<slug>`), so gallery cards and link previews (OpenGraph) show a nice still with a play badge.
- **Codec caveat:** raw `.mp4` (H.264) previews everywhere. GoPro `.mov` may contain HEVC/H.265, which downloads but may *not* preview inline in Discord/browsers. To guarantee inline preview, clip/convert to H.264 `.mp4` first.

## Notes

- API key is read from `PENGYSHARE_API_KEY` env var or `~/.secrets`.
- Max upload size: **512 MB** (configurable server-side + nginx cap).
- Supported formats: jpg, jpeg, png, gif, webp, bmp, tiff, svg + mov, mp4, m4v, webm, mkv.
- Thumbnails are auto-generated (300px WebP; ffmpeg poster for videos).
- Files are public by default; use `--unlisted` to hide from the index.

<!-- FILE: upload.py -->
#!/usr/bin/env python3
"""Upload an image or video to PengyShare and print the URL.

Usage:
    python upload.py <media_path> [options]
    echo <base64> | python upload.py - [options]

Reads API key from PENGYSHARE_API_KEY env var or ~/.secrets.
"""

import argparse
import base64
import json
import os
import sys
import urllib.request
import urllib.error
from pathlib import Path

# Set IMAGESHARE_URL to your own upload service.
BASE_URL = os.environ.get("IMAGESHARE_URL", "https://YOUR-IMAGESHARE-HOST")
SECRETS_FILES = [
    Path.home() / ".secrets",
    Path.home() / ".secrets",
]


def _read_secrets():
    """Read key=value pairs from ~/.secrets."""
    secrets = {}
    for file_ in SECRETS_FILES:
        if file_.exists():
            for line in file_.read_text().splitlines():
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    key, _, value = line.partition("=")
                    secrets.setdefault(key.strip(), value.strip())
    return secrets


def _get_api_key():
    """Get API key from env or secrets file."""
    key = os.environ.get("PENGYSHARE_API_KEY", "").strip()
    if not key:
        secrets = _read_secrets()
        key = secrets.get("PENGYSHARE_API_KEY", "").strip()
    if not key:
        print("Error: No API key found. Set PENGYSHARE_API_KEY or add to ~/.secrets", file=sys.stderr)
        sys.exit(1)
    return key


def _upload(media_path: str, api_key: str, unlisted: bool = False, stdin: bool = False) -> dict:
    """Upload an image/video file to PengyShare."""
    if stdin:
        # Read media data from stdin
        raw = sys.stdin.buffer.read()
        if not raw:
            print("Error: No data on stdin", file=sys.stderr)
            sys.exit(1)
        # Assume base64 if it looks like it, otherwise raw bytes
        try:
            media_data = base64.b64decode(raw.strip())
        except Exception:
            media_data = raw
        filename = "upload.bin"
    else:
        path = Path(media_path)
        if not path.exists():
            print(f"Error: File not found: {media_path}", file=sys.stderr)
            sys.exit(1)
        media_data = path.read_bytes()
        filename = path.name

    # Build multipart form data
    boundary = f"pengyshare_boundary_{os.urandom(8).hex()}"
    body = b""

    # file field (server accepts 'file' | 'image' | 'video')
    body += f"--{boundary}\r\n".encode()
    body += f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
    body += f"Content-Type: application/octet-stream\r\n\r\n".encode()
    body += media_data + b"\r\n"

    # unlisted field
    body += f"--{boundary}\r\n".encode()
    body += f'Content-Disposition: form-data; name="unlisted"\r\n\r\n{"1" if unlisted else "0"}\r\n'.encode()
    body += f"--{boundary}--\r\n".encode()

    url = f"{BASE_URL}/api/upload"
    req = urllib.request.Request(
        url,
        data=body,
        headers={
            "Content-Type": f"multipart/form-data; boundary={boundary}",
            "X-API-Key": api_key,
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        body = e.read().decode(errors="replace")
        print(f"Error: HTTP {e.code} โ€” {body}", file=sys.stderr)
        sys.exit(1)

    return result


def main():
    parser = argparse.ArgumentParser(description="Upload an image or video to PengyShare")
    parser.add_argument("media", help="Media file path, or '-' to read from stdin (base64)")
    parser.add_argument("-u", "--unlisted", action="store_true", help="Don't show on gallery index")
    parser.add_argument("-j", "--json", action="store_true", help="Output raw JSON")
    args = parser.parse_args()

    stdin = args.media == "-"
    api_key = _get_api_key()
    result = _upload(args.media, api_key, unlisted=args.unlisted, stdin=stdin)

    if result.get("ok"):
        if args.json:
            print(json.dumps(result))
        else:
            print(result["url"])
    else:
        print(f"Error: {result.get('error', 'Unknown')}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

Redaction report