A personal media library tool: list, search, play, cast, convert, and clean up video files across one or more directories — as a local CLI, a small HTTP server for remote access, or both from the same binary.
Originally a Python (Falcon) API server plus a separate Go CLI client. This is a from-scratch Go rewrite that merges both into one binary, with no implicit personal defaults — every directory it scans is one you tell it about explicitly.
go build -o tethys ./cmd/tethys
Or run directly with go run ./cmd/tethys ... during development.
A directory is any folder you want tethys to scan, given a label. There are two ways to add one, and you can mix both:
-directory label:/path/to/folder (-D for short),
repeatable. Directories don't need to share a parent — different
drives/mounts are fine.-directories /path/to/parent (-R for short),
repeatable. Every immediate subdirectory of that parent becomes its own
scanned directory, labeled by its own name — handy when you'd rather
mount one parent folder than list every subfolder by hand (see Docker
below). Re-checked on every listing, so a folder added later shows up
without restarting tethys serve.Nothing is scanned unless you configure at least one of these — there's no default directory.
~ and $HOME are expanded by tethys itself, not just relied on from the
shell — needed because -D label:~/path moves the ~ out of the one
position (start of a shell word) where your shell would normally expand
it, and because ~/.tor.yaml config values go through no shell at all.
A label starting with . (e.g. -D .private:/path, or a subdirectory
literally named .private under a -directories parent) is hidden by
default — but the dot is just a marker, not part of the name: it shows
up (and is revealed via -s all or -s private) as private, not
.private. This is a naming convention, not a permission — it just keeps
a folder out of your everyday listing. Labels are always lowercased.
The data directory (-data) holds tethysdb.json: which files
you've marked downloaded/hidden, plus bandwidth stats, with rotating
weekly backups. It's created automatically on first run. Not
dotfile-hidden on purpose — it's small and plain-text, worth being able to
ls/cat without -a. (JSON was chosen for exactly that reason: this
data is tiny — a couple of hash lists and a small stats map — so a real
embedded database would be more moving parts for no real benefit. If it
ever needs proper transactions or grows past "flat file" scale, a
pure-Go, no-cgo option like modernc.org/sqlite
or etcd-io/bbolt would be the
natural upgrade — pure Go matters here since a cgo-linked SQLite driver
adds a real cross-compilation/Docker-build burden a single JSON file
doesn't have.)
Scans directories directly — no server needed.
tethys -directory movies:/data/Movies -directory "shows:/data/TV Shows"
# auto-discovery: /data/Media/Movies, /data/Media/Shows, /data/Media/.Private
# become labels "movies", "shows", "private" (hidden) — no per-folder flag
tethys -R /data/Media
# short flag, search, JSON output
tethys -D movies:/data/Movies -search "die hard" -json
# a folder you don't want cluttering the default listing
tethys -D movies:/data/Movies -D .private:/data/private-stash
tethys -D movies:/data/Movies -D .private:/data/private-stash -s private
# hide/delete/cleanup (need a real hash from a listing first)
tethys -D movies:/data/Movies -hide abc12345
tethys -D movies:/data/Movies -d abc12345
tethys -D movies:/data/Movies -cleanup
# play (mpv, or vlc with -vlc), cast, convert, download
# note: flags must come before the hash — Go's flag parser treats
# everything after the first non-flag argument as positional
tethys -D movies:/data/Movies abc12345 # play by hash
tethys -D movies:/data/Movies -play-all -search movie
tethys -D movies:/data/Movies -cast -cast-uuid <chromecast-uuid> abc12345
tethys -D movies:/data/Movies -convert 480 abc12345
tethys -D movies:/data/Movies -wget abc12345
Run the scanner as an HTTP API (e.g. on a machine with the actual files), then talk to it from anywhere with the same CLI:
tethys serve -D movies:/data/Movies -addr :8080 -token mysecret
tethys -remote http://your-server:8080 -token mysecret -json
-remote mode doesn't scan locally at all — -directory/-D and
-directories/-R only matter on the serve side.
tethys serve serves the files themselves, not just listings — no separate
static file server (nginx, Caddy, etc.) needed. GET /get/<label>/<relpath>
streams the raw bytes straight from wherever that label's directory
actually lives, resolved fresh on every request (nothing is pre-scanned or
cached at startup, so a multi-hundred-GB library that's constantly growing,
shrinking, or being reorganized is never stale). play/cast/convert/wget
work with zero extra setup as a result — -file-base-url is only for the
rarer case where you genuinely want files served from somewhere else (a
CDN, a caching reverse proxy) instead.
Since mpv/vlc/wget/go-chromecast consume a bare URL and can't attach a
custom header, the token travels as a ?token=... query parameter on
/get/ URLs (the Token header still works too, e.g. for a browser
extension or curl). The JSON API (/api/files) is unaffected — it only
ever used the header.
Repeating -directory/-data/-token etc. on every invocation gets old —
put them in ~/.tor.yaml instead, under a tethys: key (other tools can
have their own top-level keys in the same file):
tethys:
data: /data
directories:
- "movies:/data/Movies"
- "shows:/data/TV Shows"
- ".private:/data/private-stash"
auto_directories:
- "/data/Media"
token: mysecret
addr: ":8080"
Precedence is flag > environment variable > config file > built-in default.
$TETHYS_CONFIG overrides the config file's path if you don't want
~/.tor.yaml.
| Flag | Env var | Config key |
|---|---|---|
-data | TETHYS_DATA | data |
-directory/-D | — | directories |
-directories/-R | — | auto_directories |
-remote | TETHYS_REMOTE | remote |
-token | TETHYS_TOKEN | token |
-file-base-url | TETHYS_FILE_BASE_URL | file_base_url |
-cast-uuid | TETHYS_CAST_UUID | cast_uuid |
-addr (serve) | — | addr |
Run tethys -h / tethys serve -h for the full flag list.
tethys serve is the piece that makes sense in a container — the CLI's
play/cast/convert/wget need mpv/vlc/ffmpeg/go-chromecast and a real
display/network presence, so build/run that part locally instead.
-directories/-R (auto-discovery) is the natural fit here: mount one
parent folder — laid out as movies/, shows/, .private/, etc. — to one
container path, instead of one volume + one -directory per label. Add a
new subfolder on the host later and it shows up without recreating the
container, since discovery happens fresh on every request.
docker build -t tethys .
docker run -d -p 8080:8080 \
-v tethys-data:/data \
-v /path/to/your/media:/media \
-e TETHYS_TOKEN=mysecret \
tethys serve -data /data -directories /media
(the trailing command overrides the Dockerfile's default CMD entirely, so
-data /data has to be repeated here — otherwise the mounted volume goes
unused and state lives in the container's ephemeral filesystem instead.)
If your directories genuinely live in separate places on the host (not
under one common parent), use -directory/-D instead — one -v +
-directory label:/container/path per directory. Both flags work together
if you need some explicit and some auto-discovered.
With Compose — copy docker-compose.yml, add a .env file next to it:
TETHYS_TOKEN=some-long-random-string
MEDIA_DIR=/path/to/your/media
then:
docker compose up -d
docker-compose.yml mounts $MEDIA_DIR to /media and passes
-directories /media — arrange $MEDIA_DIR as movies/, shows/,
.private/, etc. and each becomes its own label automatically. Edit it to
add explicit -directory mounts instead/as well if you need directories
that don't share a parent.
-downloads root with a hardcoded personal category list
(folder1, folder2, eskalaator, etanker, nsfw). Auto-discovery is
back (-directories/-R), but generalized: any parent you point it at,
not one baked-in path, and it now also composes with explicit
-directory/-D roots that don't need to share that (or any) parent.OpenDB auto-creates a fresh tethysdb.json if none exists, rather than
requiring one to be seeded by hand — needed for a first run against an
empty Docker volume to just work. It's also no longer dotfile-hidden.Content type
Image
Digest
sha256:39b16459c…
Size
6.1 MB
Last updated
25 days ago
docker pull tanel/tethys