Watch Kick subscriber-only VODs and cut clips out of them, straight from the browser.
Live: kicknosubviewer.duckdns.org
Paste a Kick VOD link, hit Play, and the video runs. Don't have a link? Type a channel name and browse its VODs from the grid. No account, no subscription, nothing to install. Found a moment worth keeping? Mark in, mark out, download the clip. The recording happens in your browser, so the file never leaves your machine and the server never stores it.
- Paste any Kick VOD URL (including sub-only) → instant playback via HLS
- Or browse a channel: autocomplete on the channel name, then a paginated grid of its VODs
- Mark In/Out points on the video timeline
- Download the clipped segment as WebM, recorded client-side
- Playback speed control
- No login required, no subscription needed
- Dark theme, responsive UI
- Reads video metadata from
kick.com/api/v1/video/<uuid>, which returns usable JSON even behind a 403 - Uses
cloudscraperto get past the Cloudflare protection on Kick's API - Pulls the channel ARN and recording ID out of the thumbnail URLs returned by the v2 API
- Brute-forces the exact stream URL by testing ±10 minute offsets against 3 known CDN base URLs, since the path encodes the stream start time and the metadata timestamp does not match it exactly
- Plays the resulting HLS stream client-side via HLS.js
- Clips are captured in the browser with
MediaRecorder, so nothing is transcoded or stored server-side
Step 4 is the fragile part. The CDN base URLs are hardcoded in BASE_URLS in server.py. If Kick moves to a different CDN prefix, or changes its thumbnail URL format, resolution stops working and the failure surfaces to the user as "stream not found" rather than as a server error. That is the first place to look when the site stops resolving VODs.
The frontend is served from the same Flask app and talks to three endpoints. All are rate-limited per IP by flask-limiter, using in-memory storage, so the counters reset whenever the process restarts.
| Endpoint | Limit | Purpose |
|---|---|---|
POST /api/resolve |
10/min | Body {"url": "<kick vod url>"} → resolved HLS URL and title |
GET /api/channel?channel=<name> |
15/min | Channel profile: avatar, banner, followers, verified flag, current live state |
GET /api/videos?channel=<name>&page=<n> |
15/min | One page of VODs: id, title, thumbnail, duration, date, viewers, plus hasMore |
Channel names are validated against ^[a-zA-Z0-9_-]+$ and the page number is clamped to 1-50 before anything is forwarded to Kick.
- Backend: Python/Flask + Gunicorn + cloudscraper, rate-limited with flask-limiter
- Frontend: Vanilla HTML/JS + HLS.js
- Clip engine: browser
MediaRecorder(VP9/Opus when supported) - Process manager: PM2
- Reverse proxy: Nginx
- Python 3.8+
- Node.js (for PM2)
- Nginx (optional, for reverse proxy)
git clone https://github.com/Pkkls/kicknosub-web.git
cd kicknosub-web
pip install -r requirements.txtpython server.py
# → http://localhost:3001PM2 runs Gunicorn directly, with interpreter: 'none', so Node is only there as a process supervisor.
pm2 start ecosystem.config.js
pm2 save
# then point nginx at 127.0.0.1:3001ecosystem.config.js hardcodes /home/ubuntu/kicknosub-web and the Gunicorn path under /home/ubuntu/.local/bin. Adjust both if you deploy anywhere else. Gunicorn runs 2 workers with a 120s timeout, since resolving a VOD can take several seconds of probing.
deploy.sh is stale: it still installs FFmpeg and runs npm install, neither of which this version needs. Use the steps above instead.
- Copy a Kick VOD URL (e.g.
https://kick.com/channel/videos/uuid) - Paste it in the input field and click Play
- The video loads in the HLS player
- Open the Browse tab
- Start typing a channel name, the autocomplete resolves it and shows the channel card with its live state
- Pick a VOD from the grid, Load more pages through the older ones
- Selecting a VOD sends it to the player
- Use Mark In / Mark Out to set the clip boundaries
- Click Download Clip
The clip is recorded in real time by the browser: playback seeks to the In mark, runs through the range, and MediaRecorder captures the video element's stream as it goes. A three minute clip takes three minutes, or half that at 2x playback speed. Keep the tab in the foreground while it records, browsers throttle timers and rendering in background tabs and the capture suffers for it. Nothing is uploaded and the server never sees the file.
- Inspired by KickNoSub (Python CLI + Chrome extension)
- Uses Kick API for metadata
- HLS.js for client-side playback
For educational and research purposes only. Support creators by subscribing on Kick.