pengyshare
Upload images and videos to a self-hosted file-sharing service with API-key auth and get back a shareable URL.
Downloads: 8 ยท ID: 6a1ab19b9fb74c1f69000000
Upload images and videos to a self-hosted file-sharing service with API-key auth and get back a shareable URL.
Downloads: 8 ยท ID: 6a1ab19b9fb74c1f69000000
<!-- 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()