Tiny Tube is a self-hosted video-sharing server for training videos, presentations, and recorded meetings. It runs as a single Go binary, with an embedded web interface, SQLite metadata, and video files stored on disk.
- One binary: runs on Windows, Linux, and macOS. No separate database server or frontend setup is required.
- A searchable video library: categories, sorting, playback speed controls, chapter links, links to a playback position, and original-file downloads, with a multilingual interface.
- Optional HLS and posters: add FFmpeg for conversion and preview images. Without it, play original files in browsers that support their codecs.
- Local or LDAP login: editors upload and edit; administrators also delete videos and manage users. Roles are assigned locally.
- Public viewing or locked lists: share a group of videos through a secret link. Anyone with the link can view that list; editors and administrators can access all videos for management.
Download a release · Installation · Configuration · HTTP API · Changelog · License
Run one server process per video directory. HLS currently provides a single 720p rendition; backups, storage quotas, and adaptive quality are not built in. See the review and remaining limitations for deployment details.
Screenshots
The examples below use demonstration content.
Library: browse by category, search, and sort videos by date, views, or title.
Watch: play a video, adjust playback speed, and share or download the original. Use Copy link from here in the share menu to start the recipient at the current playback position. The link follows the same access rules as the video. When you return to an unfinished video in the same browser, choose Resume from the saved time or Start over. If subtitles are available, choose a language in the player's subtitle controls.
Upload: add a video and choose its title, category, description, and visibility.
To add chapters, start a line in the description with MM:SS or H:MM:SS,
followed by a space and a title. For example, 05:30 Demo becomes a link that
jumps to that point in the video. You can add chapters to existing videos by
editing their descriptions.
Installation
1. Download and extract
Download the archive for your system from GitHub Releases:
Windows (.zip), Linux, or macOS (.tar.gz), choosing amd64 for Intel/AMD
or arm64 for ARM, including Apple Silicon. Extract it and open a terminal in
the extracted folder. The binaries are unsigned; Windows or macOS may show a
security prompt.
The release contains the server, the tiny-tube-cli administration tool,
configuration examples, documentation, and VERSION.txt. Go and a separate
database server are not required: SQLite and the web interface are included.
You need a writable video directory with space for originals and generated media.
To verify the download, compare its hash with the matching filename in the
release's SHA256SUMS. For example, for the v1.1.0 amd64 archives:
# Windows PowerShell Get-FileHash .\tiny-tube-v1.1.0-windows-amd64.zip -Algorithm SHA256
# Linux sha256sum tiny-tube-v1.1.0-linux-amd64.tar.gz # macOS (use darwin-arm64 for Apple Silicon) shasum -a 256 tiny-tube-v1.1.0-darwin-amd64.tar.gz
Use the filename for the version and platform you downloaded.
2. Configure the first administrator
For a new installation, copy .env.example to .env in the extracted folder:
# Windows PowerShell Copy-Item .env.example .env
# Linux / macOS
cp .env.example .envOpen .env in a text editor and set ADMIN_PASS to a strong password. The
default administrator name is admin; change ADMIN_USER if you prefer.
The password cannot be empty or changeme. Keep .env private: it may contain
credentials. Do not replace an existing .env when upgrading.
3. Start the server
From that same extracted folder, run:
# Windows PowerShell .\tiny-tube.exe
# Linux / macOS
./tiny-tubeOpen http://localhost:8081 on the same computer. Sign in with the administrator
credentials, then use Upload video to add a video or Administration to
manage users. Press Ctrl+C in the terminal to stop the server.
On the first start with an empty user table, Tiny Tube creates the administrator.
After that, manage users and passwords at /admin; changing ADMIN_USER or
ADMIN_PASS does not change existing accounts. The library starts empty. By
default, videos and the SQLite database are stored in videos/ under the
working directory.
For HLS conversion and automatically generated posters, install FFmpeg separately
with the libx264 and AAC encoders, and make it available on PATH (or set
FFMPEG_BIN). Without FFmpeg, original-file playback remains available when the
browser supports the video's format and codecs.
The default address only accepts local connections. For network access, configure the address and HTTPS; see Configuration and the Nginx example. Plan backups of the videos and database; Tiny Tube does not create them automatically. See Security and remaining limitations.
For a long-running Linux deployment, configure a service with the intended
working directory, absolute VIDEO_DIR, and credentials. If conversion is
needed, ensure the service can find FFmpeg. A systemd unit is not included.
For operator role fixes without the web UI, the release's tiny-tube-cli
provides users list and users set-role --username U --role admin|editor|viewer.
Use the same VIDEO_DIR, STORE, and DB_PATH settings as the server. The CLI
refuses to demote the last active administrator.
Locked lists
Public videos stay visible to everyone. For sensitive material, an
administrator creates any number of locked lists at /admin/lists,
each with its own long random key. Uploads and edits
offer a visibility picker: public by default, or one locked list per video.
Existing videos stay public unless moved.
- Holders of
/view_list?key=…see exactly that list; without the key its videos are excluded from the library, search, category counts, and API listings. Direct requests for their watch page, metadata, originals, HLS segments, posters or subtitles return 404 unless a valid key, an unlock cookie or editor/administrator access applies. - The first key visit sets an unlock cookie so posters, HLS segments and later pages keep working without repeating the key.
- Editors and administrators see every video without a key, so lost links never block management.
- A link is a bearer credential: anyone it is forwarded to gains access. Share links only over a safe channel. Rotating the key revokes every copy at once, including previously granted cookies. Deleting a list is refused while it still holds videos, so videos cannot silently become public.
- The Nginx sample proxies every route through Go, so the same checks apply behind the proxy.
- Missing/unreadable metadata never authorizes media access. Orphan files without metadata return 404; recover their metadata before sharing them.
- Visibility changes are synchronized with media authorization. Failed list-picker
loads block upload/edit forms. An API edit omitting
access_listpreserves visibility; send an explicit empty value to make a video public. - Copy Link offers a locked-video URL only when its valid list key is present. Otherwise the page explains that the original locked-list link is required.
For scripts, pass the key as ?key=… on /api/videos, /api/video,
/watch and /videos/…, or reuse the unlock cookie from a link visit.
Configuration
The server loads .env from its working directory. Explicit environment variables
take precedence, including empty values. Values may be quoted; there is no variable
expansion or inline-comment support. .env is ignored by Git.
| Variable | Default | Behavior |
|---|---|---|
VIDEO_DIR |
videos |
Video storage; relative paths use the process working directory. |
ADDR |
unset | Full host:port bind address, overriding HOST and PORT. Default effective address is 127.0.0.1:8081. |
HOST |
127.0.0.1 |
Bind host when ADDR is unset. Set 0.0.0.0 explicitly for IPv4 network access. |
PORT |
8081 |
HTTP port when ADDR is unset; PORT=8080 remains supported. |
STORE |
sqlite |
Metadata backend: sqlite or legacy fs. Video files remain on disk in both modes. |
DB_PATH |
<VIDEO_DIR>/tiny-tube.db |
SQLite metadata database in WAL mode. Relative paths use the working directory. |
ADMIN_USER |
admin |
First administrator, created on first boot when no users exist. Ignored afterwards. |
ADMIN_PASS |
required on first boot | Password for the first administrator; startup rejects an unset/blank password or changeme when the user table is empty. |
AUTH_MODE |
local |
Login source: local, ldap, or both. In both, existing local accounts use their local password directly; other identities use LDAP. LDAP never adopts same-named local accounts, roles or keys. |
COOKIE_SECURE |
false |
Set true behind HTTPS so session cookies carry the Secure flag. |
LDAP_ADDR |
ldap://localhost:389 |
Directory address (ldap:// or ldaps://). |
LDAP_BASE_DN |
dc=example,dc=com |
Fallback search base and direct-bind default. |
LDAP_BIND_DN |
unset | Service account for subtree search; without it, users bind directly. |
LDAP_BIND_PASSWORD |
unset | Service account password. |
LDAP_SEARCH_BASES |
unset | ;-separated search bases enabling subtree search. |
LDAP_USER_FILTER |
(uid=%s) |
Search filter with the escaped username as %s. |
LDAP_USER_DN |
uid=%s,<base> |
Direct-bind DN template for the username. |
LDAP_ATTR_EMAIL |
mail |
Directory attribute mapped to the user record. |
LDAP_ATTR_FIRST_NAME |
givenName |
Directory attribute for the display name. |
LDAP_ATTR_LAST_NAME |
sn |
Directory attribute for the display name. |
LDAP_ALWAYS_UPDATE |
true |
Refresh LDAP user records from the directory on every login. |
MAX_UPLOAD_BYTES |
2147483648 |
Maximum whole multipart request, including form fields and overhead: 2 GiB. Larger requests return HTTP 413. |
TRANSCODE_WORKERS |
1 |
Maximum concurrent FFmpeg processes, between 1 and 16. Pending jobs remain on disk. |
AUTH_REQUESTS_PER_MINUTE |
60 |
Protected-route requests per source IP per minute, checked before authentication; 1–3600. Excess requests return 429 with Retry-After. |
UPLOAD_TIMEOUT_SECONDS |
1800 |
Total authenticated multipart read deadline; 1–86400 seconds. Stalled uploads return 408 and are cleaned up. |
TRANSCODE_TIMEOUT_SECONDS |
1800 |
Total HLS/poster conversion deadline; 1–86400 seconds. Expired jobs become failed, preserving originals. |
FFMPEG_BIN |
unset | Explicit executable path/name, preferred over PATH. An invalid explicit value fails startup. Otherwise FFmpeg is discovered on PATH. |
Empty optional values fall back to the defaults. For direct LAN access, set HOST
explicitly (or set ADDR=0.0.0.0:8081) and configure the firewall. Session cookies
need HTTPS when used over a network; the Go server itself serves HTTP. Video files
and public metadata are public. Login sessions last 24 hours sliding, with expiry
and the browser cookie renewed at most once per five minutes of activity. Refresh
only updates live sessions and cannot recreate a revoked token. Password resets,
deactivation and user deletion revoke sessions. Unlock cookies are session
cookies signed per process and die with the browser session or a restart;
reopening the list link restores them.
When upgrading an older build that automatically adopted local usernames into LDAP, review its existing LDAP role grants. The new login path prevents future adoption but preserves existing accounts/roles; it does not reconstruct their previous authentication source.
Browser mutations must originate from this site. Cross-origin upload/edit/delete
requests return 403, including JSON clients; command-line clients without browser
origin headers continue to work. Changing Accept/format=json does not bypass
authentication, request limits or origin protection. CSP blocks inline scripts
and embedding the interface in frames; MIME sniffing is disabled.
The auth limit uses the actual socket source address and ignores forwarding headers. Behind Nginx, clients share its IP bucket: tune the Go aggregate limit and apply per-client limits at the trusted edge. Public viewing/API reads remain available during an auth cooldown. Limiter memory is bounded.
Headers have a 10-second timeout and a 64 KiB limit. Ordinary request reads are limited to 30 seconds; authenticated uploads use their separate configured deadline. Response writes are limited to `max(30 minutes, UPLOAD_TIMEOUT_SECONDS
- 60 seconds)`, so unusually slow original downloads may need a larger setting.
Upload and playback
The interface supports English and Greek, Italian, Spanish, French, German and Simplified Chinese, including administration, upload validation and player messages. Use the language selector in the header; the choice is saved in a browser cookie for one year and works without JavaScript. Until a choice is saved, the interface defaults to Greek. Switching preserves the current page, search/filter/pagination parameters and locked-list key. Switching reloads the page, so save edits or finish an upload first. Dates, file sizes and view/video counts follow the selected language. Titles, descriptions, categories, list names and other user content retain their original text. JSON API fields, status values, errors, server logs and CLI output retain their existing format/language for automation.
English is the source catalog in web/locales/en.json; el.json translates the
same stable message IDs (for example upload.title). The server uses go-i18n
for named variables and CLDR plural rules, and golang.org/x/text for language
tags and number formatting. Browser messages use the same catalogs, with
Intl.PluralRules and Intl.NumberFormat; CSP needs no inline exceptions.
Missing translations fall back to English. To add another language, add its
standard go-i18n JSON catalog with a language.name entry and rebuild: discovery,
cookies and the selector automatically include it. No Go/HTML/JS
language lists need editing. See the translation guide for examples,
plural forms and validation. No frontend packages are required.
The interface defaults to light mode. The theme button switches between light and dark and remembers the selection in browser-local storage; it does not follow the operating system's theme automatically. Library, watch, and upload pages adapt to mobile, tablet, and desktop screens. Categories can be combined with text search. Library and locked-list pages show 24 videos by default, with Previous/Next navigation preserving search, category and sort. Long visibility labels fit narrow screens. Administrator deletion confirmations apply to every row and identify whether a video, user or list is being deleted.
The upload page supports file selection or drag-and-drop, shows the chosen file, and reports upload progress and errors without discarding the form. Without JavaScript, its standard multipart form still works. The watch page provides link copying and access to the original file. The interface uses restrained colors and task-oriented wording for workplace use. Cards display an extracted poster when available and a neutral placeholder otherwise. The player offers speeds from 0.5× to 2× and attempts playback with sound; where the browser blocks it, the viewer presses play. The video is never muted. Playback positions are stored locally in the browser and work for anonymous viewers. Returning to an unfinished video offers a choice before autoplay. A valid timestamp link takes precedence, and finishing the video clears its saved position.
The watch page links to /edit?id=<id> and provides deletion with a confirmation
prompt. GET /edit and POST /api/edit require an editor or administrator login;
POST /api/delete and /admin require an administrator. The watch page shows
management actions only to users holding the matching role. Editing changes title/description/category/visibility while
preserving playback data. Deletion cancels and waits for active conversion, then
removes the original, HLS, poster, subtitles and metadata. There is no undo.
Editors and administrators can add subtitles from Edit video. Choose a
UTF-8 WebVTT file (.vtt, up to 1 MiB) and enter
a language code such as el, en or en-US. Upload again with the same language
code to replace its file, or delete it from the list. Subtitles follow the video's
visibility and are available with both HLS and original-file playback.
Each authorized watch-page load counts one view, debounced to one per viewer
address per video every 24 hours so refreshes do not inflate totals; denied
requests never count. Totals appear on library cards and the watch page, and
the library and locked-list pages offer most-viewed and title sorts alongside
the default recency order. Title sorting is case-insensitive and orders numbers
naturally, so Lesson 2 comes before Lesson 10. The library and API accept
sort=views or sort=title; paging and filters preserve the selection.
- An authenticated
POST /api/uploadaccepts multipart fieldstitle(required),desc,category, andfile. - The original is saved to
VIDEO_DIR/<id>/. Recognized filename extensions are.mp4,.mov,.mkv, and.webm; unsupported extensions or mismatched container signatures return HTTP 415. This is a basic header check, not full codec/file validation. Titles are limited to 512 bytes, descriptions to 8,192, and categories to 256. A unique directory is reserved for each upload, and incomplete uploads are removed if saving the original or metadata fails. - If FFmpeg is available, metadata starts as
queued. A bounded worker marks itprocessing, generates HLS in a staging directory, and publishes it only after FFmpeg succeeds and produces a complete playlist. A JPEG poster is extracted at one second, falling back to the first frame; poster failure is non-fatal. Status then becomesready. Without FFmpeg, status is immediatelyreadyfor original-file playback. - Reload the watch page after conversion to use HLS. Safari can use native HLS;
other supported browsers use hls.js, with original-file fallback when HLS
support is unavailable or HLS playback fails. The page uses the vendored
hls.js 1.6.19 (
web/static/hls.min.js, Apache-2.0, seehls.js.LICENSE.txt); no external browser access is required. - Conversion failures become
failed, with a visible message and the original retained. Pending/interrupted jobs resume on startup if FFmpeg is available; otherwise they becomefailed. On graceful shutdown, active conversions are cancelled and requeued. Failed jobs are not automatically retried.
HTTP API
GET /api/videos is paginated: use page (starting at 1) and page_size
(default 50, maximum 100; HTML defaults to 24). Responses include total
(visible library size), filtered (matching visible videos), count (items on
this page), page, page_size, pages and items. Clients that assumed one
request returned every video must iterate through the pages. Out-of-range positive
pages resolve to the last page; invalid/oversized parameters return 400.
Authorization applies before counts, category totals or paging. SQLite filters
and pages in queries, retaining Greek search and precise date ordering; legacy
FS still scans JSON records. Readiness and conversion polling use lightweight
SQLite queries. View debounce memory has a hard 8,192-entry cap; eviction can
permit an extra view under heavy churn.
Programmatic clients log in with POST /api/login ({"username", "password"})
and reuse the session cookie, exactly like a browser. For scripts that should
not juggle cookies, an administrator can issue a long-lived API key per user
at /admin in the API keys section or with
cli.bat users api-key --username U --name N, and the client sends it as
Authorization: Bearer tt-…. Keys carry the user's role, never expire, and
are revoked from /admin or with users api-key-revoke --prefix P; only
their SHA-256 is stored and the plaintext is shown once at creation.
Uploads stay
multipart/form-data; only the response format changes. Send
Accept: application/json (or ?format=json) to get JSON instead of the
browser 303 redirect. Error responses use the same HTTP status codes with a
{"error": "..."} body. Anonymous JSON clients get 401; logged-in users
without the role get 403. Reading is public, like the library pages, except
locked-list videos (see above); writing
requires the matching role.
| Method | Route | Auth | Description |
|---|---|---|---|
POST |
/language |
none | Form fields language=<registered locale> (initially en or el) and optional site-local next; saves the browser language and redirects back. Same-origin protection applies. |
POST |
/api/login |
none | JSON {"username", "password"}; sets the session cookie, returns {"username", "role"}. |
POST |
/logout |
session | Revokes the session cookie. |
POST |
/api/upload |
editor | Multipart fields title (required), desc, category, file, access_list (empty for public, otherwise a locked-list id). JSON clients get 201 with id, title, status, original, access_list, watch_url, video_url, api_url. Browsers get 303 to /watch?id=. |
GET |
/view_list?key=… |
none | HTML page with exactly one locked list's videos; grants an unlock cookie. Without key, shows everything the cookie already unlocked. Unknown keys return 404. |
GET |
/api/videos?q=&category=&page=1&page_size=50 |
none | JSON fields total, filtered, count, page, page_size, pages, q, category, sort, and items, with the same filtering as the library page. Locked videos are excluded unless ?key=…, an unlock cookie, or an editor/administrator credential is presented. total counts all visible videos; filtered counts matches; count counts this page's items. Use sort=views for view count or sort=title for title; the default is recency. |
GET |
/api/video?id=<id> |
none | JSON metadata for one video, including status (queued/processing/ready/failed), access_list and views. 404 when missing or locked without access. |
POST |
/api/edit |
editor | Form fields id, title, desc, category, access_list. JSON clients get 200 with id, title, watch_url, api_url. |
POST |
/api/subtitles/upload |
editor | Multipart fields id, language, file. Adds or replaces the track for that language. The file limit is 1 MiB and the request also respects MAX_UPLOAD_BYTES. JSON clients get 200 with id and the canonical language. |
POST |
/api/subtitles/delete |
editor | Form fields id, language. JSON clients get 200 with id and language, or 404 if the video or track is missing. |
POST |
/api/delete |
admin | Form field id. JSON clients get 200 with {"id", "deleted": true}. |
Poll GET /api/video?id=<id> until status is ready (or failed), then
open watch_url. Serve over HTTPS when credentials cross a network; the Go
server itself serves HTTP (see the Nginx example).
curl -c session.txt -H "Content-Type: application/json" \ -d '{"username":"admin","password":"secret"}' http://127.0.0.1:8081/api/login curl -b session.txt -H "Accept: application/json" \ -F title="Demo" -F desc="Recorded meeting" -F category="Meetings" \ -F file=@demo.mp4 http://127.0.0.1:8081/api/upload curl "http://127.0.0.1:8081/api/video?id=<id>"
With an API key instead of the session cookie:
curl -H "Authorization: Bearer tt-..." -H "Accept: application/json" \ -F title="Demo" -F desc="Recorded meeting" -F category="Meetings" \ -F file=@demo.mp4 http://127.0.0.1:8081/api/upload
import os import requests base = "http://127.0.0.1:8081" s = requests.Session() s.post(f"{base}/api/login", json={"username": "admin", "password": os.environ["ADMIN_PASS"]}).raise_for_status() with open("demo.mp4", "rb") as f: r = s.post(f"{base}/api/upload", headers={"Accept": "application/json"}, data={"title": "Demo", "desc": "Recorded meeting", "category": "Meetings"}, files={"file": ("demo.mp4", f, "video/mp4")}) r.raise_for_status() video_id = r.json()["id"] print(requests.get(f"{base}/api/video", params={"id": video_id}).json())
Storage and conversion
SQLite uses the pure Go modernc.org/sqlite driver, so single-binary builds do
not require CGO. Legacy filesystem JSON metadata remains available with STORE=fs.
videos/
tiny-tube.db # SQLite metadata; WAL/SHM files may also exist
<id>/
original.mp4 # or .mov, .mkv, .webm
meta.json # legacy FS mode, or retained migration source
subtitles/ # optional, with one .vtt file per language code
track-el.vtt
hls-<generated>/ # published after successful conversion
index.m3u8
seg000.ts
poster.jpg # optional preview frame
...
HLS currently has a single rendition scaled to 720 pixels high, H.264 video, AAC audio, and a target segment duration of six seconds. It is not adaptive multi-quality streaming. Originals are kept after conversion.
Original files support HTTP Range requests, but seeking also depends on the file
layout and browser. The code does not apply MP4 faststart to originals. Multipart
file parts use at most an 8 MiB memory threshold before spilling to temporary disk;
the separate request limit is enforced by http.MaxBytesReader.
Existing videos, including root-level index.m3u8 and poster.jpg, remain compatible.
SQLite startup imports legacy meta.json records whose IDs are absent from the
database. Repeated imports leave existing SQLite edits untouched. JSON files are
retained; switching back to STORE=fs does not export later SQLite edits. Existing
SQLite databases are upgraded to retain conversion error messages and add
user/session tables without losing rows.
In FS mode, metadata is written to a temporary file, synced/closed, and replaced.
Application reads and writes use in-process synchronization. Run one server process per video
directory; shared storage across multiple instances is not supported.
Optional Nginx deployment
Go can serve everything itself. nginx.sample.conf proxies all
routes, including /videos/, through Go so its private-file and link checks apply.
Range/206 and HLS/poster content types are preserved. It is a starting point,
not a complete deployment configuration:
- Replace the sample hostname and storage path, and configure TLS certificates; the configured files must exist before Nginx can start.
- The sample has a 2 GiB whole-request cap and 300-second inactivity timeouts.
Keep the proxy cap consistent if changing
MAX_UPLOAD_BYTES. Upload request buffering is disabled to avoid an additional full copy at the proxy. - Restrict direct access to the Go port when TLS/auth routing relies on the proxy.
- Go already checks session roles. Additional Nginx upload authentication is optional and must not break the session cookie if enabled.
- Keep SQLite files and their journals private. The example blocks database
extensions and staging paths; Go also checks file identities to reject DB aliases.
Its upstream uses the default port 8081; adjust it if changing
ADDR/PORT.
Capacity has not been measured. Validate the edited configuration with nginx -t
on the deployment machine, then check a real authenticated upload and playback.
Development
Run and build from source
In a source checkout, install patched Go 1.26.10 or 1.27.3+; go.mod selects
Go 1.27.3. Copy .env.example to .env and set the first administrator password
as described in Installation. Then run
go run . from the repository directory, or build an executable:
# Windows PowerShell go build -o tiny-tube.exe . .\tiny-tube.exe
# Linux / macOS go build -o tiny-tube . ./tiny-tube
On Windows, dev.bat (or .\dev.bat in PowerShell) can rebuild and restart
the application as source files, assets, catalogs, module files, or .env change.
For the administration CLI, use cli.bat or go run ./cmd/cli with the same
storage settings as the server. The tracked video under testdata/ is a test
fixture, not library content.
To cross-build a Linux binary from Windows PowerShell, choose the target's architecture:
$env:GOOS = "linux" $env:GOARCH = "amd64" # or arm64 go build -o tiny-tube . Remove-Item Env:GOOS, Env:GOARCH
Tests and CI
GitHub Actions runs CI for every push and pull request, and can also be started manually from the Actions tab:
- Go tests on Linux and Windows, with real FFmpeg installed so HLS/poster tests run.
- Linux race detection, shuffled uncached tests, coverage reports retained for seven days, and CGO-free builds on both platforms.
gofmtfor changed Go files (matching the scoped-formatting policy),go vet, module checksum verification andgo mod tidy -diff.- The Node UI behavior suite and syntax checks for embedded JavaScript.
- Pinned
govulncheckscans for Linux, Windows and macOS using the live vulnerability database. Database/tool failures fail the job; they are not treated as a pass.
Pushing a version tag such as v1.0 or v1.0.1 runs these same checks first,
then cross-builds six CGO-free platform packages and publishes a GitHub Release
with all archives and SHA256SUMS. A failed check or platform build blocks release
publication. Add a heading such as ## v1.1.0 to CHANGELOG.md
before tagging. The matching section supplies the release notes, and every archive
includes the changelog. Linux/Windows tests execute natively; macOS and ARM packages are
cross-compiled, without native runtime tests for every target. Ordinary branch
pushes and pull requests do not publish releases.
Actions are pinned to commit SHAs, repository permissions default to read-only, jobs
have time limits, and superseded branch runs are cancelled. CI uses the Go
toolchain selected in go.mod. Only the final release job has permission to
publish downloads. It does not deploy the application. Branch
protection/required checks must be configured separately in repository settings.
go test ./...
go vet ./...
go build ./...Tests cover concurrent uploads, request limits, cleanup, metadata replacement,
authentication, HTTP playback/Range support, bounded conversion concurrency, and
shutdown/restart recovery, both storage backends, schema/import migration, and
editing/deleting during conversion. A real FFmpeg upload-to-HLS integration test runs when
FFmpeg is on PATH, and is skipped otherwise.
GET /healthz reports liveness. GET /readyz checks writable storage and readable
metadata, returning 503 if either fails; it does not certify FFmpeg codecs or disk
capacity. For manual verification, check search, authenticated upload, conversion,
and browser playback. Use a temporary VIDEO_DIR for checks that modify storage.
Contributor guidance is in AGENTS.md. Runtime uploads are ignored by
Git; only the fixture under testdata/ remains tracked. Back up video files and
the SQLite database, including a custom external DB_PATH. Stop the server before
copying a SQLite database, or use SQLite's backup facility; copying only the live
.db file may omit uncheckpointed WAL changes. There is no built-in quota,
recovery or backup mechanism.
HTML templates, styles, and scripts live under web/ and are embedded in the Go
executable. No frontend package installation or separate asset deployment is
needed. Rebuild/restart the server after editing these files.
The post-merge security check and its deployment limitations are recorded in docs/SECURITY.md. Keep video/database storage writable only by the service account, run FFmpeg with minimal OS privileges, use HTTPS for network access, and rebuild existing binaries with the selected patched toolchain.
The 2026-10-24 review and resolutions records
the additional fixes. Run node --test tests/ui.test.cjs for the client-behavior
suite (Node is a test tool, not a runtime dependency).
License
Tiny Tube is available under the MIT License. The vendored hls.js player is licensed under Apache-2.0. Third-party dependencies retain their respective licenses.


