Files
kaledonya-clips/README.md
T

224 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<h1 align="center">kaledonya clips</h1>
<p align="center">
<b>Share game clips that actually play inside Discord.</b><br>
A small self-hosted clip site on top of <a href="https://github.com/diced/zipline">Zipline</a>, dressed like Steam in 2003.
</p>
<p align="center">
<img alt="MIT license" src="https://img.shields.io/badge/license-MIT-c4b550?style=flat-square">
<img alt="Python 3.12" src="https://img.shields.io/badge/python-3.12-3e4637?style=flat-square&logo=python&logoColor=white">
<img alt="FastAPI" src="https://img.shields.io/badge/FastAPI-backend-3e4637?style=flat-square&logo=fastapi&logoColor=white">
<img alt="Docker Compose" src="https://img.shields.io/badge/docker-compose-3e4637?style=flat-square&logo=docker&logoColor=white">
<img alt="Built on Zipline 4.8" src="https://img.shields.io/badge/built%20on-Zipline%204.8-3e4637?style=flat-square">
<img alt="No build step" src="https://img.shields.io/badge/frontend-no%20build%20step-3e4637?style=flat-square">
</p>
<p align="center">
<img src="docs/upload.gif" width="540" alt="Uploading two clips: send, prepare for Discord with a live percentage, then the links appear with Copy buttons">
</p>
## Why
Discord only shows a video player for a linked file it can download in about **8 seconds**. From a home
connection that's around 30 MB, while a few minutes of 1080p is hundreds. Post a raw link to a big clip and your
friends get a grey box.
So for every big clip, the server makes a small 720p copy that is only used for Discord's player, and it holds
the link back until that copy is ready (Discord caches whatever it sees first). The original stays untouched and
plays at full quality on the site.
<table>
<tr>
<td width="50%"><img src="docs/library.png" alt="Library: a grid of clips with thumbnails, views and days left"><br><sub><b>Library:</b> only your own clips, with thumbnails, views and how long each one has left</sub></td>
<td width="50%"><img src="docs/watch.png" alt="Watch page: the clip in a player with Copy link, Download, Rename and Delete"><br><sub><b>Watch page:</b> the <code>/v/</code> link you share, which gets a proper player in Discord and Telegram</sub></td>
</tr>
<tr>
<td width="50%"><img src="docs/login.png" alt="Login screen styled like an old Steam window"><br><sub><b>Login:</b> invite only, with your own Zipline accounts</sub></td>
<td width="50%"><img src="docs/upload-done.png" alt="Upload finished: two links with Copy buttons"><br><sub><b>Done:</b> links only show up once they'll play in Discord</sub></td>
</tr>
</table>
## What you get
- **Library** of your own clips (each account only sees its own), with thumbnails, rename and delete
- **Watch page** at `/v/<name>`: the link you share. Discord, Telegram and others get a proper video embed
- **Uploads** by drag and drop, up to 5 GB, as a three-step install dialog:
*Send → Prepare for Discord → Link ready*, with real progress and time left at each step
- **Clean names:** spaces become dashes, a taken name gets `-2`, `-3`, ...
- **`.mkv` → `.mp4`** automatically (stream copy, no quality loss), so OBS recordings play in browsers and Discord
- **Clips delete themselves** after 90 days (configurable)
- **Invite only:** you make the accounts, nobody can sign up
- The raw file is still at `https://<domain>/<name>` and Zipline's own dashboard at `/dashboard`
## How it fits together
```
browser / Discord
|
Caddy (or a Cloudflare Tunnel)
|-- / /v/* /_site/* POST /upload --> intake
'-- everything else (clips, /dashboard, /api) --> Zipline --> Postgres
intake = this repo (Python/FastAPI + plain HTML/CSS/JS): the site, uploads, ffmpeg remux and Discord copies
Zipline = stores the files, users, expiry and thumbnails
```
intake never stores passwords. Logging in checks the username and password with Zipline and keeps that user's
Zipline token in an HttpOnly cookie, so every request runs as that user.
## Requirements
- A Linux machine with Docker and Docker Compose v2
- A domain (or subdomain) you control
- Either open ports 80 and 443 (option A) or a free Cloudflare account (option B)
- CPU for the Discord copies: a 3-minute 1080p clip takes about a minute on 4 threads of a laptop i7
## Setup
```bash
git clone https://git.userevolt.app/esef/kaledonya-clips.git
cd kaledonya-clips
cp .env.example .env
```
Edit `.env`: set `SITE_DOMAIN`, and fill `POSTGRESQL_PASSWORD` and `CORE_SECRET` with two different random
strings (`openssl rand -hex 32` makes one). The rest is optional and explained in the file.
### Option A: Caddy (open ports, automatic HTTPS)
1. Point your domain's DNS (A/AAAA record) at the machine and open ports 80 and 443.
2. Keep `COMPOSE_PROFILES=caddy` in `.env`.
3. `docker compose up -d --build`
Caddy gets a certificate for the domain by itself. Uploads go to the same address, so there's nothing else to set.
### Option B: Cloudflare Tunnel (no open ports)
1. In Cloudflare Zero Trust → Networks → Tunnels, create a tunnel and copy its token into `TUNNEL_TOKEN`.
Set `COMPOSE_PROFILES=tunnel`.
2. Add two public hostname routes for your domain, **in this order** (the first match wins):
1. path `^/($|v/|_site/)` → `http://intake:8000`
2. no path → `http://zipline:3000`
3. **Uploads can't go through Cloudflare:** it refuses request bodies over 100 MB. Give intake a second address
that doesn't go through Cloudflare and set `UPLOAD_URL` to it. That can be a VPN such as Tailscale
(`tailscale serve --bg --https=10000 http://127.0.0.1:3020` with a `ports: ["127.0.0.1:3020:8000"]` line added to
`intake`), or a separate subdomain with its own reverse proxy. Set `UPLOAD_HINT` to tell people what to do when
they can't reach it, e.g. `Connect to Tailscale to upload.` Watching and sharing clips works for everyone either way.
4. In Cloudflare → Caching → Cache Rules, add a rule for the domain that **bypasses the cache**. Cloudflare
caches anything ending in `.mp4`, which otherwise makes deleted or renamed clips stick around.
5. `docker compose up -d --build`
### First account and friends
1. Open `https://<domain>/dashboard`. Zipline asks you to create the first account; it becomes the admin.
2. Make accounts for friends in the Zipline dashboard's user admin.
3. Everyone logs in at `https://<domain>/` with their Zipline username and password.
## Using it
- Drag clips onto the page (or use Upload). When the dialog says **Link ready**, copy the link and post it.
- Share the `/v/<name>` link, not the raw file. That's the one with the Discord player.
- If you posted a link to Discord before it said ready and got no player, Discord has cached the broken embed.
Post it again with `?1` on the end.
- Clips over about 6⅓ minutes get no Discord copy: at that length a ~22 MB file would look too bad to watch.
They still play on the site, and Discord shows a plain link.
### Uploading from a script or ShareX
`POST https://<domain>/upload` with the file in a form field named `file` and your Zipline token (copy it in the
Zipline dashboard's settings) in the `Authorization` header:
```bash
curl -H "Authorization: $ZIPLINE_TOKEN" -F "file=@my clip.mkv" https://clips.example.com/upload
# {"url":"https://clips.example.com/my-clip.mp4","name":"my-clip.mp4","remuxed":true}
```
With option B this has to go to `UPLOAD_URL`, not through Cloudflare.
## Settings
All in `.env`; see `.env.example` for the full list.
| Setting | Default | What it does |
|---|---|---|
| `SITE_DOMAIN` | required | Your domain, without `https://` |
| `SITE_NAME` | `kaledonya clips` | Name in the tab, logo and embeds; its first word is the big login-screen word |
| `SERVER_NAME` | `the server` | "Sending to …" and the status bar |
| `UPLOAD_URL` | `https://SITE_DOMAIN` | Where browsers send uploads |
| `UPLOAD_HINT` | `Can't reach the upload server.` | Shown when an upload can't connect |
| `CLIP_LIFETIME` | `90d` | When clips delete themselves |
| `INTAKE_CPUS` | `4` | CPU cap for uploads and Discord copies. Lower it if the machine runs hot |
| `THUMBNAIL_INTERVAL` | `5m` | How often Zipline makes library thumbnails for new uploads |
The Discord copy sizes are at the top of `intake/app/previews.py`: up to 4 minutes → 720p in ~22 MB, longer →
480p in ~18 MB. If your upload speed is well above 4 MB/s you can raise them.
## Things to know
- **Links are public.** Anyone with a link can watch the clip, and names are readable words. Rename a clip to
something unguessable if it shouldn't be found.
- Discord copies are made one at a time in the background, at low CPU priority. Each one takes about a third of
the clip's length on 4 threads.
- Clips, the database and Caddy's certificates are stored in `./data`. intake's scratch space (uploads in
progress, `.mkv` conversion, Discord copies) is the `intake-work` Docker volume. To back up the database:
`docker compose exec zipline-db pg_dump -U zipline zipline | gzip > zipline.sql.gz`.
- Converting a `.mkv` briefly needs about 3× the file size free for the `intake-work` volume.
- Library thumbnails are made by Zipline every few minutes (`THUMBNAIL_INTERVAL`), so a new upload shows a
plain tile until then.
## Updating
```bash
cd kaledonya-clips
git pull
docker compose up -d --build
```
Your `.env`, `./data` and the `intake-work` volume are left alone. Compare `.env.example` with your `.env` for new settings.
## Troubleshooting
| Problem | Fix |
|---|---|
| A link shows no player in Discord | It was posted before the Discord copy was ready, and Discord cached the broken embed. Post it again with `?1` on the end. Wait for **Link ready** next time. |
| Long clips never get a player | Clips over about 6⅓ minutes get no copy on purpose. They play on the site, and Discord shows a plain link. |
| Uploads fail at 100 MB (option B) | That's Cloudflare's limit. Set `UPLOAD_URL` to an address that doesn't go through Cloudflare (see Option B, step 3). |
| Upload says "Can't reach the upload server" | The browser can't reach `UPLOAD_URL`. With a separate upload address (option B), opening `<UPLOAD_URL>/health` in that browser should show `{"ok":true}`. |
| Library tiles stay blank | Zipline makes thumbnails every `THUMBNAIL_INTERVAL` (5 min by default). |
| Caddy has no certificate / browser warns | The domain's DNS must point at this machine and ports 80 and 443 must be reachable. `docker compose logs caddy` says why. |
| A deleted or renamed clip still shows (option B) | Cloudflare cached it. Add the cache-bypass rule from Option B, step 4. |
| Login says "Too many tries" | 5 wrong passwords from one IP for one username in 10 minutes. Wait 10 minutes. |
| Something else | `docker compose logs intake` and `docker compose logs zipline` usually say what went wrong. |
## Security notes
Worth knowing before you hand out accounts:
- **The login cookie holds the user's Zipline API token** (HttpOnly, Secure, SameSite=Lax, 30 days). Logging out
deletes the cookie but doesn't change the token. If you think a token leaked, reset it in the Zipline dashboard.
- **Links are public by design.** Anyone with a clip's link can watch it, and an account can find out whether a
name is taken (an upload gets `-2`, a rename says it's used).
- **Login attempts are limited per IP and username:** 5 wrong passwords in 10 minutes and that pair has to wait.
The IP comes from `CF-Connecting-IP` or `X-Forwarded-For`. The Caddyfile strips the Cloudflare header and Caddy
replaces `X-Forwarded-For`, so neither can be faked. If you expose intake's port some other way (a VPN for
uploads, say), anyone who can reach it can fake those headers, so only expose it to people you trust.
- The no-login routes (`/v/<name>`, thumbnails) run ffmpeg/ffprobe on existing clips. At most two run at
once, and results are cached.
- intake runs as an unprivileged user, and ffmpeg runs at low CPU priority.
## Development
```bash
cd intake
python -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
.venv/bin/python -m pytest -q
```
The tests use a fake Zipline, so they don't need Docker. The few that need `ffmpeg` are skipped without it.
## Credits and license
Built on [Zipline](https://github.com/diced/zipline) by diced. The look is a nod to Steam's 2003 client.
MIT licensed, see [LICENSE](LICENSE).