Lowkey Media Server

A companion server for batch processing, AI tagging, transcription, and media ingestion.

Works on Windows and Linux

Alpha Release

Download | Free

Version 1.0.0

Overview

Lowkey Media Server is a companion application for the Lowkey Media Viewer that manages long-running offline tasks. Built in Go, it runs as an HTTP server on port 8090.

  • Job queue with persistence and real-time status updates
  • Media ingestion from local directories, YouTube, and gallery sites
  • AI-powered auto-tagging using ONNX models
  • LLM vision integration for image descriptions
  • Video transcription with Faster Whisper
  • Browser extensions for Chrome and Firefox
  • Web-based media gallery and search

Getting Started

Installation

Download and run the Media Server executable. On Windows, it runs in the system tray for easy access. On Linux, run the binary from the command line.

Initial Setup

On first launch the server creates a temporary admin / admin account and forces you through a setup wizard to replace it with a real user. The default admin is deleted as soon as your real account exists. See Users & Permissions for the full flow, JWT details, and how to add or remove users later.

Initial Setup Demo Video

Installing Dependencies

The Media Server uses external tools for media processing. On the setup page, you can download and install the required dependencies with a single click. Dependencies are extracted on demand when a task requires them.

Dependencies are downloaded to:

Windows: %ProgramData%\Lowkey Media Server\tmp\
Linux: /var/tmp/lowkeymediaserver/

Available dependencies include:

  • ffmpeg / ffprobe - Media processing and conversion
  • yt-dlp - Video downloading from YouTube and other sites
  • gallery-dl - Image gallery downloading
  • faster-whisper - Video transcription
  • ONNX Runtime - AI model inference for auto-tagging
Dependency Setup Demo Video

Web Interface

Access the web interface at http://localhost:8090. The home page shows the job queue with all running and completed jobs.

Web UI Demo Video

Browser Extension

Install the browser extension for Chrome or Firefox to quickly create jobs from any webpage.

  • Chrome: Load unpacked from chrome-extension/ folder
  • Firefox: Load from firefox-extension/ folder

Features:

  • One-click task creation with current page URL
  • Command selection dropdown
  • Custom arguments support
  • Real-time job status updates
Browser Extension Demo Video

Docker Quick Start

The fastest way to run Lowkey Media Server is with Docker. A single command gets you a working server with ffmpeg and all core dependencies pre-installed.

Run the Container

Pull the image and start the server:

docker run -d --name lowkey-media-server \
-p 8090:8090 \
-v lowkey-data:/data \
ghcr.io/stevecastle/lowkey-media-server:latest

Open http://localhost:8090 in your browser. The server is ready to use.

Local Storage

To browse media from your local filesystem, bind-mount your directories into the container and register them as storage roots with environment variables.

docker run -d --name lowkey-media-server \
-p 8090:8090 \
-v lowkey-data:/data \
-v /path/to/photos:/mnt/photos:ro \
-v /path/to/videos:/mnt/videos:ro \
-e LOWKEY_ROOT_1=/mnt/photos:Photos \
-e LOWKEY_ROOT_2=/mnt/videos:Videos \
ghcr.io/stevecastle/lowkey-media-server:latest

Each LOWKEY_ROOT_<N> variable registers a storage root. The format is path or path:label where the label is the display name shown in the UI. If omitted, the path is used as the label.

Mount directories as read-only (:ro) if you only want to browse and process media without the server modifying your files.

S3-Compatible Storage

Lowkey Media Server can browse and process media stored in any S3-compatible object storage service — AWS S3, MinIO, Backblaze B2, Cloudflare R2, DigitalOcean Spaces, and others.

Use the LOWKEY_ROOTS environment variable with a JSON array to configure S3 backends. You can mix local and S3 roots in the same configuration. Mark one root with "default": true to designate it as the upload and download destination — uploaded files and ingested media will be stored there. If no root is marked default, the first root is used.

S3 Only

docker run -d --name lowkey-media-server \
-p 8090:8090 \
-v lowkey-data:/data \
-e 'LOWKEY_ROOTS=[{
"type": "s3",
"label": "My Media Bucket",
"endpoint": "https://s3.us-east-1.amazonaws.com",
"region": "us-east-1",
"bucket": "my-media",
"prefix": "photos/",
"accessKey": "AKIAIOSFODNN7EXAMPLE",
"secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"thumbnailPrefix": "thumbs/",
"default": true
}]' \
ghcr.io/stevecastle/lowkey-media-server:latest

Mixed Local + S3

docker run -d --name lowkey-media-server \
-p 8090:8090 \
-v lowkey-data:/data \
-v ~/photos:/mnt/photos:ro \
-e 'LOWKEY_ROOTS=[
{"type": "local", "path": "/mnt/photos", "label": "Local Photos"},
{
"type": "s3",
"label": "Cloud Archive",
"endpoint": "https://s3.us-west-2.amazonaws.com",
"region": "us-west-2",
"bucket": "media-archive",
"accessKey": "AKIAIOSFODNN7EXAMPLE",
"secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"default": true
}
]' \
ghcr.io/stevecastle/lowkey-media-server:latest

S3 Storage Root Fields

FieldRequiredDescription
typeYesMust be "s3"
labelYesDisplay name in the UI
endpointYesS3 API endpoint URL
regionYesAWS region or equivalent
bucketYesBucket name
accessKeyYesAccess key ID
secretKeyYesSecret access key
prefixNoKey prefix to scope browsing (e.g. "photos/")
thumbnailPrefixNoSeparate prefix or bucket path for generated thumbnails
defaultNoSet to true to make this root the upload/download destination

Tested S3-Compatible Services

ServiceEndpoint Format
AWS S3https://s3.<region>.amazonaws.com
MinIOhttp://<host>:9000
Backblaze B2https://s3.<region>.backblazeb2.com
Cloudflare R2https://<account-id>.r2.cloudflarestorage.com
DigitalOcean Spaceshttps://<region>.digitaloceanspaces.com

Environment Variables

All server settings can be configured with environment variables, no config file needed.

VariableDefaultDescription
LOWKEY_DB_PATH/data/db/media.dbSQLite database path
LOWKEY_DOWNLOAD_PATH/data/mediaDownload directory (deprecated — use a default storage root instead)
LOWKEY_OLLAMA_BASE_URLhttp://host.docker.internal:11434Ollama API endpoint for LLM features
LOWKEY_OLLAMA_MODELllama3.2-visionVision model for image descriptions
LOWKEY_JWT_SECRET(auto-generated)JWT signing secret for authentication
LOWKEY_DISCORD_TOKENDiscord token for media export
LOWKEY_FASTER_WHISPER_PATHPath to faster-whisper binary
LOWKEY_ROOT_1, _2, ...Local storage roots (path or path:label)
LOWKEY_DEFAULT_ROOT1Which root receives uploads/downloads (1-based index or label)
LOWKEY_ROOTSJSON array of storage roots (set "default":true on one)

If you're running Ollama on the host machine for AI descriptions, the default LOWKEY_OLLAMA_BASE_URL will find it automatically on macOS and Windows. On Linux, add --add-host=host.docker.internal:host-gateway to the docker run command.

Data Persistence

All server state is stored in the /data volume inside the container. Using a named volume (lowkey-data) ensures your data survives container restarts, upgrades, and image rebuilds.

/data/
db/media.db — SQLite database
media/ — Downloaded and ingested media
config/ — Auto-generated configuration
packages/ — Auto-downloaded tools (yt-dlp, gallery-dl, etc.)

To back up your data, use docker cp or mount a host directory instead of a named volume:

docker run -d --name lowkey-media-server \
-p 8090:8090 \
-v /path/to/lowkey-data:/data \
ghcr.io/stevecastle/lowkey-media-server:latest

Users & Permissions

The media server uses JWT-based authentication. All admin pages, the App Experience, the Swipe App, and the JSON API require a valid session. The system is intentionally simple today: there is one role (admin) and one shared user list. Everyone you create has full access to the server.

First-Run Setup

On the very first launch — before any users exist — the server creates a temporary administrator account:

username: admin
password: admin

Visit http://localhost:8090, log in with those credentials, and you'll be redirected to a setup wizard at /login?setup=true that forces you to register a real account. The default admin is deleted as soon as your new user is created — there is no way to skip this step while the default admin is still present.

Pick a username and a strong password. Passwords are stored as bcrypt hashes; the server never stores or transmits them in plaintext.

Managing Users

Once you're logged in, open the Config tab in the web UI. The Users section shows every registered account and lets you:

  • Add a user — type a username and password and click Create.
  • Delete a user — click Delete next to the user you want to remove. The server refuses to delete the last remaining user so you can never lock yourself out.

The same operations are available over the JSON API for automation:

GET /auth/users # list users
POST /auth/users { username, password } # create user
DELETE /auth/users?username=alice # delete user

You'll need a valid session cookie or Authorization: Bearer <token> header from POST /auth/login to call any of these.

JWT & Sessions

POST /auth/login exchanges credentials for a JWT. The token is returned in the response body and set as an auth_token HttpOnly cookie (SameSite=Lax) for browser use. Subsequent requests are accepted with either the cookie or an Authorization: Bearer <token> header — the browser extensions and the Electron app use the header; the web UI uses the cookie.

  • Cookie lifetime: 24 hours from issue.
  • JWT lifetime: 1 year (Bearer-token clients stay logged in until they explicitly log out).
  • Logout: POST /auth/logout clears the cookie. Bearer tokens remain valid until expiry — there is no server-side token revocation list, so rotate the JWT secret if you need to invalidate everyone.
  • Signing secret: set LOWKEY_JWT_SECRET to a long random string in production. If unset, one is auto-generated and persisted into the config file on first run.

Roles & Permissions

Right now there is exactly one role: admin. Every authenticated user is treated as an admin and can do everything — manage other users, edit configuration, run any task, browse and modify all media. Per-route role separation (RolePublic vs. RoleAdmin) exists in the middleware so a more granular system can be added later without redesigning the auth flow, but until then:

  • Only create accounts for people you would trust with the box itself.
  • Don't expose the server to the open internet without putting it behind a reverse proxy with TLS and additional restrictions (e.g. IP allow-list or an OAuth front-door).
  • There is no rate limit on /auth/login yet — make sure passwords are strong, especially if the server is publicly reachable.

A future release will add proper role separation (read-only users, per-storage-root access, scoped API tokens). The underlying schema and middleware already account for it.

Account Recovery

There is no password-reset flow. If you lose your only password, recover access by stopping the server and clearing the users table from SQLite:

sqlite3 /path/to/media.db "DELETE FROM users;"

Restart the server. The default admin / admin account is recreated and you'll be sent through the setup wizard again. Your media, tags, jobs, and workflows are untouched — only the user list is reset.

Job Queue

The job queue manages all processing tasks with persistence and real-time updates via Server-Sent Events (SSE).

Creating Jobs

Create jobs through the web interface, browser extension, or API. Each job specifies:

  • Task type (ingest, autotag, metadata, etc.)
  • Input source (URL, file path, or directory)
  • Optional follow-up tasks
Job Creation Demo Video

Monitoring Progress

Jobs display real-time progress with live output streaming. Job states include:

  • Pending - Waiting to start
  • In Progress - Currently running
  • Completed - Finished successfully
  • Cancelled - Stopped by user
  • Error - Failed with error

Job Management

Manage jobs with these actions:

  • Cancel - Stop a running job
  • Copy - Duplicate a job configuration
  • Remove - Delete a job from the queue
  • Clear - Remove all non-running jobs

Media Ingestion

The ingest task scans and adds media to the database from multiple sources with optional follow-up processing.

Local Directories

Scan local directories to add media files to the database. Supports recursive scanning for nested folders.

Local Ingest Demo Video

YouTube Downloads

Download videos from YouTube and other sites using the bundled yt-dlp tool. Videos are automatically added to the database after download.

YouTube Download Demo Video

Download media from gallery sites using gallery-dl. Supports hundreds of sites including:

  • Twitter/X, Instagram, Reddit
  • DeviantArt, ArtStation, Pixiv
  • Tumblr, Flickr, and many more
Gallery Download Demo Video

AI & ML Features

Auto-Tagging (ONNX)

Automatically tag images using ML models (WD Tagger). Configure thresholds for general and character tags.

  • Batch processing of entire directories
  • Configurable confidence thresholds
  • Tags organized by category (Suggested, Character, etc.)
Auto-Tagging Demo Video

LLM Vision Descriptions

Generate image descriptions using Ollama with vision models (llama3.2-vision, etc.). Customize prompts for different use cases.

LLM Description Demo Video

Transcription

Generate transcripts for videos using Faster Whisper. Transcripts are saved as VTT files and can be viewed in the Media Viewer.

Transcription Demo Video

Media Browser

Browsing & Search

Browse your media collection through the web interface at /media. Full-text search across filenames, descriptions, and tags.

Media Browser Demo Video

File Serving

Stream media files directly from the server with caching headers for performance. Access files at /media/file?path=...

Swipe App

The Swipe App is a progressive web app (PWA) that provides a TikTok-like experience for browsing your media database. Install it on your smartphone to swipe through your collection with a mobile-optimized interface.

  • Installable PWA - Add to your home screen for a native app experience
  • Swipe Navigation - Swipe up/down to browse through media
  • Mobile Optimized - Designed for touch and vertical video viewing
  • Tag & Filter - Browse by tags and filter your collection

Access the Swipe App at http://[server-ip]:8090/swipe on your mobile device.

Swipe App Demo Video

App Experience

The full Lowkey Media Viewer runs inside any browser as a web app, served by the media server at http://[server-ip]:8090/app/. It is the same React interface as the Electron desktop app — same layout, same tagging panel, same hotkeys — but routed over HTTP instead of Electron IPC so you can reach your library from any computer on your network.

The static bundle is embedded into the server binary at compile time, so there is nothing extra to install. Log in once and the UI is ready to go.

  • Same UI, anywhere — the renderer is shared between the desktop app and the web app. Anything you can do in the Electron viewer (tagging, Battle Mode, transcripts, comic archives, search) works in the browser.
  • Remote-friendly — point a laptop, tablet, or second PC at your home server's IP and you have your entire library. Pair it with a reverse proxy and TLS for access from outside your network.
  • HLS playback for big videos — videos stream as adaptive HLS so large files start instantly and scrub cleanly even over a slow connection.
  • Authenticated — gated by the same login system as the rest of the server. See Users & Permissions.

Open http://[server-ip]:8090/app/ in a browser, sign in, and use the viewer exactly like you would on the desktop.

App Experience Demo Video

Available Tasks

Task IDNameDescription
ingestIngest MediaScan directories and add files to database
metadataGenerate MetadataGenerate descriptions, transcripts, hashes
autotagAuto TagML-based automatic image tagging
yt-dlpyt-dlpDownload videos from YouTube, etc.
gallery-dlgallery-dlDownload media from gallery sites
ffmpegffmpegProcess and convert media files
moveMove MediaMove files and update database references
removeRemove MediaRemove entries from database
cleanupCleanupRemove orphaned database entries
lora-datasetLoRA DatasetGenerate datasets for ML training

Configuration

Configure the server through the web interface at /config or edit the config file directly:

Windows: %APPDATA%\Lowkey Media Viewer\config.json
Linux: ~/.config/lowkeymediaviewer/config.json

Configuration Options

  • dbPath - Path to the SQLite database
  • ollamaBaseUrl - Ollama server URL (default: http://localhost:11434)
  • ollamaModel - Vision model for descriptions
  • fasterWhisperPath - Path to Faster Whisper executable
  • onnxTagger - ONNX model configuration for auto-tagging

API Reference

Job Management

MethodEndpointDescription
POST/createCreate a new job
GET/job/{id}View job details
POST/job/{id}/cancelCancel a running job
POST/job/{id}/copyCopy job configuration
POST/job/{id}/removeRemove a job
POST/jobs/clearClear non-running jobs

Media Operations

MethodEndpointDescription
GET/mediaMedia gallery page
GET/media/apiMedia JSON API with search
GET/media/fileServe media files

System

MethodEndpointDescription
GET/healthServer health and stats
GET/streamSSE stream for real-time updates
GET/configConfiguration page
POST/configUpdate configuration

Contact & Support

The Media Server is in alpha. Report issues and request features on GitHub or via email.